From 6dfa4645b350dada15eaa07519de2584963e05ff Mon Sep 17 00:00:00 2001 From: wurstsalat Date: Sat, 13 May 2023 19:31:34 +0200 Subject: feat: Add support for XEP-0424 (Message Retraction) --- gajim/common/const.py | 1 + gajim/common/events.py | 8 +++ gajim/common/modules/mam.py | 8 +++ gajim/common/modules/message.py | 8 +++ gajim/common/modules/message_retraction.py | 84 +++++++++++++++++++++++++++++ gajim/common/modules/util.py | 38 +++++++++++++ gajim/common/storage/archive.py | 87 ++++++++++++++++++++++++++++++ 7 files changed, 234 insertions(+) create mode 100644 gajim/common/modules/message_retraction.py diff --git a/gajim/common/const.py b/gajim/common/const.py index dd1e6b3c1..29d8dc198 100644 --- a/gajim/common/const.py +++ b/gajim/common/const.py @@ -923,6 +923,7 @@ COMMON_FEATURES = [ Namespace.JINGLE_IBB, Namespace.AVATAR_METADATA + '+notify', Namespace.MESSAGE_MODERATE, + Namespace.MESSAGE_RETRACT, Namespace.OMEMO_TEMP_DL + '+notify' ] diff --git a/gajim/common/events.py b/gajim/common/events.py index 59ea378af..1e7847865 100644 --- a/gajim/common/events.py +++ b/gajim/common/events.py @@ -439,6 +439,14 @@ class MessageError(ApplicationEvent): error: Any +@dataclass +class MessageRetractionReceived(ApplicationEvent): + name: str = field(init=False, default='message-retraction-received') + account: str + jid: JID + origin_id: str + + @dataclass class RosterItemExchangeEvent(ApplicationEvent): name: str = field(init=False, default='roster-item-exchange') diff --git a/gajim/common/modules/mam.py b/gajim/common/modules/mam.py index acd8e3710..9bd05f3a4 100644 --- a/gajim/common/modules/mam.py +++ b/gajim/common/modules/mam.py @@ -54,6 +54,7 @@ from gajim.common.modules.base import BaseModule from gajim.common.modules.misc import parse_oob from gajim.common.modules.util import as_task from gajim.common.modules.util import check_if_message_correction +from gajim.common.modules.util import check_if_message_retraction from gajim.common.modules.util import get_eme_message @@ -325,6 +326,13 @@ class MAM(BaseModule): self._log): return + if check_if_message_retraction(properties, + self._account, + properties.jid, + kind, + self._log): + return + app.storage.archive.insert_into_logs( self._account, jid, diff --git a/gajim/common/modules/message.py b/gajim/common/modules/message.py index dff03c1d6..a99cdd97d 100644 --- a/gajim/common/modules/message.py +++ b/gajim/common/modules/message.py @@ -40,6 +40,7 @@ from gajim.common.modules.contacts import GroupchatParticipant from gajim.common.modules.misc import parse_oob from gajim.common.modules.misc import parse_xhtml from gajim.common.modules.util import check_if_message_correction +from gajim.common.modules.util import check_if_message_retraction from gajim.common.modules.util import get_eme_message from gajim.common.structs import OutgoingMessage @@ -190,6 +191,13 @@ class Message(BaseModule): self._log): return + if check_if_message_retraction(properties, + self._account, + from_, + kind, + self._log): + return + if type_.is_groupchat: if not msgtxt: return diff --git a/gajim/common/modules/message_retraction.py b/gajim/common/modules/message_retraction.py new file mode 100644 index 000000000..eef727554 --- /dev/null +++ b/gajim/common/modules/message_retraction.py @@ -0,0 +1,84 @@ +# 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 Retraction (XEP-0424) + +from __future__ import annotations + +import nbxmpp +from nbxmpp.namespaces import Namespace +from nbxmpp.protocol import JID +from nbxmpp.protocol import Message +from nbxmpp.structs import MessageProperties +from nbxmpp.structs import StanzaHandler + +from gajim.common import app +from gajim.common import types +from gajim.common.events import MessageRetractionReceived +from gajim.common.modules.base import BaseModule + + +class MessageRetraction(BaseModule): + + _nbxmpp_extends = 'MessageRetraction' + + def __init__(self, client: types.Client) -> None: + BaseModule.__init__(self, client) + + self.handlers = [ + StanzaHandler(name='message', + callback=self._process_message_retraction, + ns=Namespace.MESSAGE_RETRACT, + priority=47) + ] + + def _process_message_retraction(self, + _client: types.xmppClient, + _stanza: Message, + properties: MessageProperties + ) -> None: + + if properties.message_retraction is None: + return + + if properties.type.is_error: + return + + # TODO: make sure to use correct jid here + jid = properties.jid + assert jid is not None + + success = app.storage.archive.try_message_retraction( + self._account, + jid, + properties.message_retraction.origin_id, + properties.occupant_id) + + if not success: + self._log.warning( + 'Received invalid message retraction request from %s', jid) + return + + app.ged.raise_event(MessageRetractionReceived( + account=self._account, + jid=jid, + origin_id=properties.message_retraction.origin_id)) + raise nbxmpp.NodeProcessed + + def retract_message(self, + jid: JID, + message_id: str + ) -> None: + + pass diff --git a/gajim/common/modules/util.py b/gajim/common/modules/util.py index a8a6582a2..a5492c961 100644 --- a/gajim/common/modules/util.py +++ b/gajim/common/modules/util.py @@ -36,6 +36,7 @@ from gajim.common import app from gajim.common import types from gajim.common.const import EME_MESSAGES from gajim.common.const import KindConstant +from gajim.common.events import MessageRetractionReceived from gajim.common.events import MessageUpdated from gajim.common.modules.misc import parse_correction @@ -130,6 +131,43 @@ def as_task(func): return func_wrapper +def check_if_message_retraction(properties: MessageProperties, + account: str, + jid: JID, + kind: KindConstant, + logger: LoggerAdapter[logging.Logger] + ) -> bool: + + if properties.message_retraction is None: + return False + + if properties.type.is_error: + return False + + # TODO: make sure to use correct jid here + #jid = properties.jid + assert jid is not None + + successful = app.storage.archive.try_message_retraction( + account, + jid, + properties.message_retraction.origin_id, + properties.is_mam_message, + properties.from_muc, + properties.occupant_id) + + if not successful: + logger.warning( + 'Received invalid message retraction request from %s', jid) + return False + + app.ged.raise_event(MessageRetractionReceived( + account=account, + jid=jid, + origin_id=properties.message_retraction.origin_id)) + return True + + def check_if_message_correction(properties: MessageProperties, account: str, jid: JID, diff --git a/gajim/common/storage/archive.py b/gajim/common/storage/archive.py index 3ce90d309..3baa0bb36 100644 --- a/gajim/common/storage/archive.py +++ b/gajim/common/storage/archive.py @@ -1048,6 +1048,93 @@ class MessageArchiveStorage(SqliteStorage): return True + @timeit + def try_message_retraction(self, + account: str, + jid: JID, + origin_id: str, + is_mam_message: bool, + is_groupchat: bool, + occupant_id: Optional[str] + ) -> bool: + + '''Try to retract a message (XEP-0424) + + :param jid: This can be a full jid or bare jid. + ''' + + account_id = self.get_account_id(account) + + self._log.debug( + 'Check if message is retractable, parameters: %s %s %s %s', + jid, account_id, origin_id, occupant_id) + + if is_groupchat and occupant_id is None: + # Group chat messages without occupant ID cannot be retracted + self._log.warning( + 'Message retraction in MUC (%s) failed: no occupant ID', jid) + return False + + if is_groupchat: + sql = '''SELECT log_line_id, message, additional_data + FROM logs + NATURAL JOIN jids jid_id + WHERE +jid = ? + AND account_id = ? + AND message_id = ? + AND occupant_id = ? + ''' + rows = self._con.execute( + sql, + (jid, + account_id, + origin_id, + occupant_id)).fetchall() + else: + sql = '''SELECT log_line_id, message, additional_data + FROM logs + NATURAL JOIN jids jid_id + WHERE +jid = ? + AND account_id = ? + AND message_id = ? + ''' + rows = self._con.execute( + sql, + (jid, + account_id, + origin_id)).fetchall() + + if not rows: + self._log.debug('No retractable messages found') + return False + + if len(rows) != 1: + self._log.warning('More than one retractable message found') + return False + + row = rows[0] + + if row.additional_data is None: + additional_data = AdditionalDataDict() + else: + additional_data = row.additional_data + + additional_data.set_value( + 'retracted', 'by', str(jid)) + additional_data.set_value( + 'retracted', 'timestamp', '1') + serialized_dict = json.dumps(additional_data.data) + + sql = ''' + UPDATE logs SET message = ?, additional_data = ? + WHERE log_line_id = ? + ''' + self._con.execute( + sql, + ('TODO: retracted placeholder', serialized_dict, row.log_line_id)) + + return True + @timeit def update_additional_data(self, account: str, -- cgit v1.2.3