From 37f7a80396e5b73e85c45b7fdd61a334bca612f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philipp=20H=C3=B6rist?= Date: Thu, 19 Jul 2018 21:26:45 +0200 Subject: [PATCH] Move message handler into own module --- gajim/chat_control.py | 14 + gajim/common/connection_handlers.py | 129 +------- gajim/common/connection_handlers_events.py | 236 -------------- gajim/common/modules/__init__.py | 2 +- gajim/common/modules/chatstates.py | 33 ++ gajim/common/modules/mam.py | 28 +- gajim/common/modules/message.py | 297 ++++++++++++++++++ gajim/common/modules/misc.py | 36 +++ gajim/common/modules/receipts.py | 105 +++++++ gajim/common/modules/user_nickname.py | 7 + gajim/common/modules/util.py | 41 +++ .../zeroconf/connection_handlers_zeroconf.py | 132 +++++++- test/unit/test_sessions.py | 2 +- 13 files changed, 665 insertions(+), 397 deletions(-) create mode 100644 gajim/common/modules/chatstates.py create mode 100644 gajim/common/modules/message.py create mode 100644 gajim/common/modules/receipts.py create mode 100644 gajim/common/modules/util.py diff --git a/gajim/chat_control.py b/gajim/chat_control.py index ef8d2d7e3..04395e209 100644 --- a/gajim/chat_control.py +++ b/gajim/chat_control.py @@ -241,6 +241,9 @@ class ChatControl(ChatControlBase): app.ged.register_event_handler( 'decrypted-message-received', ged.GUI1, self._nec_decrypted_message_received) + app.ged.register_event_handler( + 'receipt-received', + ged.GUI1, self._receipt_received) # PluginSystem: adding GUI extension point for this ChatControl # instance object @@ -984,6 +987,14 @@ class ChatControl(ChatControlBase): else: self.old_msg_kind = kind + def _receipt_received(self, event): + if event.conn.name != self.account: + return + if event.jid != self.contact.jid: + return + + self.conv_textview.show_xep0184_ack(event.receipt_id) + def get_tab_label(self): unread = '' if self.resource: @@ -1153,6 +1164,9 @@ class ChatControl(ChatControlBase): app.ged.remove_event_handler( 'decrypted-message-received', ged.GUI1, self._nec_decrypted_message_received) + app.ged.remove_event_handler( + 'receipt-received', + ged.GUI1, self._receipt_received) self.unsubscribe_events() diff --git a/gajim/common/connection_handlers.py b/gajim/common/connection_handlers.py index 5a0fd44be..a55158de3 100644 --- a/gajim/common/connection_handlers.py +++ b/gajim/common/connection_handlers.py @@ -292,22 +292,10 @@ class ConnectionHandlersBase: # We decrypt GPG messages one after the other. Keep queue in mem self.gpg_messages_to_decrypt = [] - # XEPs that are based on Message - self._message_namespaces = set([nbxmpp.NS_HTTP_AUTH, - nbxmpp.NS_PUBSUB_EVENT, - nbxmpp.NS_ROSTERX, - nbxmpp.NS_MAM_1, - nbxmpp.NS_MAM_2, - nbxmpp.NS_CONFERENCE]) - app.ged.register_event_handler('iq-error-received', ged.CORE, self._nec_iq_error_received) app.ged.register_event_handler('presence-received', ged.CORE, self._nec_presence_received) - app.ged.register_event_handler('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, self._nec_gc_message_received) @@ -316,10 +304,6 @@ class ConnectionHandlersBase: self._nec_iq_error_received) app.ged.remove_event_handler('presence-received', ged.CORE, self._nec_presence_received) - app.ged.remove_event_handler('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, self._nec_gc_message_received) @@ -448,96 +432,10 @@ class ConnectionHandlersBase: message=obj.status, show=show) - def _nec_message_received(self, obj): - if obj.conn.name != self.name: - return - - app.plugin_manager.extension_point( - 'decrypt', self, obj, self._on_message_received) - if not obj.encrypted: - eme = parse_eme(obj.stanza) - if eme is not None: - obj.msgtxt = eme - self._on_message_received(obj) - - def _on_message_received(self, 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: - return - - # Receipt requested - # TODO: We shouldn't answer if we're invisible! - contact = app.contacts.get_contact(self.name, obj.jid) - nick = obj.resource - gc_contact = app.contacts.get_gc_contact(self.name, obj.jid, nick) - if obj.sent: - jid_to = obj.stanza.getFrom() - else: - jid_to = obj.stanza.getTo() - reply = False - if not jid_to: - reply = True - else: - fjid_to = str(jid_to) - if self.name != 'Local': - # Dont check precis for zeroconf - fjid_to = helpers.parse_jid(str(jid_to)) - jid_to = app.get_jid_without_resource(fjid_to) - if jid_to == app.get_jid_from_account(self.name): - reply = True - - if obj.jid != app.get_jid_from_account(self.name): - if obj.receipt_request_tag and app.config.get_per('accounts', - self.name, 'answer_receipts') and ((contact and contact.sub \ - not in ('to', 'none')) or gc_contact) and obj.mtype != 'error' and \ - reply: - receipt = nbxmpp.Message(to=obj.fjid, typ='chat') - receipt.setTag('received', namespace='urn:xmpp:receipts', - attrs={'id': obj.id_}) - - if obj.thread_id: - receipt.setThread(obj.thread_id) - self.connection.send(receipt) - - # We got our message's receipt - if obj.receipt_received_tag and app.config.get_per('accounts', - self.name, 'request_receipt'): - ctrl = None - if obj.session is not None: - ctrl = obj.session.control - if not ctrl: - # Received doesn't have the element - # or control is not bound to session? - # --> search for it - ctrl = app.interface.msg_win_mgr.search_control(obj.jid, - obj.conn.name, obj.resource) - - if ctrl: - id_ = obj.receipt_received_tag.getAttr('id') - if not id_: - # old XEP implementation - id_ = obj.id_ - ctrl.conv_textview.show_xep0184_ack(id_) - - if obj.mtype == 'error': - if not obj.msgtxt: - obj.msgtxt = _('message') - self.dispatch_error_message(obj.stanza, obj.msgtxt, - obj.session, obj.fjid, obj.timestamp) - return True - elif obj.mtype == 'groupchat': - app.nec.push_incoming_event(GcMessageReceivedEvent(None, - conn=self, msg_obj=obj, stanza_id=obj.unique_id)) - return True - 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: - log.warning('%s announces mam:2 without stanza-id') + log.warning('%s announces mam:2 without stanza-id', room_jid) def _nec_gc_message_received(self, obj): if obj.conn.name != self.name: @@ -743,7 +641,6 @@ class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco, self.continue_connect_info = None app.nec.register_incoming_event(StreamConflictReceivedEvent) - app.nec.register_incoming_event(MessageReceivedEvent) app.nec.register_incoming_event(NotificationEvent) app.ged.register_event_handler('roster-set-received', @@ -939,29 +836,6 @@ class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco, app.config.set_per('accounts', self.name, 'roster_version', obj.version) - def _messageCB(self, con, stanza): - """ - Called when we receive a message - """ - - # Check if a child of the message contains any - # of these namespaces, so we dont execute the - # message handler for them. - # They have defined their own message handlers - # but nbxmpp executes less common handlers last - if self._message_namespaces & set(stanza.getProperties()): - return - - muc_user = stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER) - if muc_user is not None: - if muc_user.getChildren(): - # Not a PM, handled by MUC module - return - log.debug('MessageCB') - - app.nec.push_incoming_event(NetworkEvent('raw-message-received', - conn=self, stanza=stanza, account=self.name)) - def _dispatch_gc_msg_with_captcha(self, stanza, msg_obj): msg_obj.stanza = stanza app.nec.push_incoming_event(GcMessageReceivedEvent(None, @@ -1282,7 +1156,6 @@ class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco, def _register_handlers(self, con, con_type): # try to find another way to register handlers in each class # that defines handlers - con.RegisterHandler('message', self._messageCB) con.RegisterHandler('presence', self._presenceCB) con.RegisterHandler('iq', self._rosterSetCB, 'set', nbxmpp.NS_ROSTER) con.RegisterHandler('iq', self._siSetCB, 'set', nbxmpp.NS_SI) diff --git a/gajim/common/connection_handlers_events.py b/gajim/common/connection_handlers_events.py index 11b4497db..0f3495ce1 100644 --- a/gajim/common/connection_handlers_events.py +++ b/gajim/common/connection_handlers_events.py @@ -632,242 +632,6 @@ class BeforeChangeShowEvent(nec.NetworkIncomingEvent): name = 'before-change-show' base_network_events = [] -class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): - name = 'message-received' - base_network_events = ['raw-message-received'] - - def init(self): - self.additional_data = {} - - def generate(self): - self.conn = self.base_event.conn - self.stanza = self.base_event.stanza - self.encrypted = False - self.self_message = None - self.muc_pm = None - account = self.conn.name - - if self.stanza.getFrom() == self.conn.get_own_jid(warn=True): - # Drop messages sent from our own full jid - # It can happen that when we sent message to our own bare jid - # that the server routes that message back to us - log.info('Received message from self: %s, message is dropped', - self.stanza.getFrom()) - return - - from gajim.common.modules.carbons import parse_carbon - self.stanza, self.sent, self.forwarded = parse_carbon(self.conn, - self.stanza) - - self.get_id() - try: - self.get_jid_resource() - except helpers.InvalidFormat: - log.warning('Invalid JID: %s, ignoring it', - self.stanza.getFrom()) - return - - # Check for duplicates - self.unique_id = self.get_unique_id() - # Check groupchat messages for duplicates, - # We do this because of MUC History messages - type_ = self.stanza.getType() - if type_ == 'groupchat' or self.self_message or self.muc_pm: - if type_ == 'groupchat': - archive_jid = self.stanza.getFrom().getStripped() - else: - archive_jid = self.conn.get_own_jid().getStripped() - if app.logger.find_stanza_id(account, - archive_jid, - self.unique_id, - groupchat=type_ == 'groupchat'): - return - - self.thread_id = self.stanza.getThread() - self.mtype = self.stanza.getType() - if not self.mtype or self.mtype not in ('chat', 'groupchat', 'error'): - self.mtype = 'normal' - - self.msgtxt = self.stanza.getBody() - - self.get_gc_control() - - if self.gc_control and self.jid == self.fjid: - if self.mtype == 'error': - self.msgtxt = _('error while sending %(message)s ( %(error)s )'\ - ) % {'message': self.msgtxt, - 'error': self.stanza.getErrorMsg()} - if self.stanza.getTag('html'): - self.stanza.delChild('html') - # message from a gc without a resource - self.mtype = 'groupchat' - - self.session = None - if self.mtype != 'groupchat': - if app.interface.is_pm_contact(self.fjid, account) and \ - self.mtype == 'error': - self.session = self.conn.find_session(self.fjid, self.thread_id) - if not self.session: - self.session = self.conn.get_latest_session(self.fjid) - if not self.session: - self.session = self.conn.make_new_session(self.fjid, - self.thread_id, - type_='pm') - else: - self.session = self.conn.get_or_create_session(self.fjid, - self.thread_id) - - if self.thread_id and not self.session.received_thread_id: - self.session.received_thread_id = True - - self.session.last_receive = time_time() - - self._generate_timestamp(self.stanza.timestamp) - - return True - - def get_unique_id(self): - ''' - Messages to self: - - Messages to self are stored multiple times in MAM so we cant use - stanza-id to deduplicate. We use origin-id instead. Its not perfect - but there is no better way for now. - We drop "received"-Carbons of Message to self, so we dont have to - parse origin-id in that case. - - MUC PMs: - - MUC PMs are also stored multiple times, we also depend on origin-id - for now. - ''' - - if self.stanza.getType() == 'groupchat': - # TODO: Disco the MUC check if 'urn:xmpp:mam:2' is announced - return self.get_stanza_id(self.stanza) - - elif self.stanza.getType() != 'chat': - return - - # Messages we receive live - if self.conn.get_module('MAM').archiving_namespace != nbxmpp.NS_MAM_2: - # Only mam:2 ensures valid stanza-id - return - - # Sent Carbon - sent_carbon = self.stanza.getTag('sent', - namespace=nbxmpp.NS_CARBONS, - protocol=True) - if sent_carbon is not None: - message = self.get_forwarded_message(sent_carbon) - if self._is_self_message(message) or self._is_muc_pm(message): - return message.getOriginID() - return self.get_stanza_id(message) - - # Received Carbon - received_carbon = self.stanza.getTag('received', - namespace=nbxmpp.NS_CARBONS, - protocol=True) - if received_carbon is not None: - message = self.get_forwarded_message(received_carbon) - if self._is_muc_pm(message): - return message.getOriginID() - return self.get_stanza_id(message) - - # Normal Message - if self._is_self_message(self.stanza) or self._is_muc_pm(self.stanza): - return self.stanza.getOriginID() - return self.get_stanza_id(self.stanza) - -class ZeroconfMessageReceivedEvent(MessageReceivedEvent): - name = 'message-received' - base_network_events = [] - - def get_jid_resource(self): - self.fjid = str(self.stanza.getFrom()) - - if self.fjid is None: - for key in self.conn.connection.zeroconf.contacts: - if self.ip == self.conn.connection.zeroconf.contacts[key][ - Constant.ADDRESS]: - self.fjid = key - break - - self.jid, self.resource = app.get_room_and_nick_from_fjid(self.fjid) - - def generate(self): - self.base_event = nec.NetworkIncomingEvent(None, conn=self.conn, - stanza=self.stanza) - return super(ZeroconfMessageReceivedEvent, self).generate() - - -class DecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): - name = 'decrypted-message-received' - base_network_events = [] - - def generate(self): - self.stanza = self.msg_obj.stanza - self.additional_data = self.msg_obj.additional_data - self.id_ = self.msg_obj.id_ - self.unique_id = self.msg_obj.unique_id - self.jid = self.msg_obj.jid - self.fjid = self.msg_obj.fjid - self.resource = self.msg_obj.resource - self.mtype = self.msg_obj.mtype - self.thread_id = self.msg_obj.thread_id - self.msgtxt = self.msg_obj.msgtxt - self.gc_control = self.msg_obj.gc_control - self.session = self.msg_obj.session - self.timestamp = self.msg_obj.timestamp - self.encrypted = self.msg_obj.encrypted - self.forwarded = self.msg_obj.forwarded - self.sent = self.msg_obj.sent - self.conn = self.msg_obj.conn - self.muc_pm = self.msg_obj.muc_pm - self.popup = False - self.msg_log_id = None # id in log database - self.attention = False # XEP-0224 - self.correct_id = None # XEP-0308 - self.msghash = None - - self.receipt_request_tag = self.stanza.getTag('request', - namespace=nbxmpp.NS_RECEIPTS) - self.receipt_received_tag = self.stanza.getTag('received', - namespace=nbxmpp.NS_RECEIPTS) - - self.subject = self.stanza.getSubject() - - self.displaymarking = None - self.seclabel = self.stanza.getTag('securitylabel', - namespace=nbxmpp.NS_SECLABEL) - if self.seclabel: - self.displaymarking = self.seclabel.getTag('displaymarking') - - if self.stanza.getTag('attention', namespace=nbxmpp.NS_ATTENTION): - delayed = self.stanza.getTag('x', namespace=nbxmpp.NS_DELAY) is not\ - None - if not delayed: - self.attention = True - - self.form_node = self.stanza.getTag('x', namespace=nbxmpp.NS_DATA) - - if app.config.get('ignore_incoming_xhtml'): - self.xhtml = None - else: - self.xhtml = self.stanza.getXHTML() - - # XEP-0172 User Nickname - self.user_nick = self.stanza.getTagData('nick') or '' - - self.get_chatstate() - - self.get_oob_data(self.stanza) - - from gajim.common.modules.misc import parse_correction - self.correct_id = parse_correction(self.stanza) - - return True - class ChatstateReceivedEvent(nec.NetworkIncomingEvent): name = 'chatstate-received' base_network_events = [] diff --git a/gajim/common/modules/__init__.py b/gajim/common/modules/__init__.py index 4514dbbda..7c0997e98 100644 --- a/gajim/common/modules/__init__.py +++ b/gajim/common/modules/__init__.py @@ -18,7 +18,7 @@ from pathlib import Path log = logging.getLogger('gajim.c.m') -ZEROCONF_MODULES = ['adhoc_commands'] +ZEROCONF_MODULES = ['adhoc_commands', 'receipts'] imported_modules = [] _modules = {} diff --git a/gajim/common/modules/chatstates.py b/gajim/common/modules/chatstates.py new file mode 100644 index 000000000..407d05a56 --- /dev/null +++ b/gajim/common/modules/chatstates.py @@ -0,0 +1,33 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see . + +# XEP-0085: Chat State Notifications + +import logging + +import nbxmpp + +from gajim.common.modules.misc import parse_delay + +log = logging.getLogger('gajim.c.m.chatstates') + + +def parse_chatstate(stanza): + if parse_delay(stanza) is not None: + return + + children = stanza.getChildren() + for child in children: + if child.getNamespace() == nbxmpp.NS_CHATSTATES: + return child.getName() diff --git a/gajim/common/modules/mam.py b/gajim/common/modules/mam.py index ecab13c64..a3c32656a 100644 --- a/gajim/common/modules/mam.py +++ b/gajim/common/modules/mam.py @@ -27,6 +27,8 @@ 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 +from gajim.common.modules.util import is_self_message +from gajim.common.modules.util import is_muc_pm log = logging.getLogger('gajim.c.m.archiving') @@ -61,28 +63,6 @@ class MAM: 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: @@ -150,8 +130,8 @@ class MAM: 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_']) + self_message = is_self_message(message, groupchat) + muc_pm = is_muc_pm(message, event_attrs['with_'], groupchat) stanza_id, origin_id = self._get_unique_id( result, message, groupchat, self_message, muc_pm) diff --git a/gajim/common/modules/message.py b/gajim/common/modules/message.py new file mode 100644 index 000000000..25d363359 --- /dev/null +++ b/gajim/common/modules/message.py @@ -0,0 +1,297 @@ +# 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 . + +# Message handler + +import time +import logging + +import nbxmpp + +from gajim.common import app +from gajim.common import helpers +from gajim.common.nec import NetworkIncomingEvent, NetworkEvent +from gajim.common.modules.user_nickname import parse_nickname +from gajim.common.modules.chatstates import parse_chatstate +from gajim.common.modules.carbons import parse_carbon +from gajim.common.modules.misc import parse_delay +from gajim.common.modules.misc import parse_eme +from gajim.common.modules.misc import parse_correction +from gajim.common.modules.misc import parse_attention +from gajim.common.modules.misc import parse_securitylabel +from gajim.common.modules.misc import parse_form +from gajim.common.modules.misc import parse_oob +from gajim.common.modules.misc import parse_xhtml +from gajim.common.modules.util import is_self_message +from gajim.common.modules.util import is_muc_pm +from gajim.common.connection_handlers_events import GcMessageReceivedEvent + + +log = logging.getLogger('gajim.c.m.message') + + +class Message: + def __init__(self, con): + self._con = con + self._account = con.name + + self.handlers = [('message', self._message_received)] + + # XEPs for which this message module should not be executed + self._message_namespaces = set([nbxmpp.NS_HTTP_AUTH, + nbxmpp.NS_PUBSUB_EVENT, + nbxmpp.NS_ROSTERX, + nbxmpp.NS_MAM_1, + nbxmpp.NS_MAM_2, + nbxmpp.NS_CONFERENCE, + nbxmpp.NS_IBB]) + + def _message_received(self, con, stanza): + # Check if a child of the message contains any + # namespaces that we handle in other modules. + # nbxmpp executes less common handlers last + if self._message_namespaces & set(stanza.getProperties()): + return + + muc_user = stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER) + if muc_user is not None: + if muc_user.getChildren(): + # Not a PM, handled by MUC module + return + + log.info('Received from %s', stanza.getFrom()) + + app.nec.push_incoming_event(NetworkEvent( + 'raw-message-received', + conn=self._con, + stanza=stanza, + account=self._account)) + + if stanza.getFrom() == self._con.get_own_jid(warn=True): + # Drop messages sent from our own full jid + # It can happen that when we sent message to our own bare jid + # that the server routes that message back to us + log.info('Received message from self: %s, message is dropped', + stanza.getFrom()) + return + + stanza, sent, forwarded = parse_carbon(self._con, stanza) + + from_ = stanza.getFrom() + type_ = stanza.getType() + if type_ is None: + type_ = 'normal' + self_message = is_self_message(stanza, type_ == 'groupchat') + muc_pm = is_muc_pm(stanza, from_, type_ == 'groupchat') + + id_ = stanza.getID() + + fjid = None + if from_ is not None: + try: + fjid = helpers.parse_jid(str(from_)) + except helpers.InvalidFormat: + log.warning('Invalid JID: %s, ignoring it', + stanza.getFrom()) + return + + jid, resource = app.get_room_and_nick_from_fjid(fjid) + + # Check for duplicates + unique_id = self._get_unique_id(stanza, forwarded, sent, + self_message, muc_pm) + + # Check groupchat messages for duplicates, + # We do this because of MUC History messages + if type_ == 'groupchat' or self_message or muc_pm: + if type_ == 'groupchat': + archive_jid = stanza.getFrom().getStripped() + else: + archive_jid = self._con.get_own_jid().getStripped() + if app.logger.find_stanza_id(self._account, + archive_jid, + unique_id, + groupchat=type_ == 'groupchat'): + return + + thread_id = stanza.getThread() + msgtxt = stanza.getBody() + + # TODO: remove all control UI stuff + gc_control = app.interface.msg_win_mgr.get_gc_control( + jid, self._account) + if not gc_control: + minimized = app.interface.minimized_controls[self._account] + gc_control = minimized.get(jid) + + if gc_control and jid == fjid: + if type_ == 'error': + msgtxt = _('error while sending %(message)s ( %(error)s )') % { + 'message': msgtxt, 'error': stanza.getErrorMsg()} + # TODO: why is this here? + if stanza.getTag('html'): + stanza.delChild('html') + + session = None + if type_ != 'groupchat': + if muc_pm and type_ == 'error': + session = self._con.find_session(fjid, thread_id) + if not session: + session = self._con.get_latest_session(fjid) + if not session: + session = self._con.make_new_session( + fjid, thread_id, type_='pm') + else: + session = self._con.get_or_create_session(fjid, thread_id) + + if thread_id and not session.received_thread_id: + session.received_thread_id = True + + session.last_receive = time.time() + + timestamp = parse_delay(stanza) + if timestamp is None: + timestamp = time.time() + + event_attr = { + 'conn': self._con, + 'stanza': stanza, + 'account': self._account, + 'id_': id_, + 'encrypted': False, + 'additional_data': {}, + 'forwarded': forwarded, + 'sent': sent, + 'timestamp': timestamp, + 'fjid': fjid, + 'jid': jid, + 'resource': resource, + 'unique_id': unique_id, + 'mtype': type_, + 'msgtxt': msgtxt, + 'thread_id': thread_id, + 'session': session, + 'self_message': self_message, + 'muc_pm': muc_pm, + 'gc_control': gc_control + } + + event = MessageReceivedEvent(None, **event_attr) + app.nec.push_incoming_event(event) + + app.plugin_manager.extension_point( + 'decrypt', self._con, event, self._on_message_decrypted) + if not event.encrypted: + eme = parse_eme(event.stanza) + if eme is not None: + event.msgtxt = eme + self._on_message_decrypted(event) + + def _on_message_decrypted(self, event): + try: + self._con.get_module('Receipts').delegate(event) + except nbxmpp.NodeProcessed: + return + + event_attr = { + 'popup': False, + 'msg_log_id': None, + 'subject': event.stanza.getSubject(), + 'displaymarking': parse_securitylabel(event.stanza), + 'attention': parse_attention(event.stanza), + 'correct_id': parse_correction(event.stanza), + 'user_nick': parse_nickname(event.stanza), + 'form_node': parse_form(event.stanza), + 'xhtml': parse_xhtml(event.stanza), + 'chatstate': parse_chatstate(event.stanza), + 'stanza_id': event.unique_id + } + parse_oob(event.stanza, event.additional_data) + + for name, value in event_attr.items(): + setattr(event, name, value) + + if event.mtype == 'error': + if not event.msgtxt: + event.msgtxt = _('message') + self._con.dispatch_error_message( + event.stanza, event.msgtxt, + event.session, event.fjid, event.timestamp) + return + + if event.mtype == 'groupchat': + app.nec.push_incoming_event(GcMessageReceivedEvent( + None, + conn=self._con, + msg_obj=event, + stanza_id=event.unique_id)) + return + + app.nec.push_incoming_event( + DecryptedMessageReceivedEvent( + None, **vars(event))) + + def _get_unique_id(self, stanza, forwarded, sent, self_message, muc_pm): + if stanza.getType() == 'groupchat': + # TODO: Disco the MUC check if 'urn:xmpp:mam:2' is announced + return self._get_stanza_id(stanza) + + if stanza.getType() != 'chat': + return + + # Messages we receive live + if self._con.get_module('MAM').archiving_namespace != nbxmpp.NS_MAM_2: + # Only mam:2 ensures valid stanza-id + return + + if forwarded: + if sent: + if self_message or muc_pm: + return stanza.getOriginID() + return self._get_stanza_id(stanza) + else: + if muc_pm: + return stanza.getOriginID() + return self._get_stanza_id(stanza) + + # Normal Message + if self_message or muc_pm: + return stanza.getOriginID() + return self._get_stanza_id(stanza) + + def _get_stanza_id(self, stanza): + stanza_id, by = stanza.getStanzaIDAttrs() + if by is None: + # We can not verify who set this stanza-id, ignore it. + return + if stanza.getType() == 'groupchat': + if stanza.getFrom().bareMatch(by): + # by attribute must match the server + return stanza_id + elif self._con.get_own_jid().bareMatch(by): + # by attribute must match the server + return stanza_id + return + + +class MessageReceivedEvent(NetworkIncomingEvent): + name = 'message-received' + + +class DecryptedMessageReceivedEvent(NetworkIncomingEvent): + name = 'decrypted-message-received' + + +def get_instance(*args, **kwargs): + return Message(*args, **kwargs), 'Message' diff --git a/gajim/common/modules/misc.py b/gajim/common/modules/misc.py index a2afa2bd1..d97b579d9 100644 --- a/gajim/common/modules/misc.py +++ b/gajim/common/modules/misc.py @@ -18,6 +18,7 @@ import logging import nbxmpp +from gajim.common import app from gajim.common.modules.date_and_time import parse_datetime log = logging.getLogger('gajim.c.m.misc') @@ -111,3 +112,38 @@ def parse_correction(stanza): if id_ is not None: return id_ log.warning('No id attr found: %s' % stanza) + + +# XEP-0224: Attention + +def parse_attention(stanza): + attention = stanza.getTag('attention', namespace=nbxmpp.NS_ATTENTION) + if attention is None: + return False + delayed = stanza.getTag('x', namespace=nbxmpp.NS_DELAY2) + if delayed is not None: + return False + return True + + +# XEP-0258: Security Labels in XMPP + +def parse_securitylabel(stanza): + seclabel = stanza.getTag('securitylabel', namespace=nbxmpp.NS_SECLABEL) + if seclabel is None: + return None + return seclabel.getTag('displaymarking') + + +# XEP-0004: Data Forms + +def parse_form(stanza): + return stanza.getTag('x', namespace=nbxmpp.NS_DATA) + + +# XEP-0071: XHTML-IM + +def parse_xhtml(stanza): + if app.config.get('ignore_incoming_xhtml'): + return None + return stanza.getXHTML() diff --git a/gajim/common/modules/receipts.py b/gajim/common/modules/receipts.py new file mode 100644 index 000000000..26a0fada8 --- /dev/null +++ b/gajim/common/modules/receipts.py @@ -0,0 +1,105 @@ +# This file is part of Gajim. +# +# Gajim is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published +# by the Free Software Foundation; version 3 only. +# +# Gajim is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Gajim. If not, see . + +# XEP-0184: Message Delivery Receipts + +import logging + +import nbxmpp + +from gajim.common import app +from gajim.common.nec import NetworkIncomingEvent + +log = logging.getLogger('gajim.c.m.receipts') + + +class Receipts: + def __init__(self, con): + self._con = con + self._account = con.name + + self.handlers = [] + + def delegate(self, event): + request = event.stanza.getTag('request', + namespace=nbxmpp.NS_RECEIPTS) + if request is not None: + self._answer_request(event) + return + + received = event.stanza.getTag('received', + namespace=nbxmpp.NS_RECEIPTS) + if received is not None: + self._receipt_received(event, received) + raise nbxmpp.NodeProcessed + + def _answer_request(self, event): + if not app.config.get_per('accounts', self._account, + 'answer_receipts'): + return + + if event.mtype not in ('chat', 'groupchat'): + return + + if event.sent: + # Never answer messages that we sent from another device + return + + from_ = event.stanza.getFrom() + if self._con.get_own_jid().bareMatch(from_): + # Dont answer receipts from our other resources + return + + receipt_id = event.stanza.getID() + + contact = self._get_contact(event) + if contact is None: + return + + receipt = self._build_answer_receipt(from_, receipt_id) + log.info('Answer %s', receipt_id) + self._con.connection.send(receipt) + + def _get_contact(self, event): + if event.mtype == 'chat': + contact = app.contacts.get_contact(self._account, event.jid) + if contact and contact.sub not in ('to', 'none'): + return contact + else: + return app.contacts.get_gc_contact(self._account, + event.jid, + event.resource) + + def _build_answer_receipt(self, to, receipt_id): + receipt = nbxmpp.Message(to=to, typ='chat') + receipt.setTag('received', + namespace='urn:xmpp:receipts', + attrs={'id': receipt_id}) + return receipt + + def _receipt_received(self, event, received): + receipt_id = received.getAttr('id') + if receipt_id is None: + log.warning('Receipt without ID: %s', event.stanza) + return + log.info('Received %s', receipt_id) + app.nec.push_incoming_event( + NetworkIncomingEvent('receipt-received', + conn=self._con, + receipt_id=receipt_id, + jid=event.jid)) + + +def get_instance(*args, **kwargs): + return Receipts(*args, **kwargs), 'Receipts' diff --git a/gajim/common/modules/user_nickname.py b/gajim/common/modules/user_nickname.py index 9471dffee..09a0c785d 100644 --- a/gajim/common/modules/user_nickname.py +++ b/gajim/common/modules/user_nickname.py @@ -78,5 +78,12 @@ class UserNickname(AbstractPEPModule): 'accounts', self._account, 'name') +def parse_nickname(stanza): + nick = stanza.getTag('nick', namespace=nbxmpp.NS_NICK) + if nick is None: + return '' + return nick.getData() + + def get_instance(*args, **kwargs): return UserNickname(*args, **kwargs), 'UserNickname' diff --git a/gajim/common/modules/util.py b/gajim/common/modules/util.py new file mode 100644 index 000000000..c8c36d4ae --- /dev/null +++ b/gajim/common/modules/util.py @@ -0,0 +1,41 @@ +# 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 . + +# Util module + +import nbxmpp + +from gajim.common import app + + +def is_self_message(message, groupchat=False): + if groupchat: + return False + frm = message.getFrom() + to = message.getTo() + return frm.bareMatch(to) + + +def is_muc_pm(message, jid, groupchat=False): + 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 + if app.logger.jid_is_room_jid(jid.getStripped()): + return True + return False diff --git a/gajim/common/zeroconf/connection_handlers_zeroconf.py b/gajim/common/zeroconf/connection_handlers_zeroconf.py index 6f4202a1a..1258d0218 100644 --- a/gajim/common/zeroconf/connection_handlers_zeroconf.py +++ b/gajim/common/zeroconf/connection_handlers_zeroconf.py @@ -23,21 +23,40 @@ ## along with Gajim. If not, see . ## +import time + import nbxmpp from gajim.common import app + from gajim.common.protocol.bytestream import ConnectionSocks5BytestreamZeroconf -from gajim.common.connection_handlers_events import ZeroconfMessageReceivedEvent +from gajim.common.zeroconf.zeroconf import Constant +from gajim.common import connection_handlers +from gajim.common.nec import NetworkIncomingEvent, NetworkEvent +from gajim.common.modules.user_nickname import parse_nickname +from gajim.common.modules.chatstates import parse_chatstate +from gajim.common.modules.misc import parse_eme +from gajim.common.modules.misc import parse_correction +from gajim.common.modules.misc import parse_attention +from gajim.common.modules.misc import parse_oob +from gajim.common.modules.misc import parse_xhtml import logging log = logging.getLogger('gajim.c.z.connection_handlers_zeroconf') STATUS_LIST = ['offline', 'connecting', 'online', 'chat', 'away', 'xa', 'dnd', - 'invisible'] + 'invisible'] # kind of events we can wait for an answer AGENT_REMOVED = 'agent_removed' -from gajim.common import connection_handlers + +class ZeroconfMessageReceivedEvent(NetworkIncomingEvent): + name = 'message-received' + + +class DecryptedMessageReceivedEvent(NetworkIncomingEvent): + name = 'decrypted-message-received' + class ConnectionVcard: def add_sha(self, p, *args): @@ -57,14 +76,113 @@ connection_handlers.ConnectionJingle): connection_handlers.ConnectionJingle.__init__(self) connection_handlers.ConnectionHandlersBase.__init__(self) - def _messageCB(self, ip, con, msg): + def _messageCB(self, ip, con, stanza): """ Called when we receive a message """ log.debug('Zeroconf MessageCB') - app.nec.push_incoming_event(ZeroconfMessageReceivedEvent(None, - conn=self, stanza=msg, ip=ip)) - return + + app.nec.push_incoming_event(NetworkEvent( + 'raw-message-received', + conn=self, + stanza=stanza, + account=self.name)) + + type_ = stanza.getType() + if type_ is None: + type_ = 'normal' + + id_ = stanza.getID() + + fjid = str(stanza.getFrom()) + + if fjid is None: + for key in self.connection.zeroconf.contacts: + if ip == self.connection.zeroconf.contacts[key][ + Constant.ADDRESS]: + fjid = key + break + + jid, resource = app.get_room_and_nick_from_fjid(fjid) + + thread_id = stanza.getThread() + msgtxt = stanza.getBody() + + session = self.get_or_create_session(fjid, thread_id) + + if thread_id and not session.received_thread_id: + session.received_thread_id = True + + session.last_receive = time.time() + + event_attr = { + 'conn': self, + 'stanza': stanza, + 'account': self.name, + 'id_': id_, + 'encrypted': False, + 'additional_data': {}, + 'forwarded': False, + 'sent': False, + 'timestamp': time.time(), + 'fjid': fjid, + 'jid': jid, + 'resource': resource, + 'unique_id': id_, + 'mtype': type_, + 'msgtxt': msgtxt, + 'thread_id': thread_id, + 'session': session, + 'self_message': False, + 'muc_pm': False, + 'gc_control': None} + + event = ZeroconfMessageReceivedEvent(None, **event_attr) + app.nec.push_incoming_event(event) + + app.plugin_manager.extension_point( + 'decrypt', self, event, self._on_message_decrypted) + if not event.encrypted: + eme = parse_eme(event.stanza) + if eme is not None: + event.msgtxt = eme + self._on_message_decrypted(event) + + def _on_message_decrypted(self, event): + try: + self.get_module('Receipts').delegate(event) + except nbxmpp.NodeProcessed: + return + + event_attr = { + 'popup': False, + 'msg_log_id': None, + 'subject': None, + 'displaymarking': None, + 'form_node': None, + 'attention': parse_attention(event.stanza), + 'correct_id': parse_correction(event.stanza), + 'user_nick': parse_nickname(event.stanza), + 'xhtml': parse_xhtml(event.stanza), + 'chatstate': parse_chatstate(event.stanza), + 'stanza_id': event.unique_id + } + + parse_oob(event.stanza, event.additional_data) + + for name, value in event_attr.items(): + setattr(event, name, value) + + if event.mtype == 'error': + if not event.msgtxt: + event.msgtxt = _('message') + self.dispatch_error_message( + event.stanza, event.msgtxt, + event.session, event.fjid, event.timestamp) + return + + app.nec.push_incoming_event( + DecryptedMessageReceivedEvent(None, **vars(event))) def store_metacontacts(self, tags): """ diff --git a/test/unit/test_sessions.py b/test/unit/test_sessions.py index 42cda1c4e..5d97d917f 100644 --- a/test/unit/test_sessions.py +++ b/test/unit/test_sessions.py @@ -12,7 +12,7 @@ from gajim.common import app from gajim.common import nec from gajim.common import ged from gajim.common.nec import NetworkEvent -from gajim.common.connection_handlers_events import MessageReceivedEvent +from gajim.common.modules.message import MessageReceivedEvent from gajim.common.connection_handlers_events import DecryptedMessageReceivedEvent import nbxmpp