Refactor message handlers

- Adapt to nbxmpp now unwraping MAM messages
- Use nbxmpp properties
- Save message-id to database
This commit is contained in:
Philipp Hörist 2019-01-04 00:03:35 +01:00
parent cdb37828e4
commit b600328639
5 changed files with 125 additions and 219 deletions

View File

@ -72,6 +72,7 @@ LOGS_SQL_STATEMENT = '''
subject TEXT, subject TEXT,
additional_data TEXT, additional_data TEXT,
stanza_id TEXT, stanza_id TEXT,
message_id TEXT,
encryption TEXT, encryption TEXT,
encryption_state TEXT, encryption_state TEXT,
marker INTEGER marker INTEGER
@ -228,6 +229,13 @@ class Logger:
] ]
self._execute_multiple(con, statements) self._execute_multiple(con, statements)
if self._get_user_version(con) < 3:
statements = [
'ALTER TABLE logs ADD COLUMN "message_id" TEXT',
'PRAGMA user_version=3'
]
self._execute_multiple(con, statements)
def _migrate_cache(self, con): def _migrate_cache(self, con):
if self._get_user_version(con) == 0: if self._get_user_version(con) == 0:
# All migrations from 0.16.9 until 1.0.0 # All migrations from 0.16.9 until 1.0.0

View File

@ -19,6 +19,7 @@ import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
import nbxmpp import nbxmpp
from nbxmpp.structs import StanzaHandler
from gajim.common import app from gajim.common import app
from gajim.common.nec import NetworkEvent from gajim.common.nec import NetworkEvent
@ -33,8 +34,6 @@ from gajim.common.modules.misc import parse_delay
from gajim.common.modules.misc import parse_oob from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_correction from gajim.common.modules.misc import parse_correction
from gajim.common.modules.misc import parse_eme 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') log = logging.getLogger('gajim.c.m.archiving')
@ -45,8 +44,9 @@ class MAM:
self._account = con.name self._account = con.name
self.handlers = [ self.handlers = [
('message', self._mam_message_received, '', nbxmpp.NS_MAM_1), StanzaHandler(name='message',
('message', self._mam_message_received, '', nbxmpp.NS_MAM_2), callback=self._mam_message_received,
priority=51),
] ]
self.available = False self.available = False
@ -72,132 +72,91 @@ class MAM:
account=self._account, account=self._account,
feature=self.archiving_namespace)) feature=self.archiving_namespace))
def _from_valid_archive(self, stanza, message, groupchat): def _from_valid_archive(self, stanza, properties):
if groupchat: if properties.type.is_groupchat:
expected_archive = message.getFrom() expected_archive = properties.jid
else: else:
expected_archive = self._con.get_own_jid() expected_archive = self._con.get_own_jid()
archive_jid = stanza.getFrom() return properties.mam.archive.bareMatch(expected_archive)
if archive_jid is None:
if groupchat:
return
# Message from our own archive
return self._con.get_own_jid()
if archive_jid.bareMatch(expected_archive): def _get_unique_id(self, properties):
return archive_jid if properties.type.is_groupchat:
return properties.mam.id, None
def _get_unique_id(self, result, message, groupchat, self_message, muc_pm): if properties.is_self_message:
stanza_id = result.getAttr('id') return None, properties.id
if groupchat:
return stanza_id, None
origin_id = message.getOriginID() if properties.is_muc_pm:
if self_message: return properties.mam.id, properties.id
return None, origin_id
if muc_pm: if self._con.get_own_jid().bareMatch(properties.jid):
return stanza_id, origin_id
if self._con.get_own_jid().bareMatch(message.getFrom()):
# message we sent # message we sent
return stanza_id, origin_id return properties.mam.id, properties.id
# A message we received # A message we received
return stanza_id, None return properties.mam.id, None
def _mam_message_received(self, _con, stanza, properties):
if not properties.is_mam_message:
return
def _mam_message_received(self, _con, stanza):
app.nec.push_incoming_event( app.nec.push_incoming_event(
NetworkIncomingEvent('raw-mam-message-received', NetworkIncomingEvent('raw-mam-message-received',
conn=self._con, conn=self._con,
stanza=stanza)) stanza=stanza))
result = stanza.getTag('result', protocol=True) if not self._from_valid_archive(stanza, properties):
queryid = result.getAttr('queryid') log.warning('Message from invalid archive %s',
forwarded = result.getTag('forwarded', properties.mam.archive)
namespace=nbxmpp.NS_FORWARD,
protocol=True)
message = forwarded.getTag('message', protocol=True)
groupchat = message.getType() == 'groupchat'
archive_jid = self._from_valid_archive(stanza, message, groupchat)
if archive_jid is None:
log.warning('Message from invalid archive %s', stanza)
raise nbxmpp.NodeProcessed raise nbxmpp.NodeProcessed
log.info('Received message from archive: %s', archive_jid) log.info('Received message from archive: %s', properties.mam.archive)
if not self._is_valid_request(archive_jid, queryid): if not self._is_valid_request(properties):
log.warning('Invalid MAM Message: unknown query id') log.warning('Invalid MAM Message: unknown query id %s',
properties.mam.query_id)
log.debug(stanza) log.debug(stanza)
raise nbxmpp.NodeProcessed raise nbxmpp.NodeProcessed
# Timestamp parsing
# Most servers dont set the 'from' attr, so we cant check for it
delay_timestamp = parse_delay(forwarded)
if delay_timestamp is None:
log.warning('No timestamp on MAM message')
log.warning(stanza)
raise nbxmpp.NodeProcessed
# Fix for self messaging
if not groupchat:
to = message.getTo()
if to is None:
# Some servers dont set the 'to' attribute when
# we send a message to ourself
message.setTo(self._con.get_own_jid())
event_attrs = {} event_attrs = {}
groupchat = properties.type.is_groupchat
if groupchat: if groupchat:
event_attrs.update(self._parse_gc_attrs(message)) event_attrs.update(self._parse_gc_attrs(properties))
else: else:
event_attrs.update(self._parse_chat_attrs(message)) event_attrs.update(self._parse_chat_attrs(stanza, properties))
self_message = is_self_message(message, groupchat) stanza_id, message_id = self._get_unique_id(properties)
muc_pm = is_muc_pm(message, event_attrs['with_'], groupchat)
stanza_id, origin_id = self._get_unique_id( if properties.mam.is_ver_2:
result, message, groupchat, self_message, muc_pm)
message_id = message.getID()
# Check for duplicates
namespace = self.archiving_namespace
if groupchat:
namespace = muc_caps_cache.get_mam_namespace(
archive_jid.getStripped())
if namespace == nbxmpp.NS_MAM_2:
# Search only with stanza-id for duplicates on mam:2 # Search only with stanza-id for duplicates on mam:2
if app.logger.find_stanza_id(self._account, if app.logger.find_stanza_id(self._account,
archive_jid.getStripped(), str(properties.mam.archive),
stanza_id, stanza_id,
origin_id, message_id,
groupchat=groupchat): groupchat=groupchat):
log.info('Found duplicate with stanza-id') log.info('Found duplicate with stanza-id: %s, message-id: %s',
stanza_id, message_id)
raise nbxmpp.NodeProcessed raise nbxmpp.NodeProcessed
msgtxt = message.getTagData('body')
event_attrs.update( event_attrs.update(
{'conn': self._con, {'conn': self._con,
'additional_data': AdditionalDataDict(), 'additional_data': AdditionalDataDict(),
'encrypted': False, 'encrypted': False,
'timestamp': delay_timestamp, 'timestamp': properties.mam.timestamp,
'self_message': self_message, 'self_message': properties.is_self_message,
'groupchat': groupchat, 'groupchat': groupchat,
'muc_pm': muc_pm, 'muc_pm': properties.is_muc_pm,
'stanza_id': stanza_id, 'stanza_id': stanza_id,
'origin_id': origin_id, 'origin_id': message_id,
'message_id': message_id, 'message_id': properties.id,
'correct_id': None, 'correct_id': None,
'archive_jid': archive_jid, 'archive_jid': properties.mam.archive,
'msgtxt': msgtxt, 'msgtxt': properties.body,
'message': message, 'message': stanza,
'stanza': message, 'stanza': stanza,
'namespace': namespace, 'namespace': properties.mam.namespace,
}) })
if groupchat: if groupchat:
@ -217,26 +176,18 @@ class MAM:
raise nbxmpp.NodeProcessed raise nbxmpp.NodeProcessed
@staticmethod @staticmethod
def _parse_gc_attrs(message): def _parse_gc_attrs(properties):
with_ = message.getFrom()
nick = message.getFrom().getResource()
# Get the real jid if we have it
real_jid = None real_jid = None
muc_user = message.getTag('x', namespace=nbxmpp.NS_MUC_USER) if properties.muc_user is not None:
if muc_user is not None: real_jid = properties.muc_user.jid
real_jid = muc_user.getTagAttr('item', 'jid') return {'with_': properties.jid,
if real_jid is not None: 'nick': properties.muc_nickname,
real_jid = nbxmpp.JID(real_jid)
return {'with_': with_,
'nick': nick,
'real_jid': real_jid, 'real_jid': real_jid,
'kind': KindConstant.GC_MSG} 'kind': KindConstant.GC_MSG}
def _parse_chat_attrs(self, message): def _parse_chat_attrs(self, stanza, properties):
frm = message.getFrom() frm = properties.jid
to = message.getTo() to = stanza.getTo()
if frm.bareMatch(self._con.get_own_jid()): if frm.bareMatch(self._con.get_own_jid()):
with_ = to with_ = to
kind = KindConstant.CHAT_MSG_SENT kind = KindConstant.CHAT_MSG_SENT
@ -290,17 +241,15 @@ class MAM:
message=event.msgtxt, message=event.msgtxt,
contact_name=event.nick, contact_name=event.nick,
additional_data=event.additional_data, additional_data=event.additional_data,
stanza_id=stanza_id) stanza_id=stanza_id,
message_id=event.message_id)
app.nec.push_incoming_event( app.nec.push_incoming_event(
MamDecryptedMessageReceived(None, **vars(event))) MamDecryptedMessageReceived(None, **vars(event)))
def _is_valid_request(self, jid, query_id): def _is_valid_request(self, properties):
if query_id is None: valid_id = self._mam_query_ids.get(str(properties.mam.archive), None)
return False return valid_id == properties.mam.query_id
valid_id = self._mam_query_ids.get(jid.getStripped(), None)
return valid_id == query_id
def _get_query_id(self, jid): def _get_query_id(self, jid):
query_id = self._con.connection.getAnID() query_id = self._con.connection.getAnID()

View File

@ -18,9 +18,10 @@ import time
import logging import logging
import nbxmpp import nbxmpp
from nbxmpp.structs import StanzaHandler
from nbxmpp.const import MessageType
from gajim.common import app from gajim.common import app
from gajim.common import helpers
from gajim.common import caps_cache from gajim.common import caps_cache
from gajim.common.i18n import _ from gajim.common.i18n import _
from gajim.common.nec import NetworkIncomingEvent from gajim.common.nec import NetworkIncomingEvent
@ -36,8 +37,6 @@ from gajim.common.modules.misc import parse_attention
from gajim.common.modules.misc import parse_form from gajim.common.modules.misc import parse_form
from gajim.common.modules.misc import parse_oob from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_xhtml 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 MessageErrorEvent from gajim.common.connection_handlers_events import MessageErrorEvent
@ -49,30 +48,26 @@ class Message:
self._con = con self._con = con
self._account = con.name self._account = con.name
self.handlers = [('message', self._message_received)] self.handlers = [
StanzaHandler(name='message',
callback=self._message_received,
priority=50),
]
# XEPs for which this message module should not be executed # XEPs for which this message module should not be executed
self._message_namespaces = set([nbxmpp.NS_PUBSUB_EVENT, self._message_namespaces = set([nbxmpp.NS_PUBSUB_EVENT,
nbxmpp.NS_ROSTERX, nbxmpp.NS_ROSTERX,
nbxmpp.NS_MAM_1, nbxmpp.NS_IBB])
nbxmpp.NS_MAM_2,
nbxmpp.NS_CONFERENCE,
nbxmpp.NS_IBB,
nbxmpp.NS_CAPTCHA,])
def _message_received(self, _con, stanza, properties): def _message_received(self, _con, stanza, properties):
if properties.is_mam_message:
return
# Check if a child of the message contains any # Check if a child of the message contains any
# namespaces that we handle in other modules. # namespaces that we handle in other modules.
# nbxmpp executes less common handlers last # nbxmpp executes less common handlers last
if self._message_namespaces & set(stanza.getProperties()): if self._message_namespaces & set(stanza.getProperties()):
return return
muc_user = stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER)
if muc_user is not None and (stanza.getType() != 'error'):
if muc_user.getChildren():
# Not a PM, handled by MUC module
return
log.info('Received from %s', stanza.getFrom()) log.info('Received from %s', stanza.getFrom())
app.nec.push_incoming_event(NetworkEvent( app.nec.push_incoming_event(NetworkEvent(
@ -87,49 +82,36 @@ class Message:
# Ugly, we treat the from attr as the remote jid, # Ugly, we treat the from attr as the remote jid,
# to make that work with sent carbons we have to do this. # to make that work with sent carbons we have to do this.
# TODO: Check where in Gajim and plugins we depend on that behavior # TODO: Check where in Gajim and plugins we depend on that behavior
stanza.setFrom(stanza.getTo().getBare()) stanza.setFrom(stanza.getTo())
from_ = stanza.getFrom() from_ = stanza.getFrom()
type_ = stanza.getType() fjid = str(from_)
if type_ is None: jid = from_.getBare()
type_ = 'normal' resource = from_.getResource()
self_message = is_self_message(stanza, type_ == 'groupchat') type_ = properties.type
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 # Check for duplicates
stanza_id, origin_id = self._get_unique_id( stanza_id, message_id = self._get_unique_id(properties)
stanza, forwarded, sent, self_message, muc_pm)
# Check groupchat messages for duplicates, # Check groupchat messages for duplicates,
# We do this because of MUC History messages # We do this because of MUC History messages
if type_ == 'groupchat' or self_message or muc_pm: if (properties.type.is_groupchat or
if type_ == 'groupchat': properties.is_self_message or
properties.is_muc_pm):
if properties.type.is_groupchat:
archive_jid = stanza.getFrom().getStripped() archive_jid = stanza.getFrom().getStripped()
else: else:
archive_jid = self._con.get_own_jid().getStripped() archive_jid = self._con.get_own_jid().getStripped()
if app.logger.find_stanza_id(self._account, if app.logger.find_stanza_id(self._account,
archive_jid, archive_jid,
stanza_id, stanza_id,
origin_id, message_id,
type_ == 'groupchat'): properties.type.is_groupchat):
return return
thread_id = stanza.getThread() thread_id = properties.thread
msgtxt = stanza.getBody() msgtxt = properties.body
# TODO: remove all control UI stuff # TODO: remove all control UI stuff
gc_control = app.interface.msg_win_mgr.get_gc_control( gc_control = app.interface.msg_win_mgr.get_gc_control(
@ -139,7 +121,7 @@ class Message:
gc_control = minimized.get(jid) gc_control = minimized.get(jid)
if gc_control and jid == fjid: if gc_control and jid == fjid:
if type_ == 'error': if properties.type.is_error:
if msgtxt: if msgtxt:
msgtxt = _('error while sending %(message)s ( %(error)s )') % { msgtxt = _('error while sending %(message)s ( %(error)s )') % {
'message': msgtxt, 'error': stanza.getErrorMsg()} 'message': msgtxt, 'error': stanza.getErrorMsg()}
@ -148,11 +130,11 @@ class Message:
# TODO: why is this here? # TODO: why is this here?
if stanza.getTag('html'): if stanza.getTag('html'):
stanza.delChild('html') stanza.delChild('html')
type_ = 'groupchat' type_ = MessageType.GROUPCHAT
session = None session = None
if type_ != 'groupchat': if not properties.type.is_groupchat:
if muc_pm and type_ == 'error': if properties.is_muc_pm and properties.type.is_error:
session = self._con.find_session(fjid, thread_id) session = self._con.find_session(fjid, thread_id)
if not session: if not session:
session = self._con.get_latest_session(fjid) session = self._con.get_latest_session(fjid)
@ -171,7 +153,7 @@ class Message:
'conn': self._con, 'conn': self._con,
'stanza': stanza, 'stanza': stanza,
'account': self._account, 'account': self._account,
'id_': id_, 'id_': properties.id,
'encrypted': False, 'encrypted': False,
'additional_data': AdditionalDataDict(), 'additional_data': AdditionalDataDict(),
'forwarded': forwarded, 'forwarded': forwarded,
@ -180,13 +162,14 @@ class Message:
'jid': jid, 'jid': jid,
'resource': resource, 'resource': resource,
'stanza_id': stanza_id, 'stanza_id': stanza_id,
'unique_id': stanza_id or origin_id, 'unique_id': stanza_id or message_id,
'mtype': type_, 'message_id': properties.id,
'mtype': type_.value,
'msgtxt': msgtxt, 'msgtxt': msgtxt,
'thread_id': thread_id, 'thread_id': thread_id,
'session': session, 'session': session,
'self_message': self_message, 'self_message': properties.is_self_message,
'muc_pm': muc_pm, 'muc_pm': properties.is_muc_pm,
'gc_control': gc_control 'gc_control': gc_control
} }
@ -313,7 +296,8 @@ class Message:
message=event.msgtxt, message=event.msgtxt,
contact_name=event.nick, contact_name=event.nick,
additional_data=event.additional_data, additional_data=event.additional_data,
stanza_id=event.stanza_id) stanza_id=event.stanza_id,
message_id=event.message_id)
app.logger.set_room_last_message_time(event.room_jid, event.timestamp) app.logger.set_room_last_message_time(event.room_jid, event.timestamp)
self._con.get_module('MAM').save_archive_id( self._con.get_module('MAM').save_archive_id(
event.room_jid, event.stanza_id, event.timestamp) event.room_jid, event.stanza_id, event.timestamp)
@ -324,36 +308,29 @@ class Message:
if stanza_id is None and namespace == nbxmpp.NS_MAM_2: if stanza_id is None and namespace == nbxmpp.NS_MAM_2:
log.warning('%s announces mam:2 without stanza-id', room_jid) log.warning('%s announces mam:2 without stanza-id', room_jid)
def _get_unique_id(self, stanza, _forwarded, _sent, self_message, _muc_pm): def _get_unique_id(self, properties):
if stanza.getType() == 'groupchat': if properties.is_self_message:
# TODO: Disco the MUC check if 'urn:xmpp:mam:2' is announced # Deduplicate self message with message-id
return self._get_stanza_id(stanza), None return None, properties.id
if stanza.getType() != 'chat': if properties.stanza_id.by is None:
# We can not verify who sent this stanza-id, ignore it.
return None, None return None, None
# Messages we receive live if properties.type.is_groupchat:
if self._con.get_module('MAM').archiving_namespace != nbxmpp.NS_MAM_2: namespace = caps_cache.muc_caps_cache.get_mam_namespace(
properties.jid.getBare())
archive = properties.jid
else:
namespace = self._con.get_module('MAM').archiving_namespace
archive = self._con.get_own_jid()
if namespace != nbxmpp.NS_MAM_2:
# Only mam:2 ensures valid stanza-id # Only mam:2 ensures valid stanza-id
return None, None return None, None
if self_message: if archive.bareMatch(properties.stanza_id.by):
return self._get_stanza_id(stanza), stanza.getOriginID() return properties.stanza_id.id, None
return self._get_stanza_id(stanza), None
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): class MessageReceivedEvent(NetworkIncomingEvent):

View File

@ -16,35 +16,6 @@
from typing import Union from typing import Union
import nbxmpp
from gajim.common import app
def is_self_message(message: nbxmpp.Node,
groupchat: bool = False) -> bool:
if groupchat:
return False
frm = message.getFrom()
to = message.getTo()
return frm.bareMatch(to)
def is_muc_pm(message: nbxmpp.Node,
jid: nbxmpp.JID,
groupchat: bool = False) -> bool:
if groupchat:
return False
muc_user = message.getTag('x', namespace=nbxmpp.NS_MUC_USER)
if muc_user is not None:
return muc_user.getChildren() == []
# 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
def from_xs_boolean(value: Union[str, bool]) -> bool: def from_xs_boolean(value: Union[str, bool]) -> bool:
if isinstance(value, bool): if isinstance(value, bool):

View File

@ -134,7 +134,8 @@ class ChatControlSession:
message=msg_to_log, message=msg_to_log,
subject=obj.subject, subject=obj.subject,
additional_data=obj.additional_data, additional_data=obj.additional_data,
stanza_id=obj.unique_id) stanza_id=obj.unique_id,
message_id=obj.message_id)
self.conn.get_module('MAM').save_archive_id( self.conn.get_module('MAM').save_archive_id(
None, obj.stanza_id, obj.timestamp) None, obj.stanza_id, obj.timestamp)