diff libervia/backend/plugins/plugin_xep_0424.py @ 4166:a1f7040b5a15

plugin XEP-0424: message retraction update: - follow specification update (with namespace bump) - retract from history on message reception for group chat - send bridge message
author Goffi <goffi@goffi.org>
date Thu, 30 Nov 2023 13:23:53 +0100
parents 4b842c1fb686
children 7eda7cb8a15c
line wrap: on
line diff
--- 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 <http://www.gnu.org/licenses/>.
 
-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 <retract> 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