From 0a8af73650e2412ba3203d8152be3ed6f5b4050d Mon Sep 17 00:00:00 2001 From: Yann Leboulanger 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 067e2eeea..e29474ff7 100644 --- a/src/chat_control.py +++ b/src/chat_control.py @@ -3006,7 +3006,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 739d3f7d1..28d5f657d 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 47b0fb5e0..898b88207 100644 --- a/src/common/connection.py +++ b/src/common/connection.py @@ -423,6 +423,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) @@ -501,6 +503,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, @@ -1977,8 +1983,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 898018646..a8f4b65c7 100644 --- a/src/common/connection_handlers.py +++ b/src/common/connection_handlers.py @@ -53,10 +53,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 @@ -463,6 +461,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] @@ -481,6 +480,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[ @@ -494,6 +494,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 @@ -516,10 +517,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 @@ -530,7 +533,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': @@ -565,7 +570,9 @@ class ConnectionVcard: gajim.nec.push_incoming_event(RosterReceivedEvent(None, conn=self)) GLib.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') @@ -594,6 +601,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: @@ -602,6 +610,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'): @@ -616,71 +625,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 @@ -1421,13 +1365,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 af9c1662f..22339be8e 100644 --- a/src/common/connection_handlers_events.py +++ b/src/common/connection_handlers_events.py @@ -1088,6 +1088,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 cfba7fb75..9c8aed914 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 69cf9a21d..04ff224af 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 19b2e9b80..b4e5aa307 100644 --- a/src/gui_interface.py +++ b/src/gui_interface.py @@ -1134,12 +1134,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