diff libervia/backend/plugins/plugin_xep_0033.py @ 4071:4b842c1fb686

refactoring: renamed `sat` package to `libervia.backend`
author Goffi <goffi@goffi.org>
date Fri, 02 Jun 2023 11:49:51 +0200
parents sat/plugins/plugin_xep_0033.py@c23cad65ae99
children 0d7bb4df2343
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libervia/backend/plugins/plugin_xep_0033.py	Fri Jun 02 11:49:51 2023 +0200
@@ -0,0 +1,243 @@
+#!/usr/bin/env python3
+
+
+# SAT plugin for Extended Stanza Addressing (xep-0033)
+# Copyright (C) 2013-2016 Adrien Cossa (souliane@mailoo.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program 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 Affero General Public License for more details.
+
+# 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 libervia.backend.core.i18n import _
+from libervia.backend.core.constants import Const as C
+from libervia.backend.core.log import getLogger
+
+log = getLogger(__name__)
+from libervia.backend.core import exceptions
+from wokkel import disco, iwokkel
+from zope.interface import implementer
+from twisted.words.protocols.jabber.jid import JID
+from twisted.python import failure
+import copy
+
+try:
+    from twisted.words.protocols.xmlstream import XMPPHandler
+except ImportError:
+    from wokkel.subprotocols import XMPPHandler
+from twisted.words.xish import domish
+from twisted.internet import defer
+
+from libervia.backend.tools import trigger
+from time import time
+
+# TODO: fix Prosody "addressing" plugin to leave the concerned bcc according to the spec:
+#
+# http://xmpp.org/extensions/xep-0033.html#addr-type-bcc
+# "This means that the server MUST remove these addresses before the stanza is delivered to anyone other than the given bcc addressee or the multicast service of the bcc addressee."
+#
+# http://xmpp.org/extensions/xep-0033.html#multicast
+# "Each 'bcc' recipient MUST receive only the <address type='bcc'/> associated with that addressee."
+
+# TODO: fix Prosody "addressing" plugin to determine itself if remote servers supports this XEP
+
+
+NS_XMPP_CLIENT = "jabber:client"
+NS_ADDRESS = "http://jabber.org/protocol/address"
+ATTRIBUTES = ["jid", "uri", "node", "desc", "delivered", "type"]
+ADDRESS_TYPES = ["to", "cc", "bcc", "replyto", "replyroom", "noreply"]
+
+PLUGIN_INFO = {
+    C.PI_NAME: "Extended Stanza Addressing Protocol Plugin",
+    C.PI_IMPORT_NAME: "XEP-0033",
+    C.PI_TYPE: "XEP",
+    C.PI_PROTOCOLS: ["XEP-0033"],
+    C.PI_DEPENDENCIES: [],
+    C.PI_MAIN: "XEP_0033",
+    C.PI_HANDLER: "yes",
+    C.PI_DESCRIPTION: _("""Implementation of Extended Stanza Addressing"""),
+}
+
+
+class XEP_0033(object):
+    """
+    Implementation for XEP 0033
+    """
+
+    def __init__(self, host):
+        log.info(_("Extended Stanza Addressing plugin initialization"))
+        self.host = host
+        self.internal_data = {}
+        host.trigger.add(
+            "sendMessage", self.send_message_trigger, trigger.TriggerManager.MIN_PRIORITY
+        )
+        host.trigger.add("message_received", self.message_received_trigger)
+
+    def send_message_trigger(
+        self, client, mess_data, pre_xml_treatments, post_xml_treatments
+    ):
+        """Process the XEP-0033 related data to be sent"""
+        profile = client.profile
+
+        def treatment(mess_data):
+            if not "address" in mess_data["extra"]:
+                return mess_data
+
+            def disco_callback(entities):
+                if not entities:
+                    log.warning(
+                        _("XEP-0033 is being used but the server doesn't support it!")
+                    )
+                    raise failure.Failure(
+                        exceptions.CancelError("Cancelled by XEP-0033")
+                    )
+                if mess_data["to"] not in entities:
+                    expected = _(" or ").join([entity.userhost() for entity in entities])
+                    log.warning(
+                        _(
+                            "Stanzas using XEP-0033 should be addressed to %(expected)s, not %(current)s!"
+                        )
+                        % {"expected": expected, "current": mess_data["to"]}
+                    )
+                    log.warning(
+                        _(
+                            "TODO: addressing has been fixed by the backend... fix it in the frontend!"
+                        )
+                    )
+                    mess_data["to"] = list(entities)[0].userhostJID()
+                element = mess_data["xml"].addElement("addresses", NS_ADDRESS)
+                entries = [
+                    entry.split(":")
+                    for entry in mess_data["extra"]["address"].split("\n")
+                    if entry != ""
+                ]
+                for type_, jid_ in entries:
+                    element.addChild(
+                        domish.Element(
+                            (None, "address"), None, {"type": type_, "jid": jid_}
+                        )
+                    )
+                # when the prosody plugin is completed, we can immediately return mess_data from here
+                self.send_and_store_message(mess_data, entries, profile)
+                log.debug("XEP-0033 took over")
+                raise failure.Failure(exceptions.CancelError("Cancelled by XEP-0033"))
+
+            d = self.host.find_features_set(client, [NS_ADDRESS])
+            d.addCallbacks(disco_callback, lambda __: disco_callback(None))
+            return d
+
+        post_xml_treatments.addCallback(treatment)
+        return True
+
+    def send_and_store_message(self, mess_data, entries, profile):
+        """Check if target servers support XEP-0033, send and store the messages
+        @return: a friendly failure to let the core know that we sent the message already
+
+        Later we should be able to remove this method because:
+        # XXX: sending the messages should be done by the local server
+        # FIXME: for now we duplicate the messages in the history for each recipient, this should change
+        # FIXME: for now we duplicate the echoes to the sender, this should also change
+        Ideas:
+        - fix Prosody plugin to check if target server support the feature
+        - redesign the database to save only one entry to the database
+        - change the message_new signal to eventually pass more than one recipient
+        """
+        client = self.host.get_client(profile)
+
+        def send(mess_data, skip_send=False):
+            d = defer.Deferred()
+            if not skip_send:
+                d.addCallback(
+                    lambda ret: defer.ensureDeferred(client.send_message_data(ret))
+                )
+            d.addCallback(
+                lambda ret: defer.ensureDeferred(client.message_add_to_history(ret))
+            )
+            d.addCallback(client.message_send_to_bridge)
+            d.addErrback(lambda failure: failure.trap(exceptions.CancelError))
+            return d.callback(mess_data)
+
+        def disco_callback(entities, to_jid_s):
+            history_data = copy.deepcopy(mess_data)
+            history_data["to"] = JID(to_jid_s)
+            history_data["xml"]["to"] = to_jid_s
+            if entities:
+                if entities not in self.internal_data[timestamp]:
+                    sent_data = copy.deepcopy(mess_data)
+                    sent_data["to"] = JID(JID(to_jid_s).host)
+                    sent_data["xml"]["to"] = JID(to_jid_s).host
+                    send(sent_data)
+                    self.internal_data[timestamp].append(entities)
+                # we still need to fill the history and signal the echo...
+                send(history_data, skip_send=True)
+            else:
+                # target server misses the addressing feature
+                send(history_data)
+
+        def errback(failure, to_jid):
+            disco_callback(None, to_jid)
+
+        timestamp = time()
+        self.internal_data[timestamp] = []
+        defer_list = []
+        for type_, jid_ in entries:
+            d = defer.Deferred()
+            d.addCallback(
+                self.host.find_features_set, client=client, jid_=JID(JID(jid_).host)
+            )
+            d.addCallbacks(
+                disco_callback, errback, callbackArgs=[jid_], errbackArgs=[jid_]
+            )
+            d.callback([NS_ADDRESS])
+            defer_list.append(d)
+        d = defer.Deferred().addCallback(lambda __: self.internal_data.pop(timestamp))
+        defer.DeferredList(defer_list).chainDeferred(d)
+
+    def message_received_trigger(self, client, message, post_treat):
+        """In order to save the addressing information in the history"""
+
+        def post_treat_addr(data, addresses):
+            data["extra"]["addresses"] = ""
+            for address in addresses:
+                # Depending how message has been constructed, we could get here
+                # some noise like "\n        " instead of an address element.
+                if isinstance(address, domish.Element):
+                    data["extra"]["addresses"] += "%s:%s\n" % (
+                        address["type"],
+                        address["jid"],
+                    )
+            return data
+
+        try:
+            addresses = next(message.elements(NS_ADDRESS, "addresses"))
+        except StopIteration:
+            pass  # no addresses
+        else:
+            post_treat.addCallback(post_treat_addr, addresses.children)
+        return True
+
+    def get_handler(self, client):
+        return XEP_0033_handler(self, client.profile)
+
+
+@implementer(iwokkel.IDisco)
+class XEP_0033_handler(XMPPHandler):
+
+    def __init__(self, plugin_parent, profile):
+        self.plugin_parent = plugin_parent
+        self.host = plugin_parent.host
+        self.profile = profile
+
+    def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
+        return [disco.DiscoFeature(NS_ADDRESS)]
+
+    def getDiscoItems(self, requestor, target, nodeIdentifier=""):
+        return []