changeset 4382:b897d98b2c51 default tip

plugin XEP-0297: Reworked `forward` method and add bridge method: `Forward` method has been reworked and now includes a fallback. XEP-0297 ask to not use fallback, but following a discussion on xsf@, we agreed that this is a legacy thing and a fallback should nowadays be used, I'll propose a patch to the specification. A `message_forward` has been added to bridge. rel 461
author Goffi <goffi@goffi.org>
date Fri, 04 Jul 2025 12:33:42 +0200
parents 3c97717fd662
children
files libervia/backend/plugins/plugin_xep_0297.py
diffstat 1 files changed, 123 insertions(+), 38 deletions(-) [+]
line wrap: on
line diff
--- a/libervia/backend/plugins/plugin_xep_0297.py	Fri Jul 04 12:30:20 2025 +0200
+++ b/libervia/backend/plugins/plugin_xep_0297.py	Fri Jul 04 12:33:42 2025 +0200
@@ -18,41 +18,65 @@
 # 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 datetime import datetime
+from typing import cast
+
+from dateutil import tz
+from twisted.internet import defer
+from twisted.words.protocols.jabber import jid
+from twisted.words.protocols.jabber.xmlstream import XMPPHandler
+from twisted.words.xish import domish
+from wokkel import disco, iwokkel
+from zope.interface import implementer
+
+from libervia.backend import G
+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.core_types import SatXMPPEntity
+from libervia.backend.models.core import MessageData
+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.plugins.plugin_xep_0428 import XEP_0428
+from libervia.backend.tools.common.date_utils import date_fmt
 
-from twisted.internet import defer
+
 
 log = getLogger(__name__)
 
-from wokkel import disco, iwokkel
 
-try:
-    from twisted.words.protocols.xmlstream import XMPPHandler
-except ImportError:
-    from wokkel.subprotocols import XMPPHandler
-from zope.interface import implementer
-
-from twisted.words.xish import domish
 
 PLUGIN_INFO = {
     C.PI_NAME: "Stanza Forwarding",
     C.PI_IMPORT_NAME: "XEP-0297",
     C.PI_TYPE: "XEP",
     C.PI_PROTOCOLS: ["XEP-0297"],
+    C.PI_DEPENDENCIES: ["XEP-0428"],
     C.PI_MAIN: "XEP_0297",
     C.PI_HANDLER: "yes",
     C.PI_DESCRIPTION: D_("""Implementation of Stanza Forwarding"""),
 }
 
+NS_FORWARD = "urn:xmpp:forward:0"
 
-class XEP_0297(object):
-    # FIXME: check this implementation which doesn't seems to be used
+
+class XEP_0297:
+    # TODO: Handle forwarded message reception, for now the fallback will be used.
+    # TODO: Add a method to forward Pubsub Item, and notably blog content.
 
     def __init__(self, host):
         log.info(_("Stanza Forwarding plugin initialization"))
         self.host = host
+        self._fallback = cast(XEP_0428, host.plugins["XEP-0428"])
+        host.register_namespace("forward", NS_FORWARD)
+        host.bridge.add_method(
+            "message_forward",
+            ".plugin",
+            in_sign="sss",
+            out_sign="",
+            method=self._forward,
+            async_=True,
+        )
 
     def get_handler(self, client):
         return XEP_0297_handler(self, client.profile)
@@ -72,41 +96,102 @@
             if isinstance(child, domish.Element) and not child.uri:
                 XEP_0297.update_uri(child, uri)
 
-    def forward(self, stanza, to_jid, stamp, body="", profile_key=C.PROF_KEY_NONE):
+    def _forward(
+        self,
+        message_id: str,
+        recipient_jid_s: str,
+        profile_key: str
+    ) -> defer.Deferred[None]:
+        client = self.host.get_client(profile_key)
+        recipient_jid = jid.JID(recipient_jid_s)
+        return defer.ensureDeferred(self.forward_by_id(client, message_id, recipient_jid))
+
+    async def forward_by_id(
+        self,
+        client: SatXMPPEntity,
+        message_id: str,
+        recipient_jid: jid.JID
+    ) -> None:
+        history = cast(History, await G.storage.get(
+            client,
+            History,
+            History.uid,
+            message_id,
+            joined_loads=[History.messages, History.subjects, History.thread],
+        ))
+        if not history:
+            raise exceptions.NotFound(
+                f"No history found with message {message_id!r}."
+            )
+        # FIXME: Q&D way to get MessageData from History. History should have a proper
+        #     way to do that.
+        serialised_history = history.serialise()
+        serialised_history["from"] = jid.JID(serialised_history["from"])
+        serialised_history["to"] = jid.JID(serialised_history["to"])
+        mess_data = MessageData(serialised_history)
+        client.generate_message_xml(mess_data)
+        message_elt = cast(domish.Element, mess_data["xml"])
+        timestamp_float = float(history.timestamp or history.received_timestamp)
+        timestamp = datetime.fromtimestamp(timestamp_float, tz.tzutc())
+
+        fallback_lines = [
+            f"{client.jid.userhost()} is forwarding this message to you:",
+            f"[{date_fmt(timestamp_float)}]",
+            f"From: {history.source_jid.full()}",
+            f"To: {history.dest_jid.full()}"
+        ]
+
+        for subject in history.subjects:
+            if subject.language:
+                fallback_lines.append(f"Subject [{subject.language}]: {subject.subject}")
+            else:
+                fallback_lines.append(f"Subject: {subject.subject}")
+
+        for message in history.messages:
+            if message.language:
+                fallback_lines.append(f"Message [{message.language}]: {message.message}")
+            else:
+                fallback_lines.append(f"Message: {message.message}")
+
+        fallback_msg = "\n".join(fallback_lines)
+
+        await self.forward(client, message_elt, recipient_jid, timestamp, fallback_msg)
+
+    async def forward(
+        self,
+        client: SatXMPPEntity,
+        stanza: domish.Element,
+        to_jid: jid.JID,
+        timestamp: datetime|None,
+        fallback_msg: str | None = None
+    ):
         """Forward a message to the given JID.
 
-        @param stanza (domish.Element): original stanza to be forwarded.
-        @param to_jid (JID): recipient JID.
-        @param stamp (datetime): offset-aware timestamp of the original reception.
-        @param body (unicode): optional description.
-        @param profile_key (unicode): %(doc_profile_key)s
+        @param client: client instance.
+        @param stanza: original stanza to be forwarded.
+        @param to_jid: recipient JID.
+        @param timestamp: offset-aware timestamp of the original reception.
+        @param body: optional description.
         @return: a Deferred when the message has been sent
         """
-        # FIXME: this method is not used and doesn't use mess_data which should be used for client.send_message_data
-        #        should it be deprecated? A method constructing the element without sending it seems more natural
-        log.warning(
-            "THIS METHOD IS DEPRECATED"
-        )  #  FIXME: we use this warning until we check the method
-        msg = domish.Element((None, "message"))
-        msg["to"] = to_jid.full()
-        msg["type"] = stanza["type"]
-
-        body_elt = domish.Element((None, "body"))
-        if body:
-            body_elt.addContent(body)
+        message_elt = domish.Element((None, "message"))
+        message_elt["to"] = to_jid.full()
+        message_elt["type"] = stanza["type"]
 
         forwarded_elt = domish.Element((C.NS_FORWARD, "forwarded"))
-        delay_elt = self.host.plugins["XEP-0203"].delay(stamp)
-        forwarded_elt.addChild(delay_elt)
-        if not stanza.uri:  # None or ''
+        if timestamp:
+            delay_elt = self.host.plugins["XEP-0203"].delay(timestamp)
+            forwarded_elt.addChild(delay_elt)
+        if not stanza.uri:
             XEP_0297.update_uri(stanza, "jabber:client")
         forwarded_elt.addChild(stanza)
 
-        msg.addChild(body_elt)
-        msg.addChild(forwarded_elt)
-
-        client = self.host.get_client(profile_key)
-        return defer.ensureDeferred(client.send_message_data({"xml": msg}))
+        message_elt.addChild(domish.Element((None, "body")))
+        message_elt.addChild(forwarded_elt)
+        self._fallback.add_fallback_elt(message_elt, NS_FORWARD, fallback_msg)
+        return await client.send_message_data(
+            MessageData({"xml": message_elt, "extra": {}})
+        )
 
 
 @implementer(iwokkel.IDisco)