From b91728a07eef309d93cd53218c7ab42c7cee63eb Mon Sep 17 00:00:00 2001
From: Yann Leboulanger <asterix@lagaule.org>
Date: Tue, 11 Nov 2014 15:28:24 +0100
Subject: [PATCH] add XEP-313 (MAM) support

---
 src/chat_control.py                      |   2 +-
 src/common/config.py                     |   1 +
 src/common/connection.py                 |  10 ++
 src/common/connection_handlers.py        |  93 +++----------
 src/common/connection_handlers_events.py |  54 ++++++++
 src/common/logger.py                     |  12 +-
 src/common/message_archiving.py          | 166 +++++++++++++++++++++++
 src/gui_interface.py                     |   8 +-
 8 files changed, 266 insertions(+), 80 deletions(-)

diff --git a/src/chat_control.py b/src/chat_control.py
index 5975749c7b..ac24d0a8d5 100644
--- a/src/chat_control.py
+++ b/src/chat_control.py
@@ -2972,7 +2972,7 @@ class ChatControl(ChatControlBase):
             and gajim.HAVE_PYCRYPTO and self.contact.supports(NS_ESESSION):
                 self.begin_e2e_negotiation()
             elif (not self.session or not self.session.status) and \
-            gajim.connections[self.account].archiving_supported:
+            gajim.connections[self.account].archiving_136_supported:
                 self.begin_archiving_negotiation()
         else:
             self.send_chatstate('active', self.contact)
diff --git a/src/common/config.py b/src/common/config.py
index 1be56649cd..1a57e91533 100644
--- a/src/common/config.py
+++ b/src/common/config.py
@@ -316,6 +316,7 @@ class Config:
             'ignore_incoming_attention': [opt_bool, False, _('If True, Gajim will ignore incoming attention requestd ("wizz").')],
             'remember_opened_chat_controls': [ opt_bool, True, _('If enabled, Gajim will reopen chat windows that were opened last time Gajim was closed.')],
             'positive_184_ack': [ opt_bool, False, _('If enabled, Gajim will show an icon to show that sent message has been received by your contact')],
+            'last_mam_id': [opt_str, '', _('Last MAM id we are syncronized with')],
     }, {})
 
     __options_per_key = {
diff --git a/src/common/connection.py b/src/common/connection.py
index f0f90d2513..2580e68732 100644
--- a/src/common/connection.py
+++ b/src/common/connection.py
@@ -424,6 +424,8 @@ class CommonConnection:
             msg_iq.setTag(nbxmpp.NS_ENCRYPTED + ' x').setData(msgenc)
             if self.carbons_enabled:
                 msg_iq.addChild(name='private', namespace=nbxmpp.NS_CARBONS)
+            msg_iq.addChild(name='no-permanent-storage',
+                namespace=nbxmpp.NS_MSG_HINTS)
 
         if form_node:
             msg_iq.addChild(node=form_node)
@@ -502,6 +504,10 @@ class CommonConnection:
                     if self.carbons_enabled:
                         msg_iq.addChild(name='private',
                             namespace=nbxmpp.NS_CARBONS)
+                    msg_iq.addChild(name='no-permanent-storage',
+                        namespace=nbxmpp.NS_MSG_HINTS)
+                    msg_iq.addChild(name='no-copy',
+                        namespace=nbxmpp.NS_MSG_HINTS)
 
         if callback:
             callback(jid, msg, keyID, forward_from, session, original_message,
@@ -1959,8 +1965,12 @@ class Connection(CommonConnection, ConnectionHandlers):
                         # Remove stored bookmarks accessible to everyone.
                         self.send_pb_purge(our_jid, 'storage:bookmarks')
                         self.send_pb_delete(our_jid, 'storage:bookmarks')
+                if nbxmpp.NS_MAM in obj.features:
+                    self.archiving_supported = True
+                    self.archiving_313_supported = True
                 if nbxmpp.NS_ARCHIVE in obj.features:
                     self.archiving_supported = True
+                    self.archiving_136_supported = True
                 if nbxmpp.NS_ARCHIVE_AUTO in obj.features:
                     self.archive_auto_supported = True
                 if nbxmpp.NS_ARCHIVE_MANAGE in obj.features:
diff --git a/src/common/connection_handlers.py b/src/common/connection_handlers.py
index 8b98627584..f768e80da2 100644
--- a/src/common/connection_handlers.py
+++ b/src/common/connection_handlers.py
@@ -54,10 +54,8 @@ from common.pubsub import ConnectionPubSub
 from common.protocol.caps import ConnectionCaps
 from common.protocol.bytestream import ConnectionSocks5Bytestream
 from common.protocol.bytestream import ConnectionIBBytestream
-from common.message_archiving import ConnectionArchive
-from common.message_archiving import ARCHIVING_COLLECTIONS_ARRIVED
-from common.message_archiving import ARCHIVING_COLLECTION_ARRIVED
-from common.message_archiving import ARCHIVING_MODIFICATIONS_ARRIVED
+from common.message_archiving import ConnectionArchive136
+from common.message_archiving import ConnectionArchive313
 from common.connection_handlers_events import *
 
 from common import ged
@@ -464,6 +462,7 @@ class ConnectionVcard:
 
         if id_ not in self.awaiting_answers:
             return
+
         if self.awaiting_answers[id_][0] == VCARD_PUBLISHED:
             if iq_obj.getType() == 'result':
                 vcard_iq = self.awaiting_answers[id_][1]
@@ -482,6 +481,7 @@ class ConnectionVcard:
                 if self.vcard_sha != new_sha and gajim.SHOW_LIST[
                 self.connected] != 'invisible':
                     if not self.connection or self.connected < 2:
+                        del self.awaiting_answers[id_]
                         return
                     self.vcard_sha = new_sha
                     sshow = helpers.get_xmpp_show(gajim.SHOW_LIST[
@@ -495,6 +495,7 @@ class ConnectionVcard:
             elif iq_obj.getType() == 'error':
                 gajim.nec.push_incoming_event(VcardNotPublishedEvent(None,
                     conn=self))
+            del self.awaiting_answers[id_]
         elif self.awaiting_answers[id_][0] == VCARD_ARRIVED:
             # If vcard is empty, we send to the interface an empty vcard so that
             # it knows it arrived
@@ -517,10 +518,12 @@ class ConnectionVcard:
                 vcard = {'jid': jid, 'resource': resource}
                 gajim.nec.push_incoming_event(VcardReceivedEvent(None,
                     conn=self, vcard_dict=vcard))
+            del self.awaiting_answers[id_]
         elif self.awaiting_answers[id_][0] == AGENT_REMOVED:
             jid = self.awaiting_answers[id_][1]
             gajim.nec.push_incoming_event(AgentRemovedEvent(None, conn=self,
                 agent=jid))
+            del self.awaiting_answers[id_]
         elif self.awaiting_answers[id_][0] == METACONTACTS_ARRIVED:
             if not self.connection:
                 return
@@ -531,7 +534,9 @@ class ConnectionVcard:
                 if iq_obj.getErrorCode() not in ('403', '406', '404'):
                     self.private_storage_supported = False
             self.get_roster_delimiter()
+            del self.awaiting_answers[id_]
         elif self.awaiting_answers[id_][0] == DELIMITER_ARRIVED:
+            del self.awaiting_answers[id_]
             if not self.connection:
                 return
             if iq_obj.getType() == 'result':
@@ -566,7 +571,9 @@ class ConnectionVcard:
                 gajim.nec.push_incoming_event(RosterReceivedEvent(None,
                     conn=self))
             gobject.timeout_add_seconds(10, self.discover_servers)
+            del self.awaiting_answers[id_]
         elif self.awaiting_answers[id_][0] == PRIVACY_ARRIVED:
+            del self.awaiting_answers[id_]
             if iq_obj.getType() != 'error':
                 self.privacy_rules_supported = True
                 self.get_privacy_list('block')
@@ -595,6 +602,7 @@ class ConnectionVcard:
             # Ask metacontacts before roster
             self.get_metacontacts()
         elif self.awaiting_answers[id_][0] == BLOCKING_ARRIVED:
+            del self.awaiting_answers[id_]
             if iq_obj.getType() == 'result':
                 list_node = iq_obj.getTag('blocklist')
                 if not list_node:
@@ -603,6 +611,7 @@ class ConnectionVcard:
                 for i in list_node.iterTags('item'):
                     self.blocked_contacts.append(i.getAttr('jid'))
         elif self.awaiting_answers[id_][0] == PEP_CONFIG:
+            del self.awaiting_answers[id_]
             if iq_obj.getType() == 'error':
                 return
             if not iq_obj.getTag('pubsub'):
@@ -617,71 +626,6 @@ class ConnectionVcard:
                 gajim.nec.push_incoming_event(PEPConfigReceivedEvent(None,
                     conn=self, node=node, form=form))
 
-        elif self.awaiting_answers[id_][0] == ARCHIVING_COLLECTIONS_ARRIVED:
-            # TODO
-            print 'ARCHIVING_COLLECTIONS_ARRIVED'
-
-        elif self.awaiting_answers[id_][0] == ARCHIVING_COLLECTION_ARRIVED:
-            def save_if_not_exists(with_, nick, direction, tim, payload):
-                assert len(payload) == 1, 'got several archiving messages in' +\
-                    ' the same time %s' % ''.join(payload)
-                if payload[0].getName() == 'body':
-                    gajim.logger.save_if_not_exists(with_, direction, tim,
-                        msg=payload[0].getData(), nick=nick)
-                elif payload[0].getName() == 'message':
-                    print 'Not implemented'
-            chat = iq_obj.getTag('chat')
-            if chat:
-                with_ = chat.getAttr('with')
-                start_ = chat.getAttr('start')
-                tim = helpers.datetime_tuple(start_)
-                tim = timegm(tim)
-                nb = 0
-                for element in chat.getChildren():
-                    try:
-                        secs = int(element.getAttr('secs'))
-                    except TypeError:
-                        secs = 0
-                    if secs:
-                        tim += secs
-                    nick = element.getAttr('name')
-                    if element.getName() == 'from':
-                        save_if_not_exists(with_, nick, 'from', localtime(tim),
-                            element.getPayload())
-                        nb += 1
-                    if element.getName() == 'to':
-                        save_if_not_exists(with_, nick, 'to', localtime(tim),
-                            element.getPayload())
-                        nb += 1
-                set_ = chat.getTag('set')
-                first = set_.getTag('first')
-                if first:
-                    try:
-                        index = int(first.getAttr('index'))
-                    except TypeError:
-                        index = 0
-                try:
-                    count = int(set_.getTagData('count'))
-                except TypeError:
-                    count = 0
-                if count > index + nb:
-                    # Request the next page
-                    after = element.getTagData('last')
-                    self.request_collection_page(with_, start_, after=after)
-
-        elif self.awaiting_answers[id_][0] == ARCHIVING_MODIFICATIONS_ARRIVED:
-            modified = iq_obj.getTag('modified')
-            if modified:
-                for element in modified.getChildren():
-                    if element.getName() == 'changed':
-                        with_ = element.getAttr('with')
-                        start_ = element.getAttr('start')
-                        self.request_collection_page(with_, start_)
-                    #elif element.getName() == 'removed':
-                        # do nothing
-
-        del self.awaiting_answers[id_]
-
     def _vCardCB(self, con, vc):
         """
         Called when we receive a vCard Parse the vCard and send it to plugins
@@ -1423,13 +1367,14 @@ class ConnectionHandlersBase:
 
         return sess
 
-class ConnectionHandlers(ConnectionArchive, ConnectionVcard,
-ConnectionSocks5Bytestream, ConnectionDisco, ConnectionCommands,
-ConnectionPubSub, ConnectionPEP, ConnectionCaps, ConnectionHandlersBase,
-ConnectionJingle, ConnectionIBBytestream):
+class ConnectionHandlers(ConnectionArchive136, ConnectionArchive313,
+ConnectionVcard, ConnectionSocks5Bytestream, ConnectionDisco,
+ConnectionCommands, ConnectionPubSub, ConnectionPEP, ConnectionCaps,
+ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
     def __init__(self):
         global HAS_IDLE
-        ConnectionArchive.__init__(self)
+        ConnectionArchive136.__init__(self)
+        ConnectionArchive313.__init__(self)
         ConnectionVcard.__init__(self)
         ConnectionSocks5Bytestream.__init__(self)
         ConnectionIBBytestream.__init__(self)
diff --git a/src/common/connection_handlers_events.py b/src/common/connection_handlers_events.py
index 4264db2144..b9a18ddfd0 100644
--- a/src/common/connection_handlers_events.py
+++ b/src/common/connection_handlers_events.py
@@ -1089,6 +1089,60 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
                     return
                 self.forwarded = True
 
+        result = self.stanza.getTag('result', namespace=nbxmpp.NS_MAM)
+        if result:
+            forwarded = result.getTag('forwarded', namespace=nbxmpp.NS_FORWARD)
+            if not forwarded:
+                return
+            delay = forwarded.getTag('delay', namespace=nbxmpp.NS_DELAY2)
+            if not delay:
+                return
+            tim = delay.getAttr('stamp')
+            tim = helpers.datetime_tuple(tim)
+            tim = localtime(timegm(tim))
+            msg_ = forwarded.getTag('message')
+            to_ = msg_.getAttr('to')
+            if to_:
+                gajim.get_jid_without_resource(to_)
+            else:
+                to_ = gajim.get_jid_from_account(account)
+            frm_ = gajim.get_jid_without_resource(msg_.getAttr('from'))
+            nick = None
+            msg_txt = msg_.getTagData('body')
+            if to_ == gajim.get_jid_from_account(account):
+                with_ = frm_
+                direction = 'from'
+                res = gajim.get_resource_from_jid(msg_.getAttr('from'))
+            else:
+                with_ = to_
+                direction = 'to'
+                res = gajim.get_resource_from_jid(msg_.getAttr('to'))
+            is_pm = gajim.logger.jid_is_room_jid(with_)
+            if msg_.getAttr('type') == 'groupchat':
+                if is_pm == False:
+                    log.warn('JID %s is marked as normal contact in database '
+                        'but we got a groupchat message from it.')
+                    return
+                if is_pm == None:
+                    gajim.logger.get_jid_id(with_, 'ROOM')
+                nick = res
+            else:
+                if is_pm == None:
+                    # we don't know this JID, we need to disco it.
+                    server = gajim.get_server_from_jid(with_)
+                    if server not in self.conn.mam_awaiting_disco_result:
+                        self.conn.mam_awaiting_disco_result[server] = [
+                            [with_, direction, tim, msg_txt, res]]
+                        self.conn.discoverInfo(server)
+                    else:
+                        self.conn.mam_awaiting_disco_result[server].append(
+                            [with_, direction, tim, msg_txt, res])
+                    return
+
+            gajim.logger.save_if_not_exists(with_, direction, tim,
+                msg=msg_txt, nick=nick)
+            return
+
         self.enc_tag = self.stanza.getTag('x', namespace=nbxmpp.NS_ENCRYPTED)
 
         self.invite_tag = None
diff --git a/src/common/logger.py b/src/common/logger.py
index ac27b41428..b6a5642b2c 100644
--- a/src/common/logger.py
+++ b/src/common/logger.py
@@ -223,13 +223,17 @@ class Logger:
             return False
 
     def jid_is_room_jid(self, jid):
-        self.cur.execute('SELECT jid_id FROM jids WHERE jid=?  AND type=?',
-                (jid, constants.JID_ROOM_TYPE))
+        """
+        Return True if it's a room jid, False if it's not, None if we don't know
+        """
+        self.cur.execute('SELECT type FROM jids WHERE jid=?', (jid,))
         row = self.cur.fetchone()
         if row is None:
-            return False
+            return None
         else:
-            return True
+            if row[0] == constants.JID_ROOM_TYPE:
+                return True
+            return False
 
     def get_jid_id(self, jid, typestr=None):
         """
diff --git a/src/common/message_archiving.py b/src/common/message_archiving.py
index 5a5388f633..786a528691 100644
--- a/src/common/message_archiving.py
+++ b/src/common/message_archiving.py
@@ -29,9 +29,97 @@ log = logging.getLogger('gajim.c.message_archiving')
 ARCHIVING_COLLECTIONS_ARRIVED = 'archiving_collections_arrived'
 ARCHIVING_COLLECTION_ARRIVED = 'archiving_collection_arrived'
 ARCHIVING_MODIFICATIONS_ARRIVED = 'archiving_modifications_arrived'
+MAM_RESULTS_ARRIVED = 'mam_results_arrived'
 
 class ConnectionArchive:
     def __init__(self):
+        pass
+
+
+class ConnectionArchive313(ConnectionArchive):
+    def __init__(self):
+        ConnectionArchive.__init__(self)
+        self.archiving_313_supported = False
+        self.mam_awaiting_disco_result = {}
+        gajim.ged.register_event_handler('raw-iq-received', ged.CORE,
+            self._nec_raw_iq_313_received)
+        gajim.ged.register_event_handler('agent-info-error-received', ged.CORE,
+            self._nec_agent_info_error)
+        gajim.ged.register_event_handler('agent-info-received', ged.CORE,
+            self._nec_agent_info)
+
+    def cleanup(self):
+        gajim.ged.remove_event_handler('raw-iq-received', ged.CORE,
+            self._nec_raw_iq_313_received)
+
+    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 in self.mam_awaiting_disco_result:
+            for identity in obj.identities:
+                if identity['category'] == 'conference':
+                    # it's a groupchat
+                    for with_, direction, tim, msg_txt, res in \
+                    self.mam_awaiting_disco_result[obj.jid]:
+                        gajim.logger.get_jid_id(with_, 'ROOM')
+                        gajim.logger.save_if_not_exists(with_, direction, tim,
+                            msg=msg_txt, nick=res)
+                    del self.mam_awaiting_disco_result[obj.jid]
+                    return
+            # it's not a groupchat
+            for with_, direction, tim, msg_txt, res in \
+            self.mam_awaiting_disco_result[obj.jid]:
+                gajim.logger.get_jid_id(with_)
+                gajim.logger.save_if_not_exists(with_, direction, tim,
+                    msg=msg_txt)
+            del self.mam_awaiting_disco_result[obj.jid]
+
+    def _nec_raw_iq_313_received(self, obj):
+        if obj.conn.name != self.name:
+            return
+
+        id_ = obj.stanza.getID()
+        if id_ not in self.awaiting_answers:
+            return
+
+        if self.awaiting_answers[id_][0] == MAM_RESULTS_ARRIVED:
+            query = obj.stanza.getTag('query', namespace=nbxmpp.NS_MAM)
+            if query:
+                set_ = query.getTag('set', namespace=nbxmpp.NS_RSM)
+                if set_:
+                    last = set_.getTagData('last')
+                    if last:
+                        gajim.config.set('last_mam_id', last)
+                        self.request_archive(after=last)
+            del self.awaiting_answers[id_]
+
+    def request_archive(self, start=None, end=None, with_=None, after=None,
+    max=30):
+        iq_ = nbxmpp.Iq('get')
+        query = iq_.setTag('query', namespace=nbxmpp.NS_MAM)
+        if start:
+            query.addChild('start', payload=start)
+        if end:
+            query.addChild('end', payload=end)
+        if with_:
+            query.addChild('with', payload=with_)
+        set_ = query.setTag('set', namespace=nbxmpp.NS_RSM)
+        set_.setTagData('max', max)
+        if after:
+            set_.setTagData('after', after)
+        id_ = self.connection.getAnID()
+        iq_.setID(id_)
+        self.awaiting_answers[id_] = (MAM_RESULTS_ARRIVED, )
+        self.connection.send(iq_)
+
+
+class ConnectionArchive136(ConnectionArchive):
+    def __init__(self):
+        ConnectionArchive.__init__(self)
+        self.archiving_136_supported = False
         self.archive_auto_supported = False
         self.archive_manage_supported = False
         self.archive_manual_supported = False
@@ -45,11 +133,89 @@ class ConnectionArchive:
         gajim.ged.register_event_handler(
             'archiving-preferences-changed-received', ged.CORE,
             self._nec_archiving_changed_received)
+        gajim.ged.register_event_handler('raw-iq-received', ged.CORE,
+            self._nec_raw_iq_136_received)
 
     def cleanup(self):
         gajim.ged.remove_event_handler(
             'archiving-preferences-changed-received', ged.CORE,
             self._nec_archiving_changed_received)
+        gajim.ged.remove_event_handler('raw-iq-received', ged.CORE,
+            self._nec_raw_iq_136_received)
+
+    def _nec_raw_iq_136_received(self, obj):
+        if obj.conn.name != self.name:
+            return
+
+        id_ = obj.stanza.getID()
+        if id_ not in self.awaiting_answers:
+            return
+
+        if self.awaiting_answers[id_][0] == ARCHIVING_COLLECTIONS_ARRIVED:
+            del self.awaiting_answers[id_]
+            # TODO
+            print 'ARCHIVING_COLLECTIONS_ARRIVED'
+
+        elif self.awaiting_answers[id_][0] == ARCHIVING_COLLECTION_ARRIVED:
+            def save_if_not_exists(with_, nick, direction, tim, payload):
+                assert len(payload) == 1, 'got several archiving messages in' +\
+                    ' the same time %s' % ''.join(payload)
+                if payload[0].getName() == 'body':
+                    gajim.logger.save_if_not_exists(with_, direction, tim,
+                        msg=payload[0].getData(), nick=nick)
+                elif payload[0].getName() == 'message':
+                    print 'Not implemented'
+            chat = iq_obj.getTag('chat')
+            if chat:
+                with_ = chat.getAttr('with')
+                start_ = chat.getAttr('start')
+                tim = helpers.datetime_tuple(start_)
+                tim = timegm(tim)
+                nb = 0
+                for element in chat.getChildren():
+                    try:
+                        secs = int(element.getAttr('secs'))
+                    except TypeError:
+                        secs = 0
+                    if secs:
+                        tim += secs
+                    nick = element.getAttr('name')
+                    if element.getName() == 'from':
+                        save_if_not_exists(with_, nick, 'from', localtime(tim),
+                            element.getPayload())
+                        nb += 1
+                    if element.getName() == 'to':
+                        save_if_not_exists(with_, nick, 'to', localtime(tim),
+                            element.getPayload())
+                        nb += 1
+                set_ = chat.getTag('set')
+                first = set_.getTag('first')
+                if first:
+                    try:
+                        index = int(first.getAttr('index'))
+                    except TypeError:
+                        index = 0
+                try:
+                    count = int(set_.getTagData('count'))
+                except TypeError:
+                    count = 0
+                if count > index + nb:
+                    # Request the next page
+                    after = element.getTagData('last')
+                    self.request_collection_page(with_, start_, after=after)
+            del self.awaiting_answers[id_]
+
+        elif self.awaiting_answers[id_][0] == ARCHIVING_MODIFICATIONS_ARRIVED:
+            modified = iq_obj.getTag('modified')
+            if modified:
+                for element in modified.getChildren():
+                    if element.getName() == 'changed':
+                        with_ = element.getAttr('with')
+                        start_ = element.getAttr('start')
+                        self.request_collection_page(with_, start_)
+                    #elif element.getName() == 'removed':
+                        # do nothing
+            del self.awaiting_answers[id_]
 
     def request_message_archiving_preferences(self):
         iq_ = nbxmpp.Iq('get')
diff --git a/src/gui_interface.py b/src/gui_interface.py
index 7d481d0a80..66258341a4 100644
--- a/src/gui_interface.py
+++ b/src/gui_interface.py
@@ -1133,12 +1133,18 @@ class Interface:
             # Else disable autoaway
             gajim.sleeper_state[account] = 'off'
 
-        if obj.conn.archiving_supported:
+        if obj.conn.archiving_136_supported:
             # Start merging logs from server
             obj.conn.request_modifications_page(gajim.config.get_per('accounts',
                 account, 'last_archiving_time'))
             gajim.config.set_per('accounts', account, 'last_archiving_time',
                 time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()))
+        if obj.conn.archiving_313_supported:
+            mam_id = gajim.config.get('last_mam_id')
+            if mam_id:
+                obj.conn.request_archive(after=mam_id)
+            else:
+                obj.conn.request_archive(start='2013-02-24T03:51:42Z')
 
         invisible_show = gajim.SHOW_LIST.index('invisible')
         # We cannot join rooms if we are invisible
-- 
GitLab