# 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 nbxmpp from nbxmpp.namespaces import Namespace from nbxmpp.structs import StanzaHandler from nbxmpp.util import generate_id from gajim.common import app from gajim.common.nec import NetworkEvent from gajim.common.helpers import AdditionalDataDict from gajim.common.helpers import should_log from gajim.common.const import KindConstant from gajim.common.modules.base import BaseModule from gajim.common.modules.util import get_eme_message from gajim.common.modules.misc import parse_correction from gajim.common.modules.misc import parse_oob from gajim.common.modules.misc import parse_xhtml class Message(BaseModule): def __init__(self, con): BaseModule.__init__(self, con) self.handlers = [ StanzaHandler(name='message', callback=self._check_if_unknown_contact, priority=41), StanzaHandler(name='message', callback=self._message_received, priority=50), StanzaHandler(name='message', typ='error', callback=self._message_error_received, priority=50), ] # XEPs for which this message module should not be executed self._message_namespaces = set([Namespace.ROSTERX, Namespace.IBB]) def _check_if_unknown_contact(self, _con, stanza, properties): if (properties.type.is_groupchat or properties.is_muc_pm or properties.is_self_message or properties.is_mam_message): return if self._con.get_own_jid().domain == str(properties.jid): # Server message return if not app.settings.get_account_setting(self._account, 'ignore_unknown_contacts'): return jid = properties.jid.bare if self._con.get_module('Roster').get_item(jid) is None: self._log.warning('Ignore message from unknown contact: %s', jid) self._log.warning(stanza) raise nbxmpp.NodeProcessed def _message_received(self, _con, stanza, properties): if (properties.is_mam_message or properties.is_pubsub or properties.type.is_error): return # 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 self._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 properties.is_carbon_message and properties.carbon.is_sent: # Ugly, we treat the from attr as the remote jid, # to make that work with sent carbons we have to do this. # TODO: Check where in Gajim and plugins we depend on that behavior stanza.setFrom(stanza.getTo()) from_ = stanza.getFrom() fjid = str(from_) jid = from_.bare resource = from_.resource type_ = properties.type stanza_id, message_id = self._get_unique_id(properties) if properties.type.is_groupchat and properties.has_server_delay: # Only for XEP-0045 MUC History # Don’t check for message text because the message could be # encrypted. if app.storage.archive.deduplicate_muc_message( self._account, properties.jid.bare, properties.jid.resource, properties.timestamp, properties.id): raise nbxmpp.NodeProcessed if (properties.is_self_message or properties.is_muc_pm): archive_jid = self._con.get_own_jid().bare if app.storage.archive.find_stanza_id( self._account, archive_jid, stanza_id, message_id, properties.type.is_groupchat): return msgtxt = properties.body # 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) session = None if not properties.type.is_groupchat: if properties.is_muc_pm and properties.type.is_error: session = self._con.find_session(fjid, properties.thread) if not session: session = self._con.get_latest_session(fjid) if not session: session = self._con.make_new_session( fjid, properties.thread, type_='pm') else: session = self._con.get_or_create_session( fjid, properties.thread) if properties.thread and not session.received_thread_id: session.received_thread_id = True session.last_receive = time.time() additional_data = AdditionalDataDict() if properties.has_user_delay: additional_data.set_value( 'gajim', 'user_timestamp', properties.user_timestamp) parse_oob(properties, additional_data) parse_xhtml(properties, additional_data) app.nec.push_incoming_event(NetworkEvent('update-client-info', account=self._account, jid=jid, resource=resource)) if properties.is_encrypted: additional_data['encrypted'] = properties.encrypted.additional_data else: if properties.eme is not None: msgtxt = get_eme_message(properties.eme) displaymarking = None if properties.has_security_label: displaymarking = properties.security_label.displaymarking event_attr = { 'conn': self._con, 'stanza': stanza, 'account': self._account, 'additional_data': additional_data, 'fjid': fjid, 'jid': jid, 'resource': resource, 'stanza_id': stanza_id, 'unique_id': stanza_id or message_id, 'correct_id': parse_correction(properties), 'msgtxt': msgtxt, 'session': session, 'delayed': properties.user_timestamp is not None, 'gc_control': gc_control, 'popup': False, 'msg_log_id': None, 'displaymarking': displaymarking, 'properties': properties, } if type_.is_groupchat: if not msgtxt: return event_attr.update({ 'room_jid': jid, }) event = NetworkEvent('gc-message-received', **event_attr) app.nec.push_incoming_event(event) # TODO: Some plugins modify msgtxt in the GUI event self._log_muc_message(event) return app.nec.push_incoming_event( NetworkEvent('decrypted-message-received', **event_attr)) def _message_error_received(self, _con, _stanza, properties): jid = properties.jid if not properties.is_muc_pm: jid = jid.new_as_bare() self._log.info(properties.error) app.storage.archive.set_message_error( app.get_jid_from_account(self._account), jid, properties.id, properties.error) app.nec.push_incoming_event( NetworkEvent('message-error', account=self._account, jid=jid, room_jid=jid, message_id=properties.id, error=properties.error)) def _log_muc_message(self, event): self._check_for_mam_compliance(event.room_jid, event.stanza_id) if (should_log(self._account, event.jid) and event.msgtxt and event.properties.muc_nickname): # if not event.nick, it means message comes from room itself # usually it hold description and can be send at each connection # so don't store it in logs app.storage.archive.insert_into_logs( self._account, event.jid, event.properties.timestamp, KindConstant.GC_MSG, message=event.msgtxt, contact_name=event.properties.muc_nickname, additional_data=event.additional_data, stanza_id=event.stanza_id, message_id=event.properties.id) def _check_for_mam_compliance(self, room_jid, stanza_id): disco_info = app.storage.cache.get_last_disco_info(room_jid) if stanza_id is None and disco_info.mam_namespace == Namespace.MAM_2: self._log.warning('%s announces mam:2 without stanza-id', room_jid) def _get_unique_id(self, properties): if properties.is_self_message: # Deduplicate self message with message-id return None, properties.id if properties.stanza_id is None: return None, None if properties.type.is_groupchat: disco_info = app.storage.cache.get_last_disco_info( properties.jid.bare) if disco_info.mam_namespace != Namespace.MAM_2: return None, None archive = properties.jid else: if not self._con.get_module('MAM').available: return None, None archive = self._con.get_own_jid() if archive.bare_match(properties.stanza_id.by): return properties.stanza_id.id, None # stanza-id not added by the archive, ignore it. return None, None def build_message_stanza(self, message): own_jid = self._con.get_own_jid() stanza = nbxmpp.Message(to=message.jid, body=message.message, typ=message.type_, subject=message.subject, xhtml=message.xhtml) if message.correct_id: stanza.setTag('replace', attrs={'id': message.correct_id}, namespace=Namespace.CORRECT) # XEP-0359 message.message_id = generate_id() stanza.setID(message.message_id) stanza.setOriginID(message.message_id) if message.label: stanza.addChild(node=message.label.to_node()) # XEP-0172: user_nickname if message.user_nick: stanza.setTag('nick', namespace=Namespace.NICK).setData( message.user_nick) # XEP-0203 # TODO: Seems delayed is not set anywhere if message.delayed: timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime(message.delayed)) stanza.addChild('delay', namespace=Namespace.DELAY2, attrs={'from': str(own_jid), 'stamp': timestamp}) # XEP-0224 if message.attention: stanza.setTag('attention', namespace=Namespace.ATTENTION) # XEP-0066 if message.oob_url is not None: oob = stanza.addChild('x', namespace=Namespace.X_OOB) oob.addChild('url').setData(message.oob_url) # XEP-0184 if not own_jid.bare_match(message.jid): if message.message and not message.is_groupchat: stanza.setReceiptRequest() # Mark Message as MUC PM if message.contact.is_pm_contact: stanza.setTag('x', namespace=Namespace.MUC_USER) # XEP-0085 if message.chatstate is not None: stanza.setTag(message.chatstate, namespace=Namespace.CHATSTATES) if not message.message: stanza.setTag('no-store', namespace=Namespace.MSG_HINTS) # XEP-0333 if message.message: stanza.setMarkable() if message.marker: marker, id_ = message.marker stanza.setMarker(marker, id_) # Add other nodes if message.nodes is not None: for node in message.nodes: stanza.addChild(node=node) return stanza def log_message(self, message): if not message.is_loggable: return if not should_log(self._account, message.jid): return if message.message is None: return app.storage.archive.insert_into_logs( self._account, message.jid, message.timestamp, message.kind, message=message.message, subject=message.subject, additional_data=message.additional_data, message_id=message.message_id, stanza_id=message.message_id) def get_instance(*args, **kwargs): return Message(*args, **kwargs), 'Message'