From ebbe06d5874aa5d0575c770d4efb729023bf499e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Mon, 9 Jul 2018 00:21:24 +0200 Subject: [PATCH] Refactor MAM into own module - Rework the MAM Preference dialog - Move MAM Preference dialog into a new gtk module - Refactor all MAM code into own module - Refactor the MAM code itself so we can easier test it in the future - Add a misc module for smaller XEPs and move EME, Last Message Correction Delay, OOB into it - Add dedicated module for XEP-0082 Time Profiles --- gajim/app_actions.py | 12 +- gajim/application.py | 2 +- gajim/chat_control.py | 9 +- gajim/common/app.py | 8 +- gajim/common/config.py | 1 - gajim/common/connection.py | 12 +- gajim/common/connection_handlers.py | 63 +- gajim/common/connection_handlers_events.py | 336 +--------- gajim/common/helpers.py | 144 +--- gajim/common/logger.py | 12 +- gajim/common/message_archiving.py | 372 ----------- gajim/common/modules/date_and_time.py | 144 ++++ gajim/common/modules/mam.py | 626 ++++++++++++++++++ gajim/common/modules/misc.py | 113 ++++ .../gui/archiving_313_preferences_item.ui | 143 ---- .../gui/archiving_313_preferences_window.ui | 214 ------ gajim/data/gui/mam_preferences.ui | 236 +++++++ gajim/dialogs.py | 164 ----- gajim/groupchat_control.py | 9 +- gajim/gtk/__init__.py | 0 gajim/gtk/mam_preferences.py | 158 +++++ gajim/gtk/util.py | 54 ++ gajim/gui_interface.py | 4 +- gajim/history_sync.py | 24 +- gajim/roster_window.py | 2 +- gajim/server_info.py | 3 +- 26 files changed, 1403 insertions(+), 1462 deletions(-) delete mode 100644 gajim/common/message_archiving.py create mode 100644 gajim/common/modules/date_and_time.py create mode 100644 gajim/common/modules/mam.py create mode 100644 gajim/common/modules/misc.py delete mode 100644 gajim/data/gui/archiving_313_preferences_item.ui delete mode 100644 gajim/data/gui/archiving_313_preferences_window.ui create mode 100644 gajim/data/gui/mam_preferences.ui create mode 100644 gajim/gtk/__init__.py create mode 100644 gajim/gtk/mam_preferences.py create mode 100644 gajim/gtk/util.py diff --git a/gajim/app_actions.py b/gajim/app_actions.py index 14329e49f..9e3bce2be 100644 --- a/gajim/app_actions.py +++ b/gajim/app_actions.py @@ -29,6 +29,7 @@ from gajim import history_window from gajim import disco from gajim.history_sync import HistorySyncAssistant from gajim.server_info import ServerInfoDialog +from gajim.gtk.mam_preferences import MamPreferences # General Actions @@ -181,14 +182,13 @@ def on_import_contacts(action, param): # Advanced Actions -def on_archiving_preferences(action, param): +def on_mam_preferences(action, param): account = param.get_string() - if 'archiving_preferences' in interface.instances[account]: - interface.instances[account]['archiving_preferences'].window.\ - present() + window = app.get_app_window(MamPreferences, account) + if window is None: + MamPreferences(account) else: - interface.instances[account]['archiving_preferences'] = \ - dialogs.Archiving313PreferencesWindow(account) + window.present() def on_history_sync(action, param): diff --git a/gajim/application.py b/gajim/application.py index 375fb2bb3..e076abbc5 100644 --- a/gajim/application.py +++ b/gajim/application.py @@ -356,7 +356,7 @@ class GajimApplication(Gtk.Application): ('-profile', app_actions.on_profile, 'feature', 's'), ('-xml-console', app_actions.on_xml_console, 'always', 's'), ('-server-info', app_actions.on_server_info, 'online', 's'), - ('-archive', app_actions.on_archiving_preferences, 'feature', 's'), + ('-archive', app_actions.on_mam_preferences, 'feature', 's'), ('-sync-history', app_actions.on_history_sync, 'online', 's'), ('-privacylists', app_actions.on_privacy_lists, 'feature', 's'), ('-send-server-message', diff --git a/gajim/chat_control.py b/gajim/chat_control.py index d4d21fce9..386c272b9 100644 --- a/gajim/chat_control.py +++ b/gajim/chat_control.py @@ -809,8 +809,13 @@ class ChatControl(ChatControlBase): def _nec_mam_decrypted_message_received(self, obj): if obj.conn.name != self.account: return - if obj.with_ != self.contact.jid: - return + + if obj.muc_pm: + if not obj.with_ == self.contact.get_full_jid(): + return + else: + if not obj.with_.bareMatch(self.contact.jid): + return kind = '' # incoming if obj.kind == KindConstant.CHAT_MSG_SENT: diff --git a/gajim/common/app.py b/gajim/common/app.py index e414f40a3..e293b37c6 100644 --- a/gajim/common/app.py +++ b/gajim/common/app.py @@ -595,11 +595,17 @@ def prefers_app_menu(): return False return app.prefers_app_menu() -def get_app_window(cls): +def get_app_window(cls, account=None): for win in app.get_windows(): if isinstance(cls, str): if type(win).__name__ == cls: + if account is not None: + if account != win.account: + continue return win elif isinstance(win, cls): + if account is not None: + if account != win.account: + continue return win return None diff --git a/gajim/common/config.py b/gajim/common/config.py index 29e8fa170..a63637a10 100644 --- a/gajim/common/config.py +++ b/gajim/common/config.py @@ -305,7 +305,6 @@ class Config: 'use_keyring': [opt_bool, True, _('If true, Gajim will use the Systems Keyring to store account passwords.')], 'pgp_encoding': [ opt_str, '', _('Sets the encoding used by python-gnupg'), True], 'remote_commands': [opt_bool, False, _('If true, Gajim will execute XEP-0146 Commands.')], - 'mam_blacklist': [opt_str, '', _('All non-compliant MAM Groupchats')], }, {}) __options_per_key = { diff --git a/gajim/common/connection.py b/gajim/common/connection.py index be49fe65a..7496d0106 100644 --- a/gajim/common/connection.py +++ b/gajim/common/connection.py @@ -121,9 +121,6 @@ class CommonConnection: self.privacy_rules_supported = False self.vcard_supported = False self.private_storage_supported = False - self.archiving_namespace = None - self.archiving_supported = False - self.archiving_313_supported = False self.roster_supported = True self.blocking_supported = False self.addressing_supported = False @@ -1611,12 +1608,11 @@ class Connection(CommonConnection, ConnectionHandlers): if obj.fjid == our_jid: if nbxmpp.NS_MAM_2 in obj.features: - self.archiving_namespace = nbxmpp.NS_MAM_2 + self.get_module('MAM').archiving_namespace = nbxmpp.NS_MAM_2 elif nbxmpp.NS_MAM_1 in obj.features: - self.archiving_namespace = nbxmpp.NS_MAM_1 - if self.archiving_namespace: - self.archiving_supported = True - self.archiving_313_supported = True + self.get_module('MAM').archiving_namespace = nbxmpp.NS_MAM_1 + if self.get_module('MAM').archiving_namespace: + self.get_module('MAM').available = True get_action(self.name + '-archive').set_enabled(True) for identity in obj.identities: if identity['category'] == 'pubsub': diff --git a/gajim/common/connection_handlers.py b/gajim/common/connection_handlers.py index 952e768d9..1566345de 100644 --- a/gajim/common/connection_handlers.py +++ b/gajim/common/connection_handlers.py @@ -45,8 +45,8 @@ from gajim.common.caps_cache import muc_caps_cache from gajim.common.protocol.caps import ConnectionCaps from gajim.common.protocol.bytestream import ConnectionSocks5Bytestream from gajim.common.protocol.bytestream import ConnectionIBBytestream -from gajim.common.message_archiving import ConnectionArchive313 from gajim.common.connection_handlers_events import * +from gajim.common.modules.misc import parse_eme from gajim.common import ged from gajim.common.nec import NetworkEvent @@ -295,7 +295,9 @@ class ConnectionHandlersBase: # XEPs that are based on Message self._message_namespaces = set([nbxmpp.NS_HTTP_AUTH, nbxmpp.NS_PUBSUB_EVENT, - nbxmpp.NS_ROSTERX]) + nbxmpp.NS_ROSTERX, + nbxmpp.NS_MAM_1, + nbxmpp.NS_MAM_2]) app.ged.register_event_handler('iq-error-received', ged.CORE, self._nec_iq_error_received) @@ -303,10 +305,6 @@ class ConnectionHandlersBase: self._nec_presence_received) app.ged.register_event_handler('message-received', ged.CORE, self._nec_message_received) - app.ged.register_event_handler('mam-message-received', ged.CORE, - self._nec_message_received) - app.ged.register_event_handler('mam-gc-message-received', ged.CORE, - self._nec_message_received) app.ged.register_event_handler('decrypted-message-received', ged.CORE, self._nec_decrypted_message_received) app.ged.register_event_handler('gc-message-received', ged.CORE, @@ -319,10 +317,6 @@ class ConnectionHandlersBase: self._nec_presence_received) app.ged.remove_event_handler('message-received', ged.CORE, self._nec_message_received) - app.ged.remove_event_handler('mam-message-received', ged.CORE, - self._nec_message_received) - app.ged.remove_event_handler('mam-gc-message-received', ged.CORE, - self._nec_message_received) app.ged.remove_event_handler('decrypted-message-received', ged.CORE, self._nec_decrypted_message_received) app.ged.remove_event_handler('gc-message-received', ged.CORE, @@ -460,37 +454,15 @@ class ConnectionHandlersBase: app.plugin_manager.extension_point( 'decrypt', self, obj, self._on_message_received) if not obj.encrypted: - # XEP-0380 - enc_tag = obj.stanza.getTag('encryption', namespace=nbxmpp.NS_EME) - if enc_tag: - ns = enc_tag.getAttr('namespace') - if ns: - if ns == 'urn:xmpp:otr:0': - obj.msgtxt = _('This message was encrypted with OTR ' - 'and could not be decrypted.') - elif ns == 'jabber:x:encrypted': - obj.msgtxt = _('This message was encrypted with Legacy ' - 'OpenPGP and could not be decrypted. You can install ' - 'the PGP plugin to handle those messages.') - elif ns == 'urn:xmpp:openpgp:0': - obj.msgtxt = _('This message was encrypted with ' - 'OpenPGP for XMPP and could not be decrypted.') - else: - enc_name = enc_tag.getAttr('name') - if not enc_name: - enc_name = ns - obj.msgtxt = _('This message was encrypted with %s ' - 'and could not be decrypted.') % enc_name + eme = parse_eme(obj.stanza) + if eme is not None: + obj.msgtxt = eme self._on_message_received(obj) def _on_message_received(self, obj): - if isinstance(obj, MessageReceivedEvent): - app.nec.push_incoming_event( - DecryptedMessageReceivedEvent( - None, conn=self, msg_obj=obj, stanza_id=obj.unique_id)) - else: - app.nec.push_incoming_event( - MamDecryptedMessageReceivedEvent(None, **vars(obj))) + app.nec.push_incoming_event( + DecryptedMessageReceivedEvent( + None, conn=self, msg_obj=obj, stanza_id=obj.unique_id)) def _nec_decrypted_message_received(self, obj): if obj.conn.name != self.name: @@ -564,7 +536,7 @@ class ConnectionHandlersBase: def _check_for_mam_compliance(self, room_jid, stanza_id): namespace = muc_caps_cache.get_mam_namespace(room_jid) if stanza_id is None and namespace == nbxmpp.NS_MAM_2: - helpers.add_to_mam_blacklist(room_jid) + log.warning('%s announces mam:2 without stanza-id') def _nec_gc_message_received(self, obj): if obj.conn.name != self.name: @@ -743,11 +715,10 @@ class ConnectionHandlersBase: return sess -class ConnectionHandlers(ConnectionArchive313, -ConnectionSocks5Bytestream, ConnectionDisco, ConnectionCaps, -ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream): +class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco, + ConnectionCaps, ConnectionHandlersBase, + ConnectionJingle, ConnectionIBBytestream): def __init__(self): - ConnectionArchive313.__init__(self) ConnectionSocks5Bytestream.__init__(self) ConnectionIBBytestream.__init__(self) @@ -772,9 +743,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream): app.nec.register_incoming_event(StreamConflictReceivedEvent) app.nec.register_incoming_event(MessageReceivedEvent) - app.nec.register_incoming_event(ArchivingErrorReceivedEvent) - app.nec.register_incoming_event( - Archiving313PreferencesChangedReceivedEvent) app.nec.register_incoming_event(NotificationEvent) app.ged.register_event_handler('roster-set-received', @@ -799,7 +767,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream): def cleanup(self): ConnectionHandlersBase.cleanup(self) ConnectionCaps.cleanup(self) - ConnectionArchive313.cleanup(self) app.ged.remove_event_handler('roster-set-received', ged.CORE, self._nec_roster_set_received) app.ged.remove_event_handler('roster-received', ged.CORE, @@ -1343,8 +1310,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream): con.RegisterHandler('iq', self._DiscoverItemsGetCB, 'get', nbxmpp.NS_DISCO_ITEMS) - con.RegisterHandler('iq', self._ArchiveCB, ns=nbxmpp.NS_MAM_1) - con.RegisterHandler('iq', self._ArchiveCB, ns=nbxmpp.NS_MAM_2) con.RegisterHandler('iq', self._JingleCB, 'result') con.RegisterHandler('iq', self._JingleCB, 'error') con.RegisterHandler('iq', self._JingleCB, 'set', nbxmpp.NS_JINGLE) diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py index c29cac46e..d0c8dd7b2 100644 --- a/gajim/common/connection_handlers_events.py +++ b/gajim/common/connection_handlers_events.py @@ -77,7 +77,10 @@ class HelperEvent: del self.conn.groupchat_jids[self.id_] else: self.fjid = helpers.get_full_jid_from_iq(self.stanza) - self.jid, self.resource = app.get_room_and_nick_from_fjid(self.fjid) + if self.fjid is None: + self.jid = None + else: + self.jid, self.resource = app.get_room_and_nick_from_fjid(self.fjid) def get_id(self): self.id_ = self.stanza.getID() @@ -630,240 +633,6 @@ class BeforeChangeShowEvent(nec.NetworkIncomingEvent): name = 'before-change-show' base_network_events = [] -class MamMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): - name = 'mam-message-received' - base_network_events = ['raw-mam-message-received'] - - def __init__(self, name, base_event): - ''' - Pre-Generated attributes on self: - - :conn: Connection instance - :stanza: Complete stanza Node - :forwarded: Forwarded Node - :result: Result Node - ''' - self._set_base_event_vars_as_attributes(base_event) - self.additional_data = {} - self.encrypted = False - self.groupchat = False - self.nick = None - self.self_message = None - self.muc_pm = None - - def generate(self): - account = self.conn.name - archive_jid = self.stanza.getFrom() - own_jid = self.conn.get_own_jid() - if archive_jid and not archive_jid.bareMatch(own_jid): - # MAM Message not from our Archive - return False - - self.msg_ = self.forwarded.getTag('message', protocol=True) - - if self.msg_.getType() == 'groupchat': - return False - - # use stanza-id as unique-id - self.unique_id, origin_id = self.get_unique_id() - self.message_id = self.msg_.getID() - - # Check for duplicates - if app.logger.find_stanza_id(account, - own_jid.getStripped(), - self.unique_id, origin_id): - return - - self.msgtxt = self.msg_.getTagData('body') - - frm = self.msg_.getFrom() - # Some servers dont set the 'to' attribute when - # we send a message to ourself - to = self.msg_.getTo() - if to is None: - to = own_jid - - if frm.bareMatch(own_jid): - self.with_ = to - self.kind = KindConstant.CHAT_MSG_SENT - else: - self.with_ = frm - self.kind = KindConstant.CHAT_MSG_RECV - - delay = self.forwarded.getTagAttr( - 'delay', 'stamp', namespace=nbxmpp.NS_DELAY2) - if delay is None: - log.error('Received MAM message without timestamp') - log.error(self.stanza) - return - - self.timestamp = helpers.parse_datetime( - delay, check_utc=True, epoch=True) - if self.timestamp is None: - log.error('Received MAM message with invalid timestamp: %s', delay) - log.error(self.stanza) - return - - # Save timestamp added by the user - user_delay = self.msg_.getTagAttr( - 'delay', 'stamp', namespace=nbxmpp.NS_DELAY2) - if user_delay is not None: - self.user_timestamp = helpers.parse_datetime( - user_delay, check_utc=True, epoch=True) - if self.user_timestamp is None: - log.warning('Received MAM message with ' - 'invalid user timestamp: %s', user_delay) - log.warning(self.stanza) - - log.debug('Received mam-message: unique id: %s', self.unique_id) - return True - - def get_unique_id(self): - stanza_id = self.get_stanza_id(self.result, query=True) - - if self._is_self_message(self.msg_) or self._is_muc_pm(self.msg_): - origin_id = self.msg_.getOriginID() - return stanza_id, origin_id - - if self.conn.get_own_jid().bareMatch(self.msg_.getFrom()): - # message we sent - origin_id = self.msg_.getOriginID() - return stanza_id, origin_id - - # A message we received - return stanza_id, None - -class MamGcMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): - name = 'mam-gc-message-received' - base_network_events = ['raw-mam-message-received'] - - def __init__(self, name, base_event): - ''' - Pre-Generated attributes on self: - - :conn: Connection instance - :stanza: Complete stanza Node - :forwarded: Forwarded Node - :result: Result Node - :muc_pm: True, if this is a MUC PM - propagated to MamDecryptedMessageReceivedEvent - ''' - self._set_base_event_vars_as_attributes(base_event) - self.additional_data = {} - self.encrypted = False - self.groupchat = True - self.kind = KindConstant.GC_MSG - - def generate(self): - account = self.conn.name - self.msg_ = self.forwarded.getTag('message', protocol=True) - - if self.msg_.getType() != 'groupchat': - return False - - try: - self.room_jid = self.stanza.getFrom().getStripped() - except AttributeError: - log.warning('Received GC MAM message ' - 'without from attribute\n%s', self.stanza) - return False - - self.unique_id = self.get_stanza_id(self.result, query=True) - self.message_id = self.msg_.getID() - - # Check for duplicates - if app.logger.find_stanza_id(account, - self.room_jid, - self.unique_id, - groupchat=True): - return - - self.msgtxt = self.msg_.getTagData('body') - self.with_ = self.msg_.getFrom().getStripped() - self.nick = self.msg_.getFrom().getResource() - - # Get the real jid if we have it - self.real_jid = None - muc_user = self.msg_.getTag('x', namespace=nbxmpp.NS_MUC_USER) - if muc_user is not None: - self.real_jid = muc_user.getTagAttr('item', 'jid') - - delay = self.forwarded.getTagAttr( - 'delay', 'stamp', namespace=nbxmpp.NS_DELAY2) - if delay is None: - log.error('Received MAM message without timestamp') - log.error(self.stanza) - return - - self.timestamp = helpers.parse_datetime( - delay, check_utc=True, epoch=True) - if self.timestamp is None: - log.error('Received MAM message with invalid timestamp: %s', delay) - log.error(self.stanza) - return - - # Save timestamp added by the user - user_delay = self.msg_.getTagAttr( - 'delay', 'stamp', namespace=nbxmpp.NS_DELAY2) - if user_delay is not None: - self.user_timestamp = helpers.parse_datetime( - user_delay, check_utc=True, epoch=True) - if self.user_timestamp is None: - log.warning('Received MAM message with ' - 'invalid user timestamp: %s', user_delay) - log.warning(self.stanza) - - log.debug('Received mam-gc-message: unique id: %s', self.unique_id) - return True - -class MamDecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): - name = 'mam-decrypted-message-received' - base_network_events = [] - - def generate(self): - self.correct_id = None - - if not self.msgtxt: - # For example Chatstates, Receipts, Chatmarkers - log.debug('Received MAM message without text') - return - - replace = self.msg_.getTag('replace', namespace=nbxmpp.NS_CORRECT) - if replace is not None: - self.correct_id = replace.getAttr('id') - - self.get_oob_data(self.msg_) - - if self.groupchat: - return True - - if not self.muc_pm: - # muc_pm = False, means only there was no muc#user namespace - # This could still be a muc pm, we check the database if we - # know this jid. If not we disco it. - self.muc_pm = app.logger.jid_is_room_jid(self.with_.getStripped()) - if self.muc_pm is None: - # Check if this event is triggered after a disco, so we dont - # run into an endless loop - if hasattr(self, 'disco'): - log.error('JID not known even after sucessful disco') - log.error(self.with_.getStripped()) - return - # we don't know this JID, we need to disco it. - server = self.with_.getDomain() - if server not in self.conn.mam_awaiting_disco_result: - self.conn.mam_awaiting_disco_result[server] = [self] - self.conn.discoverInfo(server) - else: - self.conn.mam_awaiting_disco_result[server].append(self) - return - - if self.muc_pm: - self.with_ = str(self.with_) - else: - self.with_ = self.with_.getStripped() - return True - class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): name = 'message-received' base_network_events = ['raw-message-received'] @@ -968,30 +737,6 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): return self.forwarded = True - result = self.stanza.getTag('result', protocol=True) - if result and result.getNamespace() in (nbxmpp.NS_MAM_1, - nbxmpp.NS_MAM_2): - - if result.getAttr('queryid') not in self.conn.mam_query_ids: - log.warning('Invalid MAM Message: unknown query id') - log.debug(self.stanza) - return - - forwarded = result.getTag('forwarded', - namespace=nbxmpp.NS_FORWARD, - protocol=True) - if not forwarded: - log.warning('Invalid MAM Message: no forwarded child') - return - - app.nec.push_incoming_event( - NetworkEvent('raw-mam-message-received', - conn=self.conn, - stanza=self.stanza, - forwarded=forwarded, - result=result)) - return - # Mediated invitation? muc_user = self.stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER) if muc_user: @@ -1085,7 +830,7 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): return # Messages we receive live - if self.conn.archiving_namespace != nbxmpp.NS_MAM_2: + if self.conn.get_module('MAM').archiving_namespace != nbxmpp.NS_MAM_2: # Only mam:2 ensures valid stanza-id return @@ -1498,77 +1243,6 @@ class JingleErrorReceivedEvent(nec.NetworkIncomingEvent): self.sid = self.jingle_session.sid return True -class ArchivingReceivedEvent(nec.NetworkIncomingEvent): - name = 'archiving-received' - base_network_events = [] - - def generate(self): - self.type_ = self.stanza.getType() - if self.type_ not in ('result', 'set', 'error'): - return - return True - -class ArchivingErrorReceivedEvent(nec.NetworkIncomingEvent): - name = 'archiving-error-received' - base_network_events = ['archiving-received'] - - def generate(self): - self.conn = self.base_event.conn - self.stanza = self.base_event.stanza - self.type_ = self.base_event.type_ - - if self.type_ == 'error': - self.error_msg = self.stanza.getErrorMsg() - return True - -class ArchivingCountReceived(nec.NetworkIncomingEvent): - name = 'archiving-count-received' - base_network_events = [] - - def generate(self): - return True - -class ArchivingIntervalFinished(nec.NetworkIncomingEvent): - name = 'archiving-interval-finished' - base_network_events = [] - - def generate(self): - return True - -class ArchivingQueryID(nec.NetworkIncomingEvent): - name = 'archiving-query-id' - base_network_events = [] - - def generate(self): - return True - -class Archiving313PreferencesChangedReceivedEvent(nec.NetworkIncomingEvent): - name = 'archiving-313-preferences-changed-received' - base_network_events = ['archiving-received'] - - def generate(self): - self.conn = self.base_event.conn - self.stanza = self.base_event.stanza - self.type_ = self.base_event.type_ - self.items = [] - self.default = None - self.id = self.stanza.getID() - self.answer = None - prefs = self.stanza.getTag('prefs') - - if self.type_ != 'result' or not prefs: - return - - self.default = prefs.getAttr('default') - - for item in prefs.getTag('always').getTags('jid'): - self.items.append((item.getData(), 'Always')) - - for item in prefs.getTag('never').getTags('jid'): - self.items.append((item.getData(), 'Never')) - - return True - class AccountCreatedEvent(nec.NetworkIncomingEvent): name = 'account-created' base_network_events = [] diff --git a/gajim/common/helpers.py b/gajim/common/helpers.py index 13eb7d49e..aad03bcbc 100644 --- a/gajim/common/helpers.py +++ b/gajim/common/helpers.py @@ -43,7 +43,7 @@ import shlex from gajim.common import caps_cache import socket import time -from datetime import datetime, timedelta, timezone, tzinfo +from datetime import datetime, timedelta from distutils.version import LooseVersion as V from encodings.punycode import punycode_encode @@ -89,77 +89,6 @@ log = logging.getLogger('gajim.c.helpers') special_groups = (_('Transports'), _('Not in Roster'), _('Observers'), _('Groupchats')) -# Patterns for DateTime parsing XEP-0082 -PATTERN_DATETIME = re.compile( - r'([0-9]{4}-[0-9]{2}-[0-9]{2})' - r'T' - r'([0-9]{2}:[0-9]{2}:[0-9]{2})' - r'(\.[0-9]{0,6})?' - r'(?:[0-9]+)?' - r'(?:(Z)|(?:([-+][0-9]{2}):([0-9]{2})))$' - ) - -PATTERN_DELAY = re.compile( - r'([0-9]{4}-[0-9]{2}-[0-9]{2})' - r'T' - r'([0-9]{2}:[0-9]{2}:[0-9]{2})' - r'(\.[0-9]{0,6})?' - r'(?:[0-9]+)?' - r'(?:(Z)|(?:([-+][0]{2}):([0]{2})))$' - ) - -ZERO = timedelta(0) -HOUR = timedelta(hours=1) -SECOND = timedelta(seconds=1) - -STDOFFSET = timedelta(seconds=-time.timezone) -if time.daylight: - DSTOFFSET = timedelta(seconds=-time.altzone) -else: - DSTOFFSET = STDOFFSET - -DSTDIFF = DSTOFFSET - STDOFFSET - - -class LocalTimezone(tzinfo): - ''' - A class capturing the platform's idea of local time. - May result in wrong values on historical times in - timezones where UTC offset and/or the DST rules had - changed in the past. - ''' - def fromutc(self, dt): - assert dt.tzinfo is self - stamp = (dt - datetime(1970, 1, 1, tzinfo=self)) // SECOND - args = time.localtime(stamp)[:6] - dst_diff = DSTDIFF // SECOND - # Detect fold - fold = (args == time.localtime(stamp - dst_diff)) - return datetime(*args, microsecond=dt.microsecond, - tzinfo=self, fold=fold) - - def utcoffset(self, dt): - if self._isdst(dt): - return DSTOFFSET - else: - return STDOFFSET - - def dst(self, dt): - if self._isdst(dt): - return DSTDIFF - else: - return ZERO - - def tzname(self, dt): - return 'local' - - def _isdst(self, dt): - tt = (dt.year, dt.month, dt.day, - dt.hour, dt.minute, dt.second, - dt.weekday(), 0, 0) - stamp = time.mktime(tt) - tt = time.localtime(stamp) - return tt.tm_isdst > 0 class InvalidFormat(Exception): pass @@ -673,56 +602,6 @@ def datetime_tuple(timestamp): tim = tim.timetuple() return tim -def parse_datetime(timestring, check_utc=False, convert='utc', epoch=False): - ''' - Parse a XEP-0082 DateTime Profile String - https://xmpp.org/extensions/xep-0082.html - - :param timestring: a XEP-0082 DateTime profile formated string - - :param check_utc: if True, returns None if timestring is not - a timestring expressing UTC - - :param convert: convert the given timestring to utc or local time - - :param epoch: if True, returns the time in epoch - - Examples: - '2017-11-05T01:41:20Z' - '2017-11-05T01:41:20.123Z' - '2017-11-05T01:41:20.123+05:00' - - return a datetime or epoch - ''' - if convert not in (None, 'utc', 'local'): - raise TypeError('"%s" is not a valid value for convert') - if check_utc: - match = PATTERN_DELAY.match(timestring) - else: - match = PATTERN_DATETIME.match(timestring) - - if match: - timestring = ''.join(match.groups('')) - strformat = '%Y-%m-%d%H:%M:%S%z' - if match.group(3): - # Fractional second addendum to Time - strformat = '%Y-%m-%d%H:%M:%S.%f%z' - if match.group(4): - # UTC string denoted by addition of the character 'Z' - timestring = timestring[:-1] + '+0000' - try: - date_time = datetime.strptime(timestring, strformat) - except ValueError: - pass - else: - if not check_utc and convert == 'utc': - date_time = date_time.astimezone(timezone.utc) - if convert == 'local': - date_time = date_time.astimezone(LocalTimezone()) - if epoch: - return date_time.timestamp() - return date_time - return None from gajim.common import app if app.is_installed('PYCURL'): @@ -1003,6 +882,9 @@ def get_full_jid_from_iq(iq_obj): """ Return the full jid (with resource) from an iq """ + jid = iq_obj.getFrom() + if jid is None: + return None return parse_jid(str(iq_obj.getFrom())) def get_jid_from_iq(iq_obj): @@ -1626,21 +1508,3 @@ def get_emoticon_theme_path(theme): emoticons_user_path = os.path.join(configpaths.get('MY_EMOTS'), theme) if os.path.exists(emoticons_user_path): return emoticons_user_path - -def add_to_mam_blacklist(jid): - config_value = app.config.get('mam_blacklist') - if not config_value: - config_value = [jid] - else: - if jid in config_value: - return - config_value = config_value.split(',') - config_value.append(jid) - log.warning('Found not-compliant MUC. %s added to MAM Blacklist', jid) - app.config.set('mam_blacklist', ','.join(config_value)) - -def get_mam_blacklist(): - config_value = app.config.get('mam_blacklist') - if not config_value: - return [] - return config_value.split(',') diff --git a/gajim/common/logger.py b/gajim/common/logger.py index ab3728b80..cfdabdbac 100644 --- a/gajim/common/logger.py +++ b/gajim/common/logger.py @@ -374,14 +374,10 @@ class Logger: """ Return True if it's a room jid, False if it's not, None if we don't know """ - row = self._con.execute( - 'SELECT type FROM jids WHERE jid=?', (jid,)).fetchone() - if row is None: - return None - else: - if row.type == JIDConstant.ROOM_TYPE: - return True - return False + jid_ = self._jid_ids.get(jid) + if jid_ is None: + return + return jid_.type == JIDConstant.ROOM_TYPE @staticmethod def _get_family_jids(account, jid): diff --git a/gajim/common/message_archiving.py b/gajim/common/message_archiving.py deleted file mode 100644 index 98368775d..000000000 --- a/gajim/common/message_archiving.py +++ /dev/null @@ -1,372 +0,0 @@ -# -*- coding:utf-8 -*- -## src/common/message_archiving.py -## -## Copyright (C) 2009 Anaƫl Verrier -## -## This file is part of Gajim. -## -## Gajim is free software; you can redistribute it and/or modify -## it under the terms of the GNU General Public License as published -## by the Free Software Foundation; version 3 only. -## -## Gajim is distributed in the hope that it will be useful, -## but WITHOUT ANY WARRANTY; without even the implied warranty of -## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -## GNU General Public License for more details. -## -## You should have received a copy of the GNU General Public License -## along with Gajim. If not, see . -## - -import logging -from datetime import datetime, timedelta - -import nbxmpp - -from gajim.common import app -from gajim.common import ged -from gajim.common import helpers -from gajim.common.const import ArchiveState, JIDConstant -from gajim.common.caps_cache import muc_caps_cache -import gajim.common.connection_handlers_events as ev - -log = logging.getLogger('gajim.c.message_archiving') - - -class ConnectionArchive313: - def __init__(self): - self.archiving_313_supported = False - self.mam_awaiting_disco_result = {} - self.iq_answer = [] - self.mam_query_ids = [] - app.nec.register_incoming_event(ev.MamMessageReceivedEvent) - app.nec.register_incoming_event(ev.MamGcMessageReceivedEvent) - app.ged.register_event_handler('agent-info-error-received', ged.CORE, - self._nec_agent_info_error) - app.ged.register_event_handler('agent-info-received', ged.CORE, - self._nec_agent_info) - app.ged.register_event_handler('mam-decrypted-message-received', - ged.CORE, self._nec_mam_decrypted_message_received) - app.ged.register_event_handler( - 'archiving-313-preferences-changed-received', ged.CORE, - self._nec_archiving_313_preferences_changed_received) - - def cleanup(self): - app.ged.remove_event_handler('agent-info-error-received', ged.CORE, - self._nec_agent_info_error) - app.ged.remove_event_handler('agent-info-received', ged.CORE, - self._nec_agent_info) - app.ged.remove_event_handler('mam-decrypted-message-received', - ged.CORE, self._nec_mam_decrypted_message_received) - app.ged.remove_event_handler( - 'archiving-313-preferences-changed-received', ged.CORE, - self._nec_archiving_313_preferences_changed_received) - - def _nec_archiving_313_preferences_changed_received(self, obj): - if obj.id in self.iq_answer: - obj.answer = True - - def _nec_agent_info_error(self, obj): - if obj.jid in self.mam_awaiting_disco_result: - log.warn('Unable to discover %s, ignoring those logs', obj.jid) - del self.mam_awaiting_disco_result[obj.jid] - - def _nec_agent_info(self, obj): - if obj.jid not in self.mam_awaiting_disco_result: - return - - for identity in obj.identities: - if identity['category'] != 'conference': - continue - # it's a groupchat - for msg_obj in self.mam_awaiting_disco_result[obj.jid]: - app.logger.insert_jid(msg_obj.with_.getStripped(), - type_=JIDConstant.ROOM_TYPE) - app.nec.push_incoming_event( - ev.MamDecryptedMessageReceivedEvent( - None, disco=True, **vars(msg_obj))) - del self.mam_awaiting_disco_result[obj.jid] - return - # it's not a groupchat - for msg_obj in self.mam_awaiting_disco_result[obj.jid]: - app.logger.insert_jid(msg_obj.with_.getStripped()) - app.nec.push_incoming_event( - ev.MamDecryptedMessageReceivedEvent( - None, disco=True, **vars(msg_obj))) - del self.mam_awaiting_disco_result[obj.jid] - - @staticmethod - def parse_iq(stanza): - if not nbxmpp.isResultNode(stanza): - log.error('Error on MAM query: %s', stanza.getError()) - raise InvalidMamIQ - - fin = stanza.getTag('fin') - if fin is None: - log.error('Malformed MAM query result received: %s', stanza) - raise InvalidMamIQ - - set_ = fin.getTag('set', namespace=nbxmpp.NS_RSM) - if set_ is None: - log.error( - 'Malformed MAM query result received (no "set" Node): %s', - stanza) - raise InvalidMamIQ - return fin, set_ - - def parse_from_jid(self, stanza): - jid = stanza.getFrom() - if jid is None: - # No from means, iq from our own archive - jid = self.get_own_jid().getStripped() - else: - jid = jid.getStripped() - return jid - - def _result_finished(self, conn, stanza, query_id, start_date, groupchat): - try: - fin, set_ = self.parse_iq(stanza) - except InvalidMamIQ: - return - - last = set_.getTagData('last') - if last is None: - log.info('End of MAM query, no items retrieved') - return - - jid = self.parse_from_jid(stanza) - complete = fin.getAttr('complete') - app.logger.set_archive_timestamp(jid, last_mam_id=last) - if complete != 'true': - self.mam_query_ids.remove(query_id) - query_id = self.get_query_id() - query = self.get_archive_query(query_id, jid=jid, after=last) - self._send_archive_query(query, query_id, groupchat=groupchat) - else: - self.mam_query_ids.remove(query_id) - if start_date is not None: - app.logger.set_archive_timestamp( - jid, - last_mam_id=last, - oldest_mam_timestamp=start_date.timestamp()) - log.info('End of MAM query, last mam id: %s', last) - - def _intervall_result_finished(self, conn, stanza, query_id, - start_date, end_date, event_id): - try: - fin, set_ = self.parse_iq(stanza) - except InvalidMamIQ: - return - - self.mam_query_ids.remove(query_id) - jid = self.parse_from_jid(stanza) - if start_date: - timestamp = start_date.timestamp() - else: - timestamp = ArchiveState.ALL - - last = set_.getTagData('last') - if last is None: - app.nec.push_incoming_event(ev.ArchivingIntervalFinished( - None, event_id=event_id)) - app.logger.set_archive_timestamp( - jid, oldest_mam_timestamp=timestamp) - log.info('End of MAM query, no items retrieved') - return - - complete = fin.getAttr('complete') - if complete != 'true': - self.request_archive_interval(event_id, start_date, end_date, last) - else: - log.info('query finished') - app.logger.set_archive_timestamp( - jid, oldest_mam_timestamp=timestamp) - app.nec.push_incoming_event(ev.ArchivingIntervalFinished( - None, event_id=event_id, stanza=stanza)) - - def _received_count(self, conn, stanza, query_id, event_id): - try: - _, set_ = self.parse_iq(stanza) - except InvalidMamIQ: - return - - self.mam_query_ids.remove(query_id) - - count = set_.getTagData('count') - log.info('message count received: %s', count) - app.nec.push_incoming_event(ev.ArchivingCountReceived( - None, event_id=event_id, count=count)) - - def _nec_mam_decrypted_message_received(self, obj): - if obj.conn.name != self.name: - return - - namespace = self.archiving_namespace - blacklisted = False - if obj.groupchat: - namespace = muc_caps_cache.get_mam_namespace(obj.room_jid) - blacklisted = obj.room_jid in helpers.get_mam_blacklist() - - if namespace != nbxmpp.NS_MAM_2 or blacklisted: - # Fallback duplicate search without stanza-id - duplicate = app.logger.search_for_duplicate( - self.name, obj.with_, obj.timestamp, obj.msgtxt) - if duplicate: - # dont propagate the event further - return True - - app.logger.insert_into_logs(self.name, - obj.with_, - obj.timestamp, - obj.kind, - unread=False, - message=obj.msgtxt, - contact_name=obj.nick, - additional_data=obj.additional_data, - stanza_id=obj.unique_id) - - def get_query_id(self): - query_id = self.connection.getAnID() - self.mam_query_ids.append(query_id) - return query_id - - def request_archive_on_signin(self): - own_jid = self.get_own_jid().getStripped() - archive = app.logger.get_archive_timestamp(own_jid) - - # Migration of last_mam_id from config to DB - if archive is not None: - mam_id = archive.last_mam_id - else: - mam_id = app.config.get_per('accounts', self.name, 'last_mam_id') - - start_date = None - query_id = self.get_query_id() - if mam_id: - log.info('MAM query after: %s', mam_id) - query = self.get_archive_query(query_id, after=mam_id) - else: - # First Start, we request the last week - start_date = datetime.utcnow() - timedelta(days=7) - log.info('First start: query archive start: %s', start_date) - query = self.get_archive_query(query_id, start=start_date) - self._send_archive_query(query, query_id, start_date) - - def request_archive_on_muc_join(self, jid): - archive = app.logger.get_archive_timestamp( - jid, type_=JIDConstant.ROOM_TYPE) - query_id = self.get_query_id() - start_date = None - if archive is not None: - log.info('Query Groupchat MAM Archive %s after %s:', - jid, archive.last_mam_id) - query = self.get_archive_query( - query_id, jid=jid, after=archive.last_mam_id) - else: - # First Start, we dont request history - # Depending on what a MUC saves, there could be thousands - # of Messages even in just one day. - start_date = datetime.utcnow() - timedelta(days=1) - log.info('First join: query archive %s from: %s', jid, start_date) - query = self.get_archive_query(query_id, jid=jid, start=start_date) - self._send_archive_query(query, query_id, start_date, groupchat=True) - - def request_archive_count(self, event_id, start_date, end_date): - query_id = self.get_query_id() - query = self.get_archive_query( - query_id, start=start_date, end=end_date, max_=0) - self.connection.SendAndCallForResponse( - query, self._received_count, {'query_id': query_id, - 'event_id': event_id}) - - def request_archive_interval(self, event_id, start_date, - end_date, after=None): - query_id = self.get_query_id() - query = self.get_archive_query(query_id, start=start_date, - end=end_date, after=after, max_=30) - app.nec.push_incoming_event(ev.ArchivingQueryID( - None, event_id=event_id, query_id=query_id)) - self.connection.SendAndCallForResponse( - query, self._intervall_result_finished, {'query_id': query_id, - 'start_date': start_date, - 'end_date': end_date, - 'event_id': event_id}) - - def _send_archive_query(self, query, query_id, start_date=None, - groupchat=False): - self.connection.SendAndCallForResponse( - query, self._result_finished, {'query_id': query_id, - 'start_date': start_date, - 'groupchat': groupchat}) - - def get_archive_query(self, query_id, jid=None, start=None, end=None, with_=None, - after=None, max_=30): - # Muc archive query? - namespace = muc_caps_cache.get_mam_namespace(jid) - if namespace is None: - # Query to our own archive - namespace = self.archiving_namespace - - iq = nbxmpp.Iq('set', to=jid) - query = iq.addChild('query', namespace=namespace) - form = query.addChild(node=nbxmpp.DataForm(typ='submit')) - field = nbxmpp.DataField(typ='hidden', - name='FORM_TYPE', - value=namespace) - form.addChild(node=field) - if start: - field = nbxmpp.DataField(typ='text-single', - name='start', - value=start.strftime('%Y-%m-%dT%H:%M:%SZ')) - form.addChild(node=field) - if end: - field = nbxmpp.DataField(typ='text-single', - name='end', - value=end.strftime('%Y-%m-%dT%H:%M:%SZ')) - form.addChild(node=field) - if with_: - field = nbxmpp.DataField(typ='jid-single', name='with', value=with_) - form.addChild(node=field) - - set_ = query.setTag('set', namespace=nbxmpp.NS_RSM) - set_.setTagData('max', max_) - if after: - set_.setTagData('after', after) - query.setAttr('queryid', query_id) - return iq - - def request_archive_preferences(self): - if not app.account_is_connected(self.name): - return - iq = nbxmpp.Iq(typ='get') - id_ = self.connection.getAnID() - iq.setID(id_) - iq.addChild(name='prefs', namespace=self.archiving_namespace) - self.connection.send(iq) - - def set_archive_preferences(self, items, default): - if not app.account_is_connected(self.name): - return - iq = nbxmpp.Iq(typ='set') - id_ = self.connection.getAnID() - self.iq_answer.append(id_) - iq.setID(id_) - prefs = iq.addChild(name='prefs', namespace=self.archiving_namespace, attrs={'default': default}) - always = prefs.addChild(name='always') - never = prefs.addChild(name='never') - for item in items: - jid, preference = item - if preference == 'always': - always.addChild(name='jid').setData(jid) - else: - never.addChild(name='jid').setData(jid) - self.connection.send(iq) - - def _ArchiveCB(self, con, iq_obj): - app.nec.push_incoming_event(ev.ArchivingReceivedEvent(None, conn=self, - stanza=iq_obj)) - raise nbxmpp.NodeProcessed - - -class InvalidMamIQ(Exception): - pass diff --git a/gajim/common/modules/date_and_time.py b/gajim/common/modules/date_and_time.py new file mode 100644 index 000000000..43d1b4edb --- /dev/null +++ b/gajim/common/modules/date_and_time.py @@ -0,0 +1,144 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see . + +# XEP-0082: XMPP Date and Time Profiles + +import re +import time +from datetime import datetime, timedelta, timezone, tzinfo + + +PATTERN_DATETIME = re.compile( + r'([0-9]{4}-[0-9]{2}-[0-9]{2})' + r'T' + r'([0-9]{2}:[0-9]{2}:[0-9]{2})' + r'(\.[0-9]{0,6})?' + r'(?:[0-9]+)?' + r'(?:(Z)|(?:([-+][0-9]{2}):([0-9]{2})))$' +) + +PATTERN_DELAY = re.compile( + r'([0-9]{4}-[0-9]{2}-[0-9]{2})' + r'T' + r'([0-9]{2}:[0-9]{2}:[0-9]{2})' + r'(\.[0-9]{0,6})?' + r'(?:[0-9]+)?' + r'(?:(Z)|(?:([-+][0]{2}):([0]{2})))$' +) + + +ZERO = timedelta(0) +HOUR = timedelta(hours=1) +SECOND = timedelta(seconds=1) + +STDOFFSET = timedelta(seconds=-time.timezone) +if time.daylight: + DSTOFFSET = timedelta(seconds=-time.altzone) +else: + DSTOFFSET = STDOFFSET + +DSTDIFF = DSTOFFSET - STDOFFSET + + +class LocalTimezone(tzinfo): + ''' + A class capturing the platform's idea of local time. + May result in wrong values on historical times in + timezones where UTC offset and/or the DST rules had + changed in the past. + ''' + def fromutc(self, dt): + assert dt.tzinfo is self + stamp = (dt - datetime(1970, 1, 1, tzinfo=self)) // SECOND + args = time.localtime(stamp)[:6] + dst_diff = DSTDIFF // SECOND + # Detect fold + fold = (args == time.localtime(stamp - dst_diff)) + return datetime(*args, microsecond=dt.microsecond, + tzinfo=self, fold=fold) + + def utcoffset(self, dt): + if self._isdst(dt): + return DSTOFFSET + else: + return STDOFFSET + + def dst(self, dt): + if self._isdst(dt): + return DSTDIFF + else: + return ZERO + + def tzname(self, dt): + return 'local' + + def _isdst(self, dt): + tt = (dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, + dt.weekday(), 0, 0) + stamp = time.mktime(tt) + tt = time.localtime(stamp) + return tt.tm_isdst > 0 + + +def parse_datetime(timestring, check_utc=False, + convert='utc', epoch=False): + ''' + Parse a XEP-0082 DateTime Profile String + + :param timestring: a XEP-0082 DateTime profile formated string + + :param check_utc: if True, returns None if timestring is not + a timestring expressing UTC + + :param convert: convert the given timestring to utc or local time + + :param epoch: if True, returns the time in epoch + + Examples: + '2017-11-05T01:41:20Z' + '2017-11-05T01:41:20.123Z' + '2017-11-05T01:41:20.123+05:00' + + return a datetime or epoch + ''' + if convert not in (None, 'utc', 'local'): + raise TypeError('"%s" is not a valid value for convert') + if check_utc: + match = PATTERN_DELAY.match(timestring) + else: + match = PATTERN_DATETIME.match(timestring) + + if match: + timestring = ''.join(match.groups('')) + strformat = '%Y-%m-%d%H:%M:%S%z' + if match.group(3): + # Fractional second addendum to Time + strformat = '%Y-%m-%d%H:%M:%S.%f%z' + if match.group(4): + # UTC string denoted by addition of the character 'Z' + timestring = timestring[:-1] + '+0000' + try: + date_time = datetime.strptime(timestring, strformat) + except ValueError: + pass + else: + if not check_utc and convert == 'utc': + date_time = date_time.astimezone(timezone.utc) + if convert == 'local': + date_time = date_time.astimezone(LocalTimezone()) + if epoch: + return date_time.timestamp() + return date_time + return None diff --git a/gajim/common/modules/mam.py b/gajim/common/modules/mam.py new file mode 100644 index 000000000..2edf98233 --- /dev/null +++ b/gajim/common/modules/mam.py @@ -0,0 +1,626 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see . + +# XEP-0313: Message Archive Management + +import logging +from datetime import datetime, timedelta + +import nbxmpp + +from gajim.common import app +from gajim.common.nec import NetworkIncomingEvent +from gajim.common.const import ArchiveState, JIDConstant, KindConstant +from gajim.common.caps_cache import muc_caps_cache +from gajim.common.modules.misc import parse_delay +from gajim.common.modules.misc import parse_oob +from gajim.common.modules.misc import parse_correction +from gajim.common.modules.misc import parse_eme + +log = logging.getLogger('gajim.c.m.archiving') + + +class MAM: + def __init__(self, con): + self._con = con + self._account = con.name + + self.handlers = [ + ('message', self._mam_message_received, nbxmpp.NS_MAM_1), + ('message', self._mam_message_received, nbxmpp.NS_MAM_2) + ] + + self.available = False + self.archiving_namespace = None + self._mam_query_ids = {} + + def _from_valid_archive(self, stanza, message, groupchat): + if groupchat: + expected_archive = message.getFrom() + else: + expected_archive = self._con.get_own_jid() + + archive_jid = stanza.getFrom() + if archive_jid is None: + if groupchat: + return + # Message from our own archive + return self._con.get_own_jid() + else: + if archive_jid.bareMatch(expected_archive): + return archive_jid + + @staticmethod + def _is_self_message(message, groupchat): + if groupchat: + return False + frm = message.getFrom() + to = message.getTo() + return frm.bareMatch(to) + + @staticmethod + def _is_muc_pm(message, groupchat, with_): + if groupchat: + return False + muc_user = message.getTag('x', namespace=nbxmpp.NS_MUC_USER) + if muc_user is not None: + return muc_user.getChildren() == [] + else: + # muc#user namespace was added in MUC 1.28 so we need a fallback + # Check if we know the jid, otherwise disco it + if app.logger.jid_is_room_jid(with_.getStripped()): + return True + return False + + def _get_unique_id(self, result, message, groupchat, self_message, muc_pm): + stanza_id = result.getAttr('id') + if groupchat: + return stanza_id, None + + origin_id = message.getOriginID() + if self_message: + return None, origin_id + + if muc_pm: + return stanza_id, origin_id + + if self._con.get_own_jid().bareMatch(message.getFrom()): + # message we sent + return stanza_id, origin_id + + # A message we received + return stanza_id, None + + def _mam_message_received(self, conn, stanza): + app.nec.push_incoming_event( + NetworkIncomingEvent('raw-mam-message-received', + conn=self._con, + stanza=stanza)) + + result = stanza.getTag('result', protocol=True) + queryid = result.getAttr('queryid') + forwarded = result.getTag('forwarded', + namespace=nbxmpp.NS_FORWARD, + protocol=True) + message = forwarded.getTag('message', protocol=True) + + groupchat = message.getType() == 'groupchat' + + archive_jid = self._from_valid_archive(stanza, message, groupchat) + if archive_jid is None: + log.warning('Message from invalid archive %s', stanza) + raise nbxmpp.NodeProcessed + + log.info('Received message from archive: %s', archive_jid) + if not self._is_valid_request(archive_jid, queryid): + log.warning('Invalid MAM Message: unknown query id') + log.debug(stanza) + raise nbxmpp.NodeProcessed + + # Timestamp parsing + timestamp = parse_delay(forwarded) + if timestamp is None: + raise nbxmpp.NodeProcessed + + user_timestamp = parse_delay(message) + + # Fix for self messaging + if not groupchat: + to = message.getTo() + if to is None: + # Some servers dont set the 'to' attribute when + # we send a message to ourself + message.setTo(self._con.get_own_jid()) + + event_attrs = {} + + if groupchat: + event_attrs.update(self._parse_gc_attrs(message)) + else: + event_attrs.update(self._parse_chat_attrs(message)) + + self_message = self._is_self_message(message, groupchat) + muc_pm = self._is_muc_pm(message, groupchat, event_attrs['with_']) + + stanza_id, origin_id = self._get_unique_id( + result, message, groupchat, self_message, muc_pm) + message_id = message.getID() + + # Check for duplicates + namespace = self.archiving_namespace + if groupchat: + namespace = muc_caps_cache.get_mam_namespace( + archive_jid.getStripped()) + + if namespace == nbxmpp.NS_MAM_2: + # Search only with stanza-id for duplicates on mam:2 + if app.logger.find_stanza_id(self._account, + archive_jid.getStripped(), + stanza_id, + origin_id, + groupchat=groupchat): + log.info('Found duplicate with stanza-id') + raise nbxmpp.NodeProcessed + + msgtxt = message.getTagData('body') + + event_attrs.update( + {'conn': self._con, + 'additional_data': {}, + 'encrypted': False, + 'timestamp': timestamp, + 'user_timestamp': user_timestamp, + 'self_message': self_message, + 'groupchat': groupchat, + 'muc_pm': muc_pm, + 'stanza_id': stanza_id, + 'origin_id': origin_id, + 'message_id': message_id, + 'correct_id': None, + 'archive_jid': archive_jid, + 'msgtxt': msgtxt, + 'message': message, + 'namespace': namespace, + }) + + if groupchat: + event = MamGcMessageReceivedEvent(None, **event_attrs) + else: + event = MamMessageReceivedEvent(None, **event_attrs) + + app.plugin_manager.extension_point( + 'decrypt', self._con, event, self._decryption_finished) + + if not event.encrypted: + eme = parse_eme(event.message) + if eme is not None: + event.msgtxt = eme + self._decryption_finished(event) + + raise nbxmpp.NodeProcessed + + def _parse_gc_attrs(self, message): + with_ = message.getFrom() + nick = message.getFrom().getResource() + + # Get the real jid if we have it + real_jid = None + muc_user = message.getTag('x', namespace=nbxmpp.NS_MUC_USER) + if muc_user is not None: + real_jid = muc_user.getTagAttr('item', 'jid') + if real_jid is not None: + real_jid = nbxmpp.JID(real_jid) + + return {'with_': with_, + 'nick': nick, + 'real_jid': real_jid, + 'kind': KindConstant.GC_MSG} + + def _parse_chat_attrs(self, message): + frm = message.getFrom() + to = message.getTo() + if frm.bareMatch(self._con.get_own_jid()): + with_ = to + kind = KindConstant.CHAT_MSG_SENT + else: + with_ = frm + kind = KindConstant.CHAT_MSG_RECV + + return {'with_': with_, + 'nick': None, + 'kind': kind} + + def _decryption_finished(self, event): + if not event.msgtxt: + # For example Chatstates, Receipts, Chatmarkers + log.debug(event.message.getProperties()) + return + log.debug(event.msgtxt) + + event.correct_id = parse_correction(event.message) + parse_oob(event.message, event.additional_data) + + with_ = event.with_.getStripped() + if event.muc_pm: + # we store the message with the full JID + with_ = str(event.with_) + + stanza_id = event.stanza_id + if event.self_message: + # Self messages can only be deduped with origin-id + if event.origin_id is None: + log.warning('Self message without origin-id found') + return + stanza_id = event.origin_id + + if event.namespace == nbxmpp.NS_MAM_1: + if app.logger.search_for_duplicate( + self._account, with_, event.timestamp, event.msgtxt): + log.info('Found duplicate with fallback for mam:1') + return + + app.logger.insert_into_logs(self._account, + with_, + event.timestamp, + event.kind, + unread=False, + message=event.msgtxt, + contact_name=event.nick, + additional_data=event.additional_data, + stanza_id=stanza_id) + + app.nec.push_incoming_event( + MamDecryptedMessageReceived(None, **vars(event))) + + def _is_valid_request(self, jid, query_id): + if query_id is None: + return False + + valid_id = self._mam_query_ids.get(jid.getStripped(), None) + return valid_id == query_id + + def _get_query_id(self, jid): + query_id = self._con.connection.getAnID() + self._mam_query_ids[jid] = query_id + return query_id + + @staticmethod + def _parse_iq(stanza): + if not nbxmpp.isResultNode(stanza): + log.error('Error on MAM query: %s', stanza.getError()) + raise InvalidMamIQ + + fin = stanza.getTag('fin') + if fin is None: + log.error('Malformed MAM query result received: %s', stanza) + raise InvalidMamIQ + + set_ = fin.getTag('set', namespace=nbxmpp.NS_RSM) + if set_ is None: + log.error( + 'Malformed MAM query result received (no "set" Node): %s', + stanza) + raise InvalidMamIQ + return fin, set_ + + def _get_from_jid(self, stanza): + jid = stanza.getFrom() + if jid is None: + # No from means, iq from our own archive + jid = self._con.get_own_jid().getStripped() + else: + jid = jid.getStripped() + return jid + + def request_archive_count(self, start_date, end_date): + jid = self._con.get_own_jid().getStripped() + log.info('Request archive count from: %s', jid) + query_id = self._get_query_id(jid) + query = self._get_archive_query( + query_id, start=start_date, end=end_date, max_=0) + self._con.connection.SendAndCallForResponse( + query, self._received_count, {'query_id': query_id}) + return query_id + + def _received_count(self, conn, stanza, query_id): + try: + _, set_ = self._parse_iq(stanza) + except InvalidMamIQ: + return + + jid = self._get_from_jid(stanza) + self._mam_query_ids.pop(jid) + + count = set_.getTagData('count') + log.info('Received archive count: %s', count) + app.nec.push_incoming_event(ArchivingCountReceived( + None, query_id=query_id, count=count)) + + def request_archive_on_signin(self): + own_jid = self._con.get_own_jid().getStripped() + + if own_jid in self._mam_query_ids: + log.warning('MAM request for %s already running', own_jid) + return + + archive = app.logger.get_archive_timestamp(own_jid) + + # Migration of last_mam_id from config to DB + if archive is not None: + mam_id = archive.last_mam_id + else: + mam_id = app.config.get_per( + 'accounts', self._account, 'last_mam_id') + if mam_id: + app.config.del_per('accounts', self._account, 'last_mam_id') + + start_date = None + query_id = self._get_query_id(own_jid) + if mam_id: + log.info('MAM query after: %s', mam_id) + query = self._get_archive_query(query_id, after=mam_id) + else: + # First Start, we request the last week + start_date = datetime.utcnow() - timedelta(days=7) + log.info('First start: query archive start: %s', start_date) + query = self._get_archive_query(query_id, start=start_date) + self._send_archive_query(query, query_id, start_date) + + def request_archive_on_muc_join(self, jid): + archive = app.logger.get_archive_timestamp( + jid, type_=JIDConstant.ROOM_TYPE) + query_id = self._get_query_id(jid) + start_date = None + if archive is not None: + log.info('Request from archive %s after %s:', + jid, archive.last_mam_id) + query = self._get_archive_query( + query_id, jid=jid, after=archive.last_mam_id) + else: + # First Start, we dont request history + # Depending on what a MUC saves, there could be thousands + # of Messages even in just one day. + start_date = datetime.utcnow() - timedelta(days=1) + log.info('First join: query archive %s from: %s', jid, start_date) + query = self._get_archive_query(query_id, jid=jid, start=start_date) + self._send_archive_query(query, query_id, start_date, groupchat=True) + + def _send_archive_query(self, query, query_id, start_date=None, + groupchat=False): + self._con.connection.SendAndCallForResponse( + query, self._result_finished, {'query_id': query_id, + 'start_date': start_date, + 'groupchat': groupchat}) + + def _result_finished(self, conn, stanza, query_id, start_date, groupchat): + try: + fin, set_ = self._parse_iq(stanza) + except InvalidMamIQ: + return + + jid = self._get_from_jid(stanza) + + last = set_.getTagData('last') + if last is None: + log.info('End of MAM query, no items retrieved') + self._mam_query_ids.pop(jid) + return + + complete = fin.getAttr('complete') + app.logger.set_archive_timestamp(jid, last_mam_id=last) + if complete != 'true': + self._mam_query_ids.pop(jid) + query_id = self._get_query_id(jid) + query = self._get_archive_query(query_id, jid=jid, after=last) + self._send_archive_query(query, query_id, groupchat=groupchat) + else: + self._mam_query_ids.pop(jid) + if start_date is not None: + app.logger.set_archive_timestamp( + jid, + last_mam_id=last, + oldest_mam_timestamp=start_date.timestamp()) + log.info('End of MAM query, last mam id: %s', last) + + def request_archive_interval(self, start_date, end_date, after=None, + query_id=None): + jid = self._con.get_own_jid().getStripped() + if after is None: + log.info('Request intervall from %s to %s from %s', + start_date, end_date, jid) + else: + log.info('Query page after %s from %s', + after, jid) + if query_id is None: + query_id = self._get_query_id(jid) + self._mam_query_ids[jid] = query_id + query = self._get_archive_query(query_id, start=start_date, + end=end_date, after=after, max_=30) + + self._con.connection.SendAndCallForResponse( + query, self._intervall_result, {'query_id': query_id, + 'start_date': start_date, + 'end_date': end_date}) + return query_id + + def _intervall_result(self, conn, stanza, query_id, + start_date, end_date): + try: + fin, set_ = self._parse_iq(stanza) + except InvalidMamIQ: + return + + jid = self._get_from_jid(stanza) + self._mam_query_ids.pop(jid) + if start_date: + timestamp = start_date.timestamp() + else: + timestamp = ArchiveState.ALL + + last = set_.getTagData('last') + if last is None: + app.nec.push_incoming_event(ArchivingIntervalFinished( + None, query_id=query_id)) + app.logger.set_archive_timestamp( + jid, oldest_mam_timestamp=timestamp) + log.info('End of MAM request, no items retrieved') + return + + complete = fin.getAttr('complete') + if complete != 'true': + self.request_archive_interval(start_date, end_date, last, query_id) + else: + log.info('Request finished') + app.logger.set_archive_timestamp( + jid, oldest_mam_timestamp=timestamp) + app.nec.push_incoming_event(ArchivingIntervalFinished( + None, query_id=query_id)) + + def _get_archive_query(self, query_id, jid=None, start=None, end=None, + with_=None, after=None, max_=30): + # Muc archive query? + namespace = muc_caps_cache.get_mam_namespace(jid) + if namespace is None: + # Query to our own archive + namespace = self.archiving_namespace + + iq = nbxmpp.Iq('set', to=jid) + query = iq.addChild('query', namespace=namespace) + form = query.addChild(node=nbxmpp.DataForm(typ='submit')) + field = nbxmpp.DataField(typ='hidden', + name='FORM_TYPE', + value=namespace) + form.addChild(node=field) + if start: + field = nbxmpp.DataField(typ='text-single', + name='start', + value=start.strftime('%Y-%m-%dT%H:%M:%SZ')) + form.addChild(node=field) + if end: + field = nbxmpp.DataField(typ='text-single', + name='end', + value=end.strftime('%Y-%m-%dT%H:%M:%SZ')) + form.addChild(node=field) + if with_: + field = nbxmpp.DataField(typ='jid-single', name='with', value=with_) + form.addChild(node=field) + + set_ = query.setTag('set', namespace=nbxmpp.NS_RSM) + set_.setTagData('max', max_) + if after: + set_.setTagData('after', after) + query.setAttr('queryid', query_id) + return iq + + def request_mam_preferences(self): + log.info('Request MAM preferences') + iq = nbxmpp.Iq('get', self.archiving_namespace) + iq.setQuery('prefs') + self._con.connection.SendAndCallForResponse( + iq, self._preferences_received) + + def _preferences_received(self, stanza): + if not nbxmpp.isResultNode(stanza): + log.info('Error: %s', stanza.getError()) + app.nec.push_incoming_event(MAMPreferenceError( + None, conn=self._con, error=stanza.getError())) + return + + log.info('Received MAM preferences') + prefs = stanza.getTag('prefs', namespace=self.archiving_namespace) + if prefs is None: + log.error('Malformed stanza (no prefs node): %s', stanza) + return + + rules = [] + default = prefs.getAttr('default') + for item in prefs.getTag('always').getTags('jid'): + rules.append((item.getData(), 'Always')) + + for item in prefs.getTag('never').getTags('jid'): + rules.append((item.getData(), 'Never')) + + app.nec.push_incoming_event(MAMPreferenceReceived( + None, conn=self._con, rules=rules, default=default)) + + def set_mam_preferences(self, rules, default): + iq = nbxmpp.Iq(typ='set') + prefs = iq.addChild(name='prefs', + namespace=self.archiving_namespace, + attrs={'default': default}) + always = prefs.addChild(name='always') + never = prefs.addChild(name='never') + for item in rules: + jid, archive = item + if archive: + always.addChild(name='jid').setData(jid) + else: + never.addChild(name='jid').setData(jid) + + self._con.connection.SendAndCallForResponse( + iq, self._preferences_saved) + + def _preferences_saved(self, stanza): + if not nbxmpp.isResultNode(stanza): + log.info('Error: %s', stanza.getError()) + app.nec.push_incoming_event(MAMPreferenceError( + None, conn=self._con, error=stanza.getError())) + else: + log.info('Preferences saved') + app.nec.push_incoming_event( + MAMPreferenceSaved(None, conn=self._con)) + + +class MamMessageReceivedEvent(NetworkIncomingEvent): + name = 'mam-message-received' + + +class MamGcMessageReceivedEvent(NetworkIncomingEvent): + name = 'mam-message-received' + + +class MamDecryptedMessageReceived(NetworkIncomingEvent): + name = 'mam-decrypted-message-received' + + +class MAMPreferenceError(NetworkIncomingEvent): + name = 'mam-prefs-error' + + +class MAMPreferenceReceived(NetworkIncomingEvent): + name = 'mam-prefs-received' + + +class MAMPreferenceSaved(NetworkIncomingEvent): + name = 'mam-prefs-saved' + + +class ArchivingCountReceived(NetworkIncomingEvent): + name = 'archiving-count-received' + + +class ArchivingIntervalFinished(NetworkIncomingEvent): + name = 'archiving-interval-finished' + + +class ArchivingErrorReceived(NetworkIncomingEvent): + name = 'archiving-error-received' + + +class InvalidMamIQ(Exception): + pass + + +def get_instance(*args, **kwargs): + return MAM(*args, **kwargs), 'MAM' diff --git a/gajim/common/modules/misc.py b/gajim/common/modules/misc.py new file mode 100644 index 000000000..a2afa2bd1 --- /dev/null +++ b/gajim/common/modules/misc.py @@ -0,0 +1,113 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see . + +# All XEPs that dont need their own module + +import logging + +import nbxmpp + +from gajim.common.modules.date_and_time import parse_datetime + +log = logging.getLogger('gajim.c.m.misc') + + +# XEP-0380: Explicit Message Encryption + +_eme_namespaces = { + 'urn:xmpp:otr:0': + _('This message was encrypted with OTR ' + 'and could not be decrypted.'), + 'jabber:x:encrypted': + _('This message was encrypted with Legacy ' + 'OpenPGP and could not be decrypted. You can install ' + 'the PGP plugin to handle those messages.'), + 'urn:xmpp:openpgp:0': + _('This message was encrypted with ' + 'OpenPGP for XMPP and could not be decrypted.'), + 'fallback': + _('This message was encrypted with %s ' + 'and could not be decrypted.') +} + + +def parse_eme(stanza): + enc_tag = stanza.getTag('encryption', namespace=nbxmpp.NS_EME) + if enc_tag is None: + return + + ns = enc_tag.getAttr('namespace') + if ns is None: + log.warning('No namespace on EME message') + return + + if ns in _eme_namespaces: + log.info('Found not decrypted message: %s', ns) + return _eme_namespaces.get(ns) + + enc_name = enc_tag.getAttr('name') + log.info('Found not decrypted message: %s', enc_name or ns) + return _eme_namespaces.get('fallback') % enc_name or ns + + +# XEP-0203: Delayed Delivery + +def parse_delay(stanza, epoch=True, convert='utc'): + timestamp = None + delay = stanza.getTagAttr( + 'delay', 'stamp', namespace=nbxmpp.NS_DELAY2) + if delay is not None: + timestamp = parse_datetime(delay, check_utc=True, + epoch=epoch, convert=convert) + if timestamp is None: + log.warning('Invalid timestamp received: %s', delay) + log.warning(stanza) + + return timestamp + + +# XEP-0066: Out of Band Data + +def parse_oob(stanza, dict_=None, key='Gajim'): + oob_node = stanza.getTag('x', namespace=nbxmpp.NS_X_OOB) + if oob_node is None: + return + result = {} + url = oob_node.getTagData('url') + if url is not None: + result['oob_url'] = url + desc = oob_node.getTagData('desc') + if desc is not None: + result['oob_desc'] = desc + + if dict_ is None: + return result + + if key in dict_: + dict_[key] += result + else: + dict_[key] = result + + return dict_ + + +# XEP-0308: Last Message Correction + +def parse_correction(stanza): + replace = stanza.getTag('replace', namespace=nbxmpp.NS_CORRECT) + if replace is not None: + id_ = replace.getAttr('id') + if id_ is not None: + return id_ + log.warning('No id attr found: %s' % stanza) diff --git a/gajim/data/gui/archiving_313_preferences_item.ui b/gajim/data/gui/archiving_313_preferences_item.ui deleted file mode 100644 index bda22250a..000000000 --- a/gajim/data/gui/archiving_313_preferences_item.ui +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - - - - - Always - - - Never - - - - - False - 12 - False - True - dialog - - - - False - vertical - 20 - - - False - end - - - gtk-close - True - True - True - True - True - - - - True - True - 0 - - - - - gtk-ok - True - True - True - True - True - - - - True - True - 1 - - - - - False - False - 0 - - - - - True - False - 5 - 5 - - - True - False - Jabber ID: - 0 - - - 0 - 0 - - - - - True - False - Preference: - 0 - - - 0 - 1 - - - - - 194 - True - True - - - 1 - 0 - - - - - True - False - start - dialog_pref_liststore - - - - 0 - - - - - 1 - 1 - - - - - False - True - 1 - - - - - - diff --git a/gajim/data/gui/archiving_313_preferences_window.ui b/gajim/data/gui/archiving_313_preferences_window.ui deleted file mode 100644 index 994a3a3f9..000000000 --- a/gajim/data/gui/archiving_313_preferences_window.ui +++ /dev/null @@ -1,214 +0,0 @@ - - - - - - True - False - gtk-add - - - - - - - - - - - - - - - - - Always - - - Roster - - - Never - - - - - True - False - gtk-remove - - - False - 12 - center - 450 - - - - - True - False - 5 - 10 - - - 150 - True - True - True - True - in - - - True - True - archive_items_liststore - - - - - - Jabber ID - True - True - 0 - - - - 0 - - - - - - - Preference - True - True - 1 - - - - 1 - - - - - - - - - 0 - 1 - 2 - - - - - True - False - start - 5 - start - - - True - True - True - add_image - - - - False - True - 0 - True - - - - - True - True - True - remove_image - - - - True - True - 1 - True - - - - - 0 - 2 - - - - - True - False - start - 5 - - - True - False - Default: - 0 - - - 0 - 0 - - - - - True - False - start - False - default_pref_liststore - - - - 0 - - - - - 1 - 0 - - - - - 0 - 0 - - - - - gtk-save - True - True - True - end - True - True - - - - 1 - 2 - - - - - - - - - diff --git a/gajim/data/gui/mam_preferences.ui b/gajim/data/gui/mam_preferences.ui new file mode 100644 index 000000000..84208bd69 --- /dev/null +++ b/gajim/data/gui/mam_preferences.ui @@ -0,0 +1,236 @@ + + + + + + + + + + + + + + Always + always + + + Roster + roster + + + Never + never + + + + + + + + + + + + + 400 + 300 + True + False + 18 + 18 + 18 + 18 + 5 + 10 + + + True + False + start + 5 + start + + + True + True + True + + + + True + False + list-add-symbolic + + + + + False + True + 0 + True + + + + + True + True + True + + + + True + False + list-remove-symbolic + + + + + True + True + 1 + True + + + + + 0 + 2 + + + + + True + False + start + 5 + + + True + False + Default: + 0 + + + 0 + 0 + + + + + True + False + start + False + default_store + 0 + 1 + + + + 0 + + + + + 1 + 0 + + + + + 0 + 0 + + + + + Save + True + True + True + end + True + + + + 1 + 2 + + + + + True + False + + + 150 + True + True + True + True + in + + + True + True + preferences_store + 0 + + + + + + Jabber ID + True + True + True + 0 + + + True + user@example.org + + + + 0 + + + + + + + Archive + True + 0.5 + True + 1 + + + + + + 1 + + + + + + + + + -1 + + + + + 0 + 1 + 2 + + + + + + + diff --git a/gajim/dialogs.py b/gajim/dialogs.py index 0ce26d1c1..4403bdda9 100644 --- a/gajim/dialogs.py +++ b/gajim/dialogs.py @@ -3871,170 +3871,6 @@ class RosterItemExchangeWindow: self.window.destroy() -class Archiving313PreferencesWindow: - - default_dict = {'always': 0, 'roster': 1, 'never': 2} - default_dict_cb = {0: 'always', 1: 'roster', 2: 'never'} - - def __init__(self, account): - self.account = account - self.idle_id = None - - # Connect to glade - self.xml = gtkgui_helpers.get_gtk_builder( - 'archiving_313_preferences_window.ui') - self.window = self.xml.get_object('archiving_313_pref') - - # Add Widgets - for widget in ('archive_items_liststore', 'default_cb'): - setattr(self, widget, self.xml.get_object(widget)) - - self.window.set_title(_('Archiving Preferences for %s') % self.account) - - app.ged.register_event_handler( - 'archiving-313-preferences-changed-received', ged.GUI1, - self._nec_archiving_313_changed_received) - app.ged.register_event_handler( - 'archiving-error-received', ged.GUI1, self._nec_archiving_error) - - self.default_cb.set_active(0) - self.set_widget_state(False) - self.window.show_all() - self.xml.connect_signals(self) - - self.idle_id = GLib.timeout_add_seconds(3, self._nec_archiving_error) - app.connections[self.account].request_archive_preferences() - - def on_key_press_event(self, widget, event): - if event.keyval == Gdk.KEY_Escape: - self.window.destroy() - - def set_widget_state(self, state): - for widget in ('default_cb', 'save_button', 'add_button', - 'remove_button'): - self.xml.get_object(widget).set_sensitive(state) - - def _nec_archiving_313_changed_received(self, obj): - if obj.conn.name != self.account: - return - try: - GLib.source_remove(self.idle_id) - except Exception as e: - log.debug(e) - self.set_widget_state(True) - if obj.answer: - def on_ok(dialog): - self.window.destroy() - dialog = HigDialog( - self.window, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, - _('Success!'), _('Your Archiving Preferences have been saved!'), - on_response_ok=on_ok, on_response_cancel=on_ok) - dialog.popup() - self.default_cb.set_active(self.default_dict[obj.default]) - self.archive_items_liststore.clear() - for items in obj.items: - self.archive_items_liststore.append(items) - - def _nec_archiving_error(self, obj=None): - if obj and obj.conn.name != self.account: - return - try: - GLib.source_remove(self.idle_id) - except Exception as e: - log.debug(e) - if not obj: - msg = _('No response from the Server') - else: - msg = _('Error received: {}').format(self.error_msg) - - dialog = HigDialog( - self.window, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, - _('Error!'), msg) - dialog.popup() - self.set_widget_state(True) - return - - def on_add_item_button_clicked(self, widget): - key_name = 'item_archiving_preferences' - if key_name in app.interface.instances[self.account]: - app.interface.instances[self.account][key_name].window.present() - else: - app.interface.instances[self.account][key_name] = \ - ItemArchiving313PreferencesWindow( - self.account, self, self.window) - - def on_remove_item_button_clicked(self, widget): - archive_view = self.xml.get_object('archive_view') - mod, path = archive_view.get_selection().get_selected_rows() - if path: - iter_ = mod.get_iter(path) - self.archive_items_liststore.remove(iter_) - - def on_save_button_clicked(self, widget): - self.set_widget_state(False) - items = [] - default = self.default_dict_cb[self.default_cb.get_active()] - for item in self.archive_items_liststore: - items.append((item[0].lower(), item[1].lower())) - self.idle_id = GLib.timeout_add_seconds(3, self._nec_archiving_error) - app.connections[self.account]. \ - set_archive_preferences(items, default) - - def on_close_button_clicked(self, widget): - self.window.destroy() - - def on_archiving_preferences_window_destroy(self, widget): - app.ged.remove_event_handler( - 'archiving-313-preferences-changed-received', ged.GUI1, - self._nec_archiving_313_changed_received) - app.ged.remove_event_handler( - 'archiving-error-received', ged.GUI1, self._nec_archiving_error) - if 'archiving_preferences' in app.interface.instances[self.account]: - del app.interface.instances[self.account]['archiving_preferences'] - - -class ItemArchiving313PreferencesWindow: - - def __init__(self, account, archive, transient): - - self.account = account - self.archive = archive - - self.xml = gtkgui_helpers.get_gtk_builder( - 'archiving_313_preferences_item.ui') - self.window = self.xml.get_object('item_dialog') - self.window.set_transient_for(transient) - # Add Widgets - for widget in ('jid_entry', 'pref_cb'): - setattr(self, widget, self.xml.get_object(widget)) - - self.window.set_title(_('Add JID')) - self.pref_cb.set_active(0) - self.window.show_all() - self.xml.connect_signals(self) - - def on_ok_button_clicked(self, widget): - if self.pref_cb.get_active() == 0: - pref = 'Always' - else: - pref = 'Never' - text = self.jid_entry.get_text() - if not text: - self.window.destroy() - return - else: - self.archive.archive_items_liststore.append((text, pref)) - self.window.destroy() - - def on_cancel_button_clicked(self, widget): - self.window.destroy() - - def on_item_archiving_preferences_window_destroy(self, widget): - key_name = 'item_archiving_preferences' - if key_name in app.interface.instances[self.account]: - del app.interface.instances[self.account][key_name] - - class PrivacyListWindow: """ Window that is used for creating NEW or EDITING already there privacy lists diff --git a/gajim/groupchat_control.py b/gajim/groupchat_control.py index 6abc4a706..ad4fdef36 100644 --- a/gajim/groupchat_control.py +++ b/gajim/groupchat_control.py @@ -1168,9 +1168,11 @@ class GroupchatControl(ChatControlBase): self._update_banner_state_image() def _nec_mam_decrypted_message_received(self, obj): + if obj.conn.name != self.account: + return if not obj.groupchat: return - if obj.room_jid != self.room_jid: + if obj.archive_jid != self.room_jid: return self.print_conversation( obj.msgtxt, contact=obj.nick, @@ -1588,7 +1590,8 @@ class GroupchatControl(ChatControlBase): if muc_caps_cache.has_mam(self.room_jid): # Request MAM - app.connections[self.account].request_archive_on_muc_join( + con = app.connections[self.account] + con.get_module('MAM').request_archive_on_muc_join( self.room_jid) app.gc_connected[self.account][self.room_jid] = True @@ -2256,6 +2259,8 @@ class GroupchatControl(ChatControlBase): self._nec_signed_in) app.ged.remove_event_handler('decrypted-message-received', ged.GUI2, self._nec_decrypted_message_received) + app.ged.remove_event_handler('mam-decrypted-message-received', + ged.GUI1, self._nec_mam_decrypted_message_received) app.ged.remove_event_handler('gc-stanza-message-outgoing', ged.OUT_POSTCORE, self._message_sent) diff --git a/gajim/gtk/__init__.py b/gajim/gtk/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/gajim/gtk/mam_preferences.py b/gajim/gtk/mam_preferences.py new file mode 100644 index 000000000..b19f35c3a --- /dev/null +++ b/gajim/gtk/mam_preferences.py @@ -0,0 +1,158 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see . + +import logging + +from gi.repository import Gtk +from gi.repository import Gdk + +from gajim.common import app +from gajim.common import ged +from gajim.gtk.util import get_builder + +from gajim.dialogs import HigDialog + +log = logging.getLogger('gajim.gtk.mam_preferences') + + +class MamPreferences(Gtk.ApplicationWindow): + def __init__(self, account): + Gtk.ApplicationWindow.__init__(self) + self.set_application(app.app) + self.set_position(Gtk.WindowPosition.CENTER) + self.set_show_menubar(False) + self.set_title(_('Archiving Preferences for %s') % account) + + self.connect('destroy', self._on_destroy) + self.connect('key-press-event', self._on_key_press) + + self.account = account + self._con = app.connections[account] + + self._builder = get_builder('mam_preferences.ui') + self.add(self._builder.get_object('preferences_grid')) + + self._default = self._builder.get_object('default_cb') + self._pref_store = self._builder.get_object('preferences_store') + self._overlay = self._builder.get_object('overlay') + self._spinner = Gtk.Spinner() + self._overlay.add_overlay(self._spinner) + + app.ged.register_event_handler('mam-prefs-received', ged.GUI1, + self._mam_prefs_received) + app.ged.register_event_handler('mam-prefs-saved', ged.GUI1, + self._mam_prefs_saved) + app.ged.register_event_handler('mam-prefs-error', ged.GUI1, + self._mam_prefs_error) + + self._set_grid_state(False) + self._builder.connect_signals(self) + self.show_all() + + self._activate_spinner() + + self._con.get_module('MAM').request_mam_preferences() + + def _mam_prefs_received(self, obj): + if obj.conn.name != self.account: + return + self._disable_spinner() + self._set_grid_state(True) + + self._default.set_active_id(obj.default) + self._pref_store.clear() + for item in obj.rules: + self._pref_store.append(item) + + def _mam_prefs_saved(self, obj): + if obj.conn.name != self.account: + return + + self._disable_spinner() + + def on_ok(dialog): + self.destroy() + dialog = HigDialog( + self, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, + _('Success!'), _('Your Archiving Preferences have been saved!'), + on_response_ok=on_ok, on_response_cancel=on_ok) + dialog.popup() + + def _mam_prefs_error(self, obj=None): + if obj and obj.conn.name != self.account: + return + + self._disable_spinner() + + if not obj: + msg = _('No response from the Server') + else: + msg = _('Error received: {}').format(obj.error_msg) + + dialog = HigDialog( + self, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, + _('Error!'), msg) + dialog.popup() + self._set_grid_state(True) + + def _set_grid_state(self, state): + self._builder.get_object('preferences_grid').set_sensitive(state) + + def _jid_edited(self, renderer, path, new_text): + iter_ = self._pref_store.get_iter(path) + self._pref_store.set_value(iter_, 0, new_text) + + def _pref_toggled(self, renderer, path): + iter_ = self._pref_store.get_iter(path) + current_value = self._pref_store[iter_][1] + self._pref_store.set_value(iter_, 1, not current_value) + + def _on_add(self, button): + self._pref_store.append(['', False]) + + def _on_remove(self, button): + pref_view = self._builder.get_object('pref_view') + mod, paths = pref_view.get_selection().get_selected_rows() + for path in paths: + iter_ = mod.get_iter(path) + self._pref_store.remove(iter_) + + def _on_save(self, button): + self._activate_spinner() + self._set_grid_state(False) + items = [] + default = self._default.get_active_id() + for item in self._pref_store: + items.append((item[0].lower(), item[1])) + self._con.get_module('MAM').set_mam_preferences(items, default) + + def _activate_spinner(self): + self._spinner.show() + self._spinner.start() + + def _disable_spinner(self): + self._spinner.hide() + self._spinner.stop() + + def _on_key_press(self, widget, event): + if event.keyval == Gdk.KEY_Escape: + self.destroy() + + def _on_destroy(self, widget): + app.ged.remove_event_handler('mam-prefs-received', ged.GUI1, + self._mam_prefs_received) + app.ged.remove_event_handler('mam-prefs-saved', ged.GUI1, + self._mam_prefs_saved) + app.ged.remove_event_handler('mam-prefs-error', ged.GUI1, + self._mam_prefs_error) diff --git a/gajim/gtk/util.py b/gajim/gtk/util.py new file mode 100644 index 000000000..8c5636414 --- /dev/null +++ b/gajim/gtk/util.py @@ -0,0 +1,54 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see . + +import os +import sys + +from gi.repository import Gtk +import xml.etree.ElementTree as ET + +from gajim.common import i18n +from gajim.common import configpaths + + +def get_builder(file_name, widget=None): + file_path = os.path.join(configpaths.get('GUI'), file_name) + builder = _translate(file_path, widget) + builder.set_translation_domain(i18n.DOMAIN) + return builder + + +def _translate(gui_file, widget): + """ + This is a workaround for non working translation on Windows + """ + if sys.platform == "win32": + tree = ET.parse(gui_file) + for node in tree.iter(): + if 'translatable' in node.attrib: + node.text = _(node.text) + xml_text = ET.tostring(tree.getroot(), + encoding='unicode', + method='xml') + if widget is not None: + builder = Gtk.Builder() + builder.add_objects_from_string(xml_text, [widget]) + return builder + return Gtk.Builder.new_from_string(xml_text, -1) + else: + if widget is not None: + builder = Gtk.Builder() + builder.add_objects_from_file(gui_file, [widget]) + return builder + return Gtk.Builder.new_from_file(gui_file) diff --git a/gajim/gui_interface.py b/gajim/gui_interface.py index 17dd8332c..6045b8dd3 100644 --- a/gajim/gui_interface.py +++ b/gajim/gui_interface.py @@ -1113,9 +1113,9 @@ class Interface: # Else disable autoaway app.sleeper_state[account] = 'off' - if obj.conn.archiving_313_supported and app.config.get_per('accounts', + if obj.conn.get_module('MAM').available and app.config.get_per('accounts', account, 'sync_logs_with_server'): - obj.conn.request_archive_on_signin() + obj.conn.get_module('MAM').request_archive_on_signin() invisible_show = app.SHOW_LIST.index('invisible') # We cannot join rooms if we are invisible diff --git a/gajim/history_sync.py b/gajim/history_sync.py index 102b431eb..b051030d1 100644 --- a/gajim/history_sync.py +++ b/gajim/history_sync.py @@ -54,7 +54,6 @@ class HistorySyncAssistant(Gtk.Assistant): self.end = None self.next = None self.hide_buttons() - self.event_id = id(self) own_jid = self.con.get_own_jid().getStripped() @@ -88,9 +87,6 @@ class HistorySyncAssistant(Gtk.Assistant): app.ged.register_event_handler('archiving-count-received', ged.GUI1, self._received_count) - app.ged.register_event_handler('archiving-query-id', - ged.GUI1, - self._new_query_id) app.ged.register_event_handler('archiving-interval-finished', ged.GUI1, self._received_finished) @@ -145,28 +141,27 @@ class HistorySyncAssistant(Gtk.Assistant): log.info('start: %s', self.start) log.info('end: %s', self.end) - self.con.request_archive_count(self.event_id, self.start, self.end) + self.query_id = self.con.get_module('MAM').request_archive_count( + self.start, self.end) def _received_count(self, event): - if event.event_id != self.event_id: + if event.query_id != self.query_id: return + if event.count is not None: self.download_history.count = int(event.count) - self.con.request_archive_interval(self.event_id, self.start, self.end) + self.query_id = self.con.get_module('MAM').request_archive_interval( + self.start, self.end) def _received_finished(self, event): - if event.event_id != self.event_id: + if event.query_id != self.query_id: return + self.query_id = None log.info('query finished') GLib.idle_add(self.download_history.finished) self.set_current_page(Pages.SUMMARY) self.summary.finished() - def _new_query_id(self, event): - if event.event_id != self.event_id: - return - self.query_id = event.query_id - def _nec_mam_message_received(self, obj): if obj.conn.name != self.account: return @@ -193,9 +188,6 @@ class HistorySyncAssistant(Gtk.Assistant): app.ged.remove_event_handler('archiving-count-received', ged.GUI1, self._received_count) - app.ged.remove_event_handler('archiving-query-id', - ged.GUI1, - self._new_query_id) app.ged.remove_event_handler('archiving-interval-finished', ged.GUI1, self._received_finished) diff --git a/gajim/roster_window.py b/gajim/roster_window.py index a0972724b..4a3cef54d 100644 --- a/gajim/roster_window.py +++ b/gajim/roster_window.py @@ -5404,7 +5404,7 @@ class RosterWindow: self.on_privacy_lists_menuitem_activate, account) else: privacy_lists_menuitem.set_sensitive(False) - if app.connections[account].archiving_313_supported: + if app.connections[account].get_module('MAM').available: archiving_preferences_menuitem.connect( 'activate', self.on_archiving_preferences_menuitem_activate, account) diff --git a/gajim/server_info.py b/gajim/server_info.py index 423001003..d88904acc 100644 --- a/gajim/server_info.py +++ b/gajim/server_info.py @@ -174,7 +174,8 @@ class ServerInfoDialog(Gtk.Dialog): Feature('XEP-0280: Message Carbons', con.carbons_available, nbxmpp.NS_CARBONS, carbons_enabled), Feature('XEP-0313: Message Archive Management', - con.archiving_namespace, con.archiving_namespace, + con.get_module('MAM').archiving_namespace, + con.get_module('MAM').archiving_namespace, mam_enabled), Feature('XEP-0363: HTTP File Upload', con.get_module('HTTPUpload').available,