# HG changeset patch # User Goffi # Date 1701347033 -3600 # Node ID a1f7040b5a15589392aa85307659dd11660a18ba # Parent 81faa85c9cfacf50740a53aee53ca7d093f47314 plugin XEP-0424: message retraction update: - follow specification update (with namespace bump) - retract from history on message reception for group chat - send bridge message diff -r 81faa85c9cfa -r a1f7040b5a15 libervia/backend/core/constants.py --- a/libervia/backend/core/constants.py Tue Nov 28 17:41:49 2023 +0100 +++ b/libervia/backend/core/constants.py Thu Nov 30 13:23:53 2023 +0100 @@ -126,6 +126,7 @@ MESS_UPDATE_EDIT = "EDIT" MESS_UPDATE_REACTION = "REACTION" + MESS_UPDATE_RETRACT = "RETRACT" MESS_EXTRA_INFO = "info_type" EXTRA_INFO_DECR_ERR = "DECRYPTION_ERROR" diff -r 81faa85c9cfa -r a1f7040b5a15 libervia/backend/models/core.py --- a/libervia/backend/models/core.py Tue Nov 28 17:41:49 2023 +0100 +++ b/libervia/backend/models/core.py Thu Nov 30 13:23:53 2023 +0100 @@ -32,11 +32,6 @@ super().__init__(**kwargs) -class MessageEditData(MessageData): - """Data used when a new message edition has been received""" - pass - - class MessageReactionData(MessageUpdateData): reactions: dict[str, list[str]] = Field( description="Reaction to reacting entities mapping" diff -r 81faa85c9cfa -r a1f7040b5a15 libervia/backend/plugins/plugin_comp_ap_gateway/__init__.py --- a/libervia/backend/plugins/plugin_comp_ap_gateway/__init__.py Tue Nov 28 17:41:49 2023 +0100 +++ b/libervia/backend/plugins/plugin_comp_ap_gateway/__init__.py Thu Nov 30 13:23:53 2023 +0100 @@ -167,7 +167,7 @@ self.ad_hoc = APAdHocService(self) self.ap_events = APEvents(self) host.trigger.add("message_received", self._message_received_trigger, priority=-1000) - host.trigger.add("XEP-0424_retractReceived", self._on_message_retract) + host.trigger.add("XEP-0424_retract_received", self._on_message_retract) host.trigger.add("XEP-0372_ref_received", self._on_reference_received) host.bridge.add_method( @@ -2321,7 +2321,7 @@ client: SatXMPPEntity, message_elt: domish.Element, retract_elt: domish.Element, - fastened_elts + history: History ) -> bool: if client != self.client: return True @@ -2341,7 +2341,7 @@ actor_id = await self.get_ap_actor_id_from_account(ap_account) inbox = await self.get_ap_inbox_from_id(actor_id, use_shared=False) url_actor, ap_item = await self.ap_delete_item( - from_jid.userhostJID(), None, fastened_elts.id, public=False + from_jid.userhostJID(), None, retract_elt["id"], public=False ) resp = await self.sign_and_post(inbox, url_actor, ap_item) return False diff -r 81faa85c9cfa -r a1f7040b5a15 libervia/backend/plugins/plugin_xep_0308.py --- a/libervia/backend/plugins/plugin_xep_0308.py Tue Nov 28 17:41:49 2023 +0100 +++ b/libervia/backend/plugins/plugin_xep_0308.py Thu Nov 30 13:23:53 2023 +0100 @@ -31,7 +31,7 @@ from libervia.backend.core.i18n import _ from libervia.backend.core.log import getLogger from libervia.backend.memory.sqla import History, Message, Subject, joinedload, select -from libervia.backend.models.core import MessageEditData, MessageEdition +from libervia.backend.models.core import MessageData, MessageEdition from libervia.backend.tools.common import data_format from libervia.backend.tools.utils import aio log = getLogger(__name__) @@ -140,7 +140,7 @@ edited_history.extra.setdefault(C.MESS_EXTRA_EDITIONS, []).append(previous_version) await self.host.memory.storage.add(edited_history) - edit_data = MessageEditData(edited_history.serialise()) + edit_data = MessageData(edited_history.serialise()) self.host.bridge.message_update( edited_history.uid, C.MESS_UPDATE_EDIT, diff -r 81faa85c9cfa -r a1f7040b5a15 libervia/backend/plugins/plugin_xep_0424.py --- a/libervia/backend/plugins/plugin_xep_0424.py Tue Nov 28 17:41:49 2023 +0100 +++ b/libervia/backend/plugins/plugin_xep_0424.py Thu Nov 30 13:23:53 2023 +0100 @@ -15,22 +15,23 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from typing import Dict, Any import time -from copy import deepcopy +from typing import Any, Dict -from twisted.words.protocols.jabber import xmlstream, jid +from sqlalchemy.orm.attributes import flag_modified +from twisted.internet import defer +from twisted.words.protocols.jabber import jid, xmlstream from twisted.words.xish import domish -from twisted.internet import defer from wokkel import disco from zope.interface import implementer +from libervia.backend.core import exceptions from libervia.backend.core.constants import Const as C -from libervia.backend.core.i18n import _, D_ -from libervia.backend.core import exceptions -from libervia.backend.core.core_types import SatXMPPEntity +from libervia.backend.core.core_types import MessageData, SatXMPPEntity +from libervia.backend.core.i18n import D_, _ from libervia.backend.core.log import getLogger from libervia.backend.memory.sqla_mapping import History +from libervia.backend.tools.common import data_format log = getLogger(__name__) @@ -40,14 +41,14 @@ C.PI_IMPORT_NAME: "XEP-0424", C.PI_TYPE: "XEP", C.PI_MODES: C.PLUG_MODE_BOTH, - C.PI_PROTOCOLS: ["XEP-0334", "XEP-0424", "XEP-0428"], - C.PI_DEPENDENCIES: ["XEP-0422"], + C.PI_PROTOCOLS: ["XEP-0424"], + C.PI_DEPENDENCIES: ["XEP-0334", "XEP-0428"], C.PI_MAIN: "XEP_0424", C.PI_HANDLER: "yes", C.PI_DESCRIPTION: _("""Implementation Message Retraction"""), } -NS_MESSAGE_RETRACT = "urn:xmpp:message-retract:0" +NS_MESSAGE_RETRACT = "urn:xmpp:message-retract:1" CATEGORY = "Privacy" NAME = "retract_history" @@ -65,14 +66,13 @@ ) -class XEP_0424(object): +class XEP_0424: def __init__(self, host): - log.info(_("XEP-0424 (Message Retraction) plugin initialization")) + log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization") self.host = host host.memory.update_params(PARAMS) self._h = host.plugins["XEP-0334"] - self._f = host.plugins["XEP-0422"] host.register_namespace("message-retract", NS_MESSAGE_RETRACT) host.trigger.add("message_received", self._message_received_trigger, 100) host.bridge.add_method( @@ -96,7 +96,7 @@ def retract_by_origin_id( self, client: SatXMPPEntity, - dest_jid: jid.JID, + peer_jid: jid.JID, origin_id: str ) -> None: """Send a message retraction using origin-id @@ -108,9 +108,9 @@ """ message_elt = domish.Element((None, "message")) message_elt["from"] = client.jid.full() - message_elt["to"] = dest_jid.full() - apply_to_elt = self._f.apply_to_elt(message_elt, origin_id) - apply_to_elt.addElement((NS_MESSAGE_RETRACT, "retract")) + message_elt["to"] = peer_jid.full() + retract_elt = message_elt.addElement((NS_MESSAGE_RETRACT, "retract")) + retract_elt["id"] = origin_id self.host.plugins["XEP-0428"].add_fallback_elt( message_elt, "[A message retraction has been requested, but your client doesn't support " @@ -138,8 +138,17 @@ "client is probably not supporting message retraction." ) else: - self.retract_by_origin_id(client, history.dest_jid, origin_id) - await self.retract_db_history(client, history) + if history.type == C.MESS_TYPE_GROUPCHAT: + is_group_chat = True + peer_jid = history.dest_jid + else: + is_group_chat = False + peer_jid = jid.JID(history.dest) + self.retract_by_origin_id(client, peer_jid, origin_id) + if not is_group_chat: + # retraction will happen when message will be received in the + # chat. + await self.retract_db_history(client, history) async def retract( self, @@ -157,7 +166,7 @@ raise ValueError("message_id can't be empty") history = await self.host.memory.storage.get( client, History, History.uid, message_id, - joined_loads=[History.messages, History.subjects] + joined_loads=[History.messages, History.subjects, History.thread] ) if history is None: raise exceptions.NotFound( @@ -171,12 +180,10 @@ @param history: history instance "messages" and "subjects" must be loaded too """ - # FIXME: should be keep history? This is useful to check why a message has been + # FIXME: should we keep history? This is useful to check why a message has been # retracted, but if may be bad if the user think it's really deleted - # we assign a new object to be sure to trigger an update - history.extra = deepcopy(history.extra) if history.extra else {} - history.extra["retracted"] = True - keep_history = self.host.memory.param_get_a( + flag_modified(history, "extra") + keep_history = await self.host.memory.param_get_a_async( NAME, CATEGORY, profile_key=client.profile ) old_version: Dict[str, Any] = { @@ -185,13 +192,27 @@ if keep_history: old_version.update({ "messages": [m.serialise() for m in history.messages], - "subjects": [s.serialise() for s in history.subjects] + "subjects": [s.serialise() for s in history.subjects], }) history.extra.setdefault("old_versions", []).append(old_version) - await self.host.memory.storage.delete( - history.messages + history.subjects, - session_add=[history] + + history.messages.clear() + history.subjects.clear() + history.extra["retracted"] = True + # we remove editions history to keep no trace of old messages + if "editions" in history.extra: + del history.extra["editions"] + if "attachments" in history.extra: + del history.extra["attachments"] + await self.host.memory.storage.add(history) + + retract_data = MessageData(history.serialise()) + self.host.bridge.message_update( + history.uid, + C.MESS_UPDATE_RETRACT, + data_format.serialise(retract_data), + client.profile, ) async def _message_received_trigger( @@ -200,39 +221,48 @@ message_elt: domish.Element, post_treat: defer.Deferred ) -> bool: - fastened_elts = await self._f.get_fastened_elts(client, message_elt) - if fastened_elts is None: + retract_elt = next(message_elt.elements(NS_MESSAGE_RETRACT, "retract"), None) + if not retract_elt: return True - for elt in fastened_elts.elements: - if elt.name == "retract" and elt.uri == NS_MESSAGE_RETRACT: - if fastened_elts.history is not None: - source_jid = fastened_elts.history.source_jid - from_jid = jid.JID(message_elt["from"]) - if source_jid.userhostJID() != from_jid.userhostJID(): - log.warning( - f"Received message retraction from {from_jid.full()}, but " - f"the message to retract is from {source_jid.full()}. This " - f"maybe a hack attempt.\n{message_elt.toXml()}" - ) - return False - break - else: - return True + try: + if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT: + col_id = History.stanza_id + else: + col_id = History.origin_id + history = await self.host.memory.storage.get( + client, History, col_id, retract_elt["id"], + joined_loads=[History.messages, History.subjects, History.thread] + ) + except KeyError: + log.warning(f"invalid retract element, missing id: {retract_elt.toXml()}") + return False + from_jid = jid.JID(message_elt["from"]) + + if ( + history is not None + and history.source_jid.userhostJID() != from_jid.userhostJID() + ): + log.warning( + f"Received message retraction from {from_jid.full()}, but the message to " + f"retract is from {history.source_jid.full()}. This maybe a hack " + f"attempt.\n{message_elt.toXml()}" + ) + return False + if not await self.host.trigger.async_point( - "XEP-0424_retractReceived", client, message_elt, elt, fastened_elts + "XEP-0424_retract_received", client, message_elt, retract_elt, history ): return False - if fastened_elts.history is None: + + if history is None: # we check history after the trigger because we may be in a component which # doesn't store messages in database. log.warning( f"No message found with given origin-id: {message_elt.toXml()}" ) return False - log.info(f"[{client.profile}] retracting message {fastened_elts.id!r}") - await self.retract_db_history(client, fastened_elts.history) - # TODO: send bridge signal - + log.info(f"[{client.profile}] retracting message {history.uid!r}") + await self.retract_db_history(client, history) return False diff -r 81faa85c9cfa -r a1f7040b5a15 libervia/backend/plugins/plugin_xep_0428.py --- a/libervia/backend/plugins/plugin_xep_0428.py Tue Nov 28 17:41:49 2023 +0100 +++ b/libervia/backend/plugins/plugin_xep_0428.py Thu Nov 30 13:23:53 2023 +0100 @@ -33,6 +33,7 @@ C.PI_NAME: "Fallback Indication", C.PI_IMPORT_NAME: "XEP-0428", C.PI_TYPE: "XEP", + C.PI_MODES: C.PLUG_MODE_BOTH, C.PI_PROTOCOLS: ["XEP-0428"], C.PI_MAIN: "XEP_0428", C.PI_HANDLER: "yes",