[Git][gajim/gajim][master] 2 commits: Refactor MAM into own module

Philipp Hörist gitlab at dev.gajim.org
Sun Jul 15 14:37:03 CEST 2018


Philipp Hörist pushed to branch master at gajim / gajim


Commits:
ebbe06d5 by Philipp Hörist at 2018-07-15T14:26:00+02:00
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

- - - - -
dd664643 by Philipp Hörist at 2018-07-15T14:32:08+02:00
Move History Sync Dialog into gtk module

- - - - -


25 changed files:

- gajim/app_actions.py
- gajim/application.py
- gajim/chat_control.py
- gajim/common/app.py
- gajim/common/config.py
- gajim/common/connection.py
- gajim/common/connection_handlers.py
- gajim/common/connection_handlers_events.py
- gajim/common/helpers.py
- gajim/common/logger.py
- − gajim/common/message_archiving.py
- + gajim/common/modules/date_and_time.py
- + gajim/common/modules/mam.py
- + gajim/common/modules/misc.py
- − gajim/data/gui/archiving_313_preferences_item.ui
- gajim/data/gui/archiving_313_preferences_window.ui → gajim/data/gui/mam_preferences.ui
- gajim/dialogs.py
- gajim/groupchat_control.py
- + gajim/gtk/__init__.py
- gajim/history_sync.py → gajim/gtk/history_sync.py
- + gajim/gtk/mam_preferences.py
- + gajim/gtk/util.py
- gajim/gui_interface.py
- gajim/roster_window.py
- gajim/server_info.py


Changes:

=====================================
gajim/app_actions.py
=====================================
--- a/gajim/app_actions.py
+++ b/gajim/app_actions.py
@@ -27,8 +27,9 @@ from gajim import accounts_window
 import gajim.plugins.gui
 from gajim import history_window
 from gajim import disco
-from gajim.history_sync import HistorySyncAssistant
+from gajim.gtk.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):


=====================================
gajim/application.py
=====================================
--- 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',


=====================================
gajim/chat_control.py
=====================================
--- 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:


=====================================
gajim/common/app.py
=====================================
--- 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


=====================================
gajim/common/config.py
=====================================
--- 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 = {


=====================================
gajim/common/connection.py
=====================================
--- 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':


=====================================
gajim/common/connection_handlers.py
=====================================
--- 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)


=====================================
gajim/common/connection_handlers_events.py
=====================================
--- 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 = []


=====================================
gajim/common/helpers.py
=====================================
--- 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(',')


=====================================
gajim/common/logger.py
=====================================
--- 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):


=====================================
gajim/common/message_archiving.py deleted
=====================================
--- 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 <elghinn AT free.fr>
-##
-## 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 <http://www.gnu.org/licenses/>.
-##
-
-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


=====================================
gajim/common/modules/date_and_time.py
=====================================
--- /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 <http://www.gnu.org/licenses/>.
+
+# 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


=====================================
gajim/common/modules/mam.py
=====================================
--- /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 <http://www.gnu.org/licenses/>.
+
+# 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'


=====================================
gajim/common/modules/misc.py
=====================================
--- /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 <http://www.gnu.org/licenses/>.
+
+# 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)


=====================================
gajim/data/gui/archiving_313_preferences_item.ui deleted
=====================================
--- a/gajim/data/gui/archiving_313_preferences_item.ui
+++ /dev/null
@@ -1,143 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.18.3 -->
-<interface>
-  <requires lib="gtk+" version="3.12"/>
-  <object class="GtkListStore" id="dialog_pref_liststore">
-    <columns>
-      <!-- column-name gchararray1 -->
-      <column type="gchararray"/>
-    </columns>
-    <data>
-      <row>
-        <col id="0" translatable="yes">Always</col>
-      </row>
-      <row>
-        <col id="0" translatable="yes">Never</col>
-      </row>
-    </data>
-  </object>
-  <object class="GtkDialog" id="item_dialog">
-    <property name="can_focus">False</property>
-    <property name="border_width">12</property>
-    <property name="resizable">False</property>
-    <property name="destroy_with_parent">True</property>
-    <property name="type_hint">dialog</property>
-    <signal name="destroy" handler="on_item_archiving_preferences_window_destroy" swapped="no"/>
-    <child internal-child="vbox">
-      <object class="GtkBox" id="dialog-vbox">
-        <property name="can_focus">False</property>
-        <property name="orientation">vertical</property>
-        <property name="spacing">20</property>
-        <child internal-child="action_area">
-          <object class="GtkButtonBox" id="dialog-action_area">
-            <property name="can_focus">False</property>
-            <property name="layout_style">end</property>
-            <child>
-              <object class="GtkButton" id="cancel_button">
-                <property name="label">gtk-close</property>
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <property name="use_stock">True</property>
-                <property name="always_show_image">True</property>
-                <signal name="clicked" handler="on_cancel_button_clicked" swapped="no"/>
-              </object>
-              <packing>
-                <property name="expand">True</property>
-                <property name="fill">True</property>
-                <property name="position">0</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkButton" id="ok_button">
-                <property name="label">gtk-ok</property>
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <property name="use_stock">True</property>
-                <property name="always_show_image">True</property>
-                <signal name="clicked" handler="on_ok_button_clicked" swapped="no"/>
-              </object>
-              <packing>
-                <property name="expand">True</property>
-                <property name="fill">True</property>
-                <property name="position">1</property>
-              </packing>
-            </child>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">False</property>
-            <property name="position">0</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkGrid" id="dialog_grid">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="row_spacing">5</property>
-            <property name="column_spacing">5</property>
-            <child>
-              <object class="GtkLabel" id="jid_label">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="label" translatable="yes">Jabber ID:</property>
-                <property name="xalign">0</property>
-              </object>
-              <packing>
-                <property name="left_attach">0</property>
-                <property name="top_attach">0</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkLabel" id="pref_label">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="label" translatable="yes">Preference:</property>
-                <property name="xalign">0</property>
-              </object>
-              <packing>
-                <property name="left_attach">0</property>
-                <property name="top_attach">1</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkEntry" id="jid_entry">
-                <property name="width_request">194</property>
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-              </object>
-              <packing>
-                <property name="left_attach">1</property>
-                <property name="top_attach">0</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkComboBox" id="pref_cb">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="halign">start</property>
-                <property name="model">dialog_pref_liststore</property>
-                <child>
-                  <object class="GtkCellRendererText" id="cellrenderertext2"/>
-                  <attributes>
-                    <attribute name="text">0</attribute>
-                  </attributes>
-                </child>
-              </object>
-              <packing>
-                <property name="left_attach">1</property>
-                <property name="top_attach">1</property>
-              </packing>
-            </child>
-          </object>
-          <packing>
-            <property name="expand">False</property>
-            <property name="fill">True</property>
-            <property name="position">1</property>
-          </packing>
-        </child>
-      </object>
-    </child>
-  </object>
-</interface>


=====================================
gajim/data/gui/archiving_313_preferences_window.ui → gajim/data/gui/mam_preferences.ui
=====================================
--- a/gajim/data/gui/archiving_313_preferences_window.ui
+++ b/gajim/data/gui/mam_preferences.ui
@@ -1,57 +1,169 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.20.0 -->
+<!-- Generated with glade 3.22.1 -->
 <interface>
   <requires lib="gtk+" version="3.12"/>
-  <object class="GtkImage" id="add_image">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="stock">gtk-add</property>
-  </object>
-  <object class="GtkListStore" id="archive_items_liststore">
+  <object class="GtkListStore" id="default_store">
     <columns>
-      <!-- column-name jid -->
+      <!-- column-name text -->
       <column type="gchararray"/>
-      <!-- column-name archive_pref -->
-      <column type="gchararray"/>
-    </columns>
-  </object>
-  <object class="GtkListStore" id="default_pref_liststore">
-    <columns>
-      <!-- column-name gchararray1 -->
+      <!-- column-name value -->
       <column type="gchararray"/>
     </columns>
     <data>
       <row>
         <col id="0" translatable="yes">Always</col>
+        <col id="1">always</col>
       </row>
       <row>
         <col id="0" translatable="yes">Roster</col>
+        <col id="1">roster</col>
       </row>
       <row>
         <col id="0" translatable="yes">Never</col>
+        <col id="1">never</col>
       </row>
     </data>
   </object>
-  <object class="GtkImage" id="remove_image">
-    <property name="visible">True</property>
-    <property name="can_focus">False</property>
-    <property name="stock">gtk-remove</property>
+  <object class="GtkListStore" id="preferences_store">
+    <columns>
+      <!-- column-name jid -->
+      <column type="gchararray"/>
+      <!-- column-name gboolean1 -->
+      <column type="gboolean"/>
+    </columns>
   </object>
-  <object class="GtkWindow" id="archiving_313_pref">
+  <object class="GtkGrid" id="preferences_grid">
+    <property name="width_request">400</property>
+    <property name="height_request">300</property>
+    <property name="visible">True</property>
     <property name="can_focus">False</property>
-    <property name="border_width">12</property>
-    <property name="window_position">center</property>
-    <property name="default_width">450</property>
-    <signal name="destroy" handler="on_archiving_preferences_window_destroy" swapped="no"/>
-    <signal name="key-press-event" handler="on_key_press_event" swapped="no"/>
+    <property name="margin_left">18</property>
+    <property name="margin_right">18</property>
+    <property name="margin_top">18</property>
+    <property name="margin_bottom">18</property>
+    <property name="row_spacing">5</property>
+    <property name="column_spacing">10</property>
+    <child>
+      <object class="GtkButtonBox">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="spacing">5</property>
+        <property name="layout_style">start</property>
+        <child>
+          <object class="GtkButton" id="add_button">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <signal name="clicked" handler="_on_add" swapped="no"/>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon_name">list-add-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+            <property name="non_homogeneous">True</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="remove_button">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <signal name="clicked" handler="_on_remove" swapped="no"/>
+            <child>
+              <object class="GtkImage">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="icon_name">list-remove-symbolic</property>
+              </object>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">True</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+            <property name="non_homogeneous">True</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">2</property>
+      </packing>
+    </child>
     <child>
-      <object class="GtkGrid" id="pref_grid">
+      <object class="GtkGrid">
         <property name="visible">True</property>
         <property name="can_focus">False</property>
-        <property name="row_spacing">5</property>
-        <property name="column_spacing">10</property>
+        <property name="halign">start</property>
+        <property name="column_spacing">5</property>
         <child>
-          <object class="GtkScrolledWindow" id="scrolledwindow1">
+          <object class="GtkLabel" id="default_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="label" translatable="yes">Default:</property>
+            <property name="xalign">0</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkComboBox" id="default_cb">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">start</property>
+            <property name="hexpand">False</property>
+            <property name="model">default_store</property>
+            <property name="active">0</property>
+            <property name="id_column">1</property>
+            <child>
+              <object class="GtkCellRendererText" id="cellrenderertext1"/>
+              <attributes>
+                <attribute name="text">0</attribute>
+              </attributes>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">0</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkButton" id="save_button">
+        <property name="label">Save</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="receives_default">True</property>
+        <property name="halign">end</property>
+        <property name="always_show_image">True</property>
+        <signal name="clicked" handler="_on_save" swapped="no"/>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkOverlay" id="overlay">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <child>
+          <object class="GtkScrolledWindow">
             <property name="height_request">150</property>
             <property name="visible">True</property>
             <property name="can_focus">True</property>
@@ -59,21 +171,27 @@
             <property name="vexpand">True</property>
             <property name="shadow_type">in</property>
             <child>
-              <object class="GtkTreeView" id="archive_view">
+              <object class="GtkTreeView" id="pref_view">
                 <property name="visible">True</property>
                 <property name="can_focus">True</property>
-                <property name="model">archive_items_liststore</property>
+                <property name="model">preferences_store</property>
+                <property name="search_column">0</property>
                 <child internal-child="selection">
                   <object class="GtkTreeSelection" id="treeview-selection2"/>
                 </child>
                 <child>
                   <object class="GtkTreeViewColumn" id="treeviewcolumn1">
                     <property name="title" translatable="yes">Jabber ID</property>
+                    <property name="expand">True</property>
                     <property name="clickable">True</property>
                     <property name="sort_indicator">True</property>
                     <property name="sort_column_id">0</property>
                     <child>
-                      <object class="GtkCellRendererText" id="cellrenderertext3"/>
+                      <object class="GtkCellRendererText" id="cellrenderertext3">
+                        <property name="editable">True</property>
+                        <property name="placeholder_text">user at example.org</property>
+                        <signal name="edited" handler="_jid_edited" swapped="no"/>
+                      </object>
                       <attributes>
                         <attribute name="text">0</attribute>
                       </attributes>
@@ -82,14 +200,17 @@
                 </child>
                 <child>
                   <object class="GtkTreeViewColumn" id="treeviewcolumn2">
-                    <property name="title" translatable="yes">Preference</property>
+                    <property name="title" translatable="yes">Archive</property>
                     <property name="clickable">True</property>
+                    <property name="alignment">0.5</property>
                     <property name="sort_indicator">True</property>
                     <property name="sort_column_id">1</property>
                     <child>
-                      <object class="GtkCellRendererText" id="cellrenderertext4"/>
+                      <object class="GtkCellRendererToggle">
+                        <signal name="toggled" handler="_pref_toggled" swapped="no"/>
+                      </object>
                       <attributes>
-                        <attribute name="text">1</attribute>
+                        <attribute name="active">1</attribute>
                       </attributes>
                     </child>
                   </object>
@@ -98,117 +219,18 @@
             </child>
           </object>
           <packing>
-            <property name="left_attach">0</property>
-            <property name="top_attach">1</property>
-            <property name="width">2</property>
+            <property name="index">-1</property>
           </packing>
         </child>
-        <child>
-          <object class="GtkButtonBox" id="buttonbox1">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="halign">start</property>
-            <property name="spacing">5</property>
-            <property name="layout_style">start</property>
-            <child>
-              <object class="GtkButton" id="add_button">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <property name="image">add_image</property>
-                <signal name="clicked" handler="on_add_item_button_clicked" swapped="no"/>
-              </object>
-              <packing>
-                <property name="expand">False</property>
-                <property name="fill">True</property>
-                <property name="position">0</property>
-                <property name="non_homogeneous">True</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkButton" id="remove_button">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
-                <property name="receives_default">True</property>
-                <property name="image">remove_image</property>
-                <signal name="clicked" handler="on_remove_item_button_clicked" swapped="no"/>
-              </object>
-              <packing>
-                <property name="expand">True</property>
-                <property name="fill">True</property>
-                <property name="position">1</property>
-                <property name="non_homogeneous">True</property>
-              </packing>
-            </child>
-          </object>
-          <packing>
-            <property name="left_attach">0</property>
-            <property name="top_attach">2</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkGrid" id="grid1">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="halign">start</property>
-            <property name="column_spacing">5</property>
-            <child>
-              <object class="GtkLabel" id="default_label">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="label" translatable="yes">Default:</property>
-                <property name="xalign">0</property>
-              </object>
-              <packing>
-                <property name="left_attach">0</property>
-                <property name="top_attach">0</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkComboBox" id="default_cb">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="halign">start</property>
-                <property name="hexpand">False</property>
-                <property name="model">default_pref_liststore</property>
-                <child>
-                  <object class="GtkCellRendererText" id="cellrenderertext1"/>
-                  <attributes>
-                    <attribute name="text">0</attribute>
-                  </attributes>
-                </child>
-              </object>
-              <packing>
-                <property name="left_attach">1</property>
-                <property name="top_attach">0</property>
-              </packing>
-            </child>
-          </object>
-          <packing>
-            <property name="left_attach">0</property>
-            <property name="top_attach">0</property>
-          </packing>
-        </child>
-        <child>
-          <object class="GtkButton" id="save_button">
-            <property name="label">gtk-save</property>
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">True</property>
-            <property name="halign">end</property>
-            <property name="use_stock">True</property>
-            <property name="always_show_image">True</property>
-            <signal name="clicked" handler="on_save_button_clicked" swapped="no"/>
-          </object>
-          <packing>
-            <property name="left_attach">1</property>
-            <property name="top_attach">2</property>
-          </packing>
-        </child>
-        <child>
-          <placeholder/>
-        </child>
       </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+        <property name="width">2</property>
+      </packing>
+    </child>
+    <child>
+      <placeholder/>
     </child>
   </object>
 </interface>


=====================================
gajim/dialogs.py
=====================================
--- 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


=====================================
gajim/groupchat_control.py
=====================================
--- 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)
 


=====================================
gajim/gtk/__init__.py
=====================================
--- /dev/null
+++ b/gajim/gtk/__init__.py


=====================================
gajim/history_sync.py → gajim/gtk/history_sync.py
=====================================
--- a/gajim/history_sync.py
+++ b/gajim/gtk/history_sync.py
@@ -1,17 +1,14 @@
-# -*- coding: utf-8 -*-
-#
 # Copyright (C) 2017 Philipp Hörist <philipp AT hoerist.com>
 #
 # 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, either version 3 of the License, or
-# (at your option) any later version.
+# 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
+# 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
@@ -25,8 +22,8 @@ from gi.repository import Gtk, GLib
 
 from gajim.common import app
 from gajim.common import ged
-from gajim.gtkgui_helpers import get_icon_pixmap
 from gajim.common.const import ArchiveState
+from gajim.gtk.util import load_icon
 
 log = logging.getLogger('gajim.c.message_archiving')
 
@@ -40,7 +37,7 @@ class Pages(IntEnum):
 class HistorySyncAssistant(Gtk.Assistant):
     def __init__(self, account, parent):
         Gtk.Assistant.__init__(self)
-        self.set_title(_('Synchronise History'))
+        # self.set_title(_('Synchronise History'))
         self.set_resizable(False)
         self.set_default_size(300, -1)
         self.set_name('HistorySyncAssistant')
@@ -54,7 +51,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 +84,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)
@@ -107,10 +100,6 @@ class HistorySyncAssistant(Gtk.Assistant):
             self.set_current_page(Pages.SUMMARY)
             self.summary.nothing_to_do()
 
-        # if self.con.mam_query_ids:
-        #     self.set_current_page(Pages.SUMMARY)
-        #     self.summary.query_already_running()
-
         self.show_all()
 
     def hide_buttons(self):
@@ -145,33 +134,34 @@ 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:
+    def _nec_mam_message_received(self, event):
+        if event.conn.name != self.account:
             return
 
-        if obj.result.getAttr('queryid') != self.query_id:
+        result = event.stanza.getTag('result')
+        queryid = result.getAttr('queryid')
+        if queryid != self.query_id:
             return
 
         log.debug('received message')
@@ -193,9 +183,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)
@@ -244,9 +231,8 @@ class DownloadHistoryPage(Gtk.Box):
         self.count = 0
         self.received = 0
 
-        pix = get_icon_pixmap('folder-download-symbolic', size=64)
-        image = Gtk.Image()
-        image.set_from_pixbuf(pix)
+        surface = load_icon('folder-download-symbolic', self, size=64)
+        image = Gtk.Image.new_from_surface(surface)
 
         self.progress = Gtk.ProgressBar()
         self.progress.set_show_text(True)
@@ -312,7 +298,7 @@ class TimeOption(Gtk.Label):
         super().__init__(label=label)
         self.date = months
         if months:
-            self.date = timedelta(days=30*months)
+            self.date = timedelta(days=30 * months)
 
     def get_delta(self):
         return self.date


=====================================
gajim/gtk/mam_preferences.py
=====================================
--- /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 <http://www.gnu.org/licenses/>.
+
+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)


=====================================
gajim/gtk/util.py
=====================================
--- /dev/null
+++ b/gajim/gtk/util.py
@@ -0,0 +1,77 @@
+# 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 <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+import logging
+
+from gi.repository import Gtk
+from gi.repository import GLib
+import xml.etree.ElementTree as ET
+
+from gajim.common import i18n
+from gajim.common import configpaths
+
+_icon_theme = Gtk.IconTheme.get_default()
+_icon_theme.append_search_path(configpaths.get('ICONS'))
+
+log = logging.getLogger('gajim.gtk.util')
+
+
+def load_icon(icon_name, widget, size=16,
+              flags=Gtk.IconLookupFlags.FORCE_SIZE):
+
+    scale = widget.get_scale_factor()
+    if not scale:
+        log.warning('Could not determine scale factor')
+        scale = 1
+
+    try:
+        iconinfo = _icon_theme.lookup_icon_for_scale(
+            icon_name, size, scale, flags)
+        return iconinfo.load_surface(None)
+    except GLib.GError as e:
+        log.error('Unable to load icon %s: %s', icon_name, str(e))
+
+
+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)


=====================================
gajim/gui_interface.py
=====================================
--- 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


=====================================
gajim/roster_window.py
=====================================
--- 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)


=====================================
gajim/server_info.py
=====================================
--- 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,



View it on GitLab: https://dev.gajim.org/gajim/gajim/compare/72ee9af79c79e95c05a9b1a11f5f2a7f11e088c1...dd664643bd72281b2c777527db692f44338d3d29

-- 
View it on GitLab: https://dev.gajim.org/gajim/gajim/compare/72ee9af79c79e95c05a9b1a11f5f2a7f11e088c1...dd664643bd72281b2c777527db692f44338d3d29
You're receiving this email because of your account on dev.gajim.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.gajim.org/pipermail/commits/attachments/20180715/0e96dad1/attachment-0001.html>


More information about the Commits mailing list