diff libervia/backend/plugins/plugin_xep_0100.py @ 4300:7ded09452875

plugin XEP-0077, XEP-0100: Adapt to component, and modernize: - Plugin XEP-0077 can now be used with component, allowing to register methods to return registration form, and to (un)register. - Plugin XEP-0077 now advertises its feature in disco. - Plugin XEP-0100 has been modernized a bit: it is one of the older plugin in Libervia, and it has now some type hints, models and async methods. - Plugin XEP-0100's bridge method `gateways_find` now returns a serialised dict with relevant data. Former XMLUI version has been moved to `gateways_find_xmlui`. rel 449
author Goffi <goffi@goffi.org>
date Fri, 06 Sep 2024 18:01:31 +0200
parents 0d7bb4df2343
children
line wrap: on
line diff
--- a/libervia/backend/plugins/plugin_xep_0100.py	Fri Sep 06 17:45:46 2024 +0200
+++ b/libervia/backend/plugins/plugin_xep_0100.py	Fri Sep 06 18:01:31 2024 +0200
@@ -17,15 +17,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 libervia.backend.core.i18n import _, D_
-from libervia.backend.core.constants import Const as C
+from typing import cast
+from pydantic import BaseModel, Field
+from twisted.internet import defer, reactor
+from twisted.words.protocols.jabber import jid
+from wokkel import disco
+
 from libervia.backend.core import exceptions
+from libervia.backend.core.constants import Const as C
+from libervia.backend.core.core_types import SatXMPPEntity
+from libervia.backend.core.i18n import D_, _
+from libervia.backend.core.log import getLogger
+from libervia.backend.models.core import DiscoIdentity
+from libervia.backend.models.types import StrictJIDType
 from libervia.backend.tools import xml_tools
-from libervia.backend.core.log import getLogger
 
 log = getLogger(__name__)
-from twisted.words.protocols.jabber import jid
-from twisted.internet import reactor, defer
+
 
 PLUGIN_INFO = {
     C.PI_NAME: "Gateways Plugin",
@@ -38,8 +46,12 @@
 }
 
 WARNING_MSG = D_(
-    """Be careful ! Gateways allow you to use an external IM (legacy IM), so you can see your contact as XMPP contacts.
-But when you do this, all your messages go throught the external legacy IM server, it is a huge privacy issue (i.e.: all your messages throught the gateway can be monitored, recorded, analysed by the external server, most of time a private company)."""
+    "Please exercise caution. Gateways facilitate the use of external instant messaging "
+    "platforms (legacy IM), enabling you to view your contacts as XMPP contacts. "
+    "However, this process routes all messages through the external legacy IM server, "
+    "raising significant privacy concerns. Specifically, it is possible for the external "
+    "server, often operated by a private company, to monitor, record, and analyze all "
+    "messages that traverse the gateway."
 )
 
 GATEWAY_TIMEOUT = 10  # time to wait before cancelling a gateway disco info, in seconds
@@ -54,9 +66,22 @@
     "gadu-gadu": D_("Gadu-Gadu"),
     "aim": D_("AOL Instant Messenger"),
     "msn": D_("Windows Live Messenger"),
+    "smtp": D_("Email"),
 }
 
 
+class GatewayData(BaseModel):
+    entity: StrictJIDType
+    identities: list[DiscoIdentity]
+
+
+class FoundGateways(BaseModel):
+    available: list[GatewayData]
+    unavailable: list[StrictJIDType] = Field(
+        description="Gateways registered but not answering."
+    )
+
+
 class XEP_0100(object):
     def __init__(self, host):
         log.info(_("Gateways plugin initialization"))
@@ -69,7 +94,14 @@
             ".plugin",
             in_sign="ss",
             out_sign="s",
-            method=self._find_gateways,
+            method=self._gateways_find,
+        )
+        host.bridge.add_method(
+            "gateways_find_xmlui",
+            ".plugin",
+            in_sign="ss",
+            out_sign="s",
+            method=self._gateways_find_xmlui,
         )
         host.bridge.add_method(
             "gateway_register",
@@ -77,6 +109,7 @@
             in_sign="ss",
             out_sign="s",
             method=self._gateway_register,
+            async_=True,
         )
         self.__menu_id = host.register_callback(self._gateways_menu, with_data=True)
         self.__selected_id = host.register_callback(
@@ -101,7 +134,7 @@
             )
         except RuntimeError:
             raise exceptions.DataError(_("Invalid JID"))
-        d = self.gateways_find(jid_, profile)
+        d = self.gateways_find_raw(jid_, profile)
         d.addCallback(self._gateways_result_2_xmlui, jid_)
         d.addCallback(lambda xmlui: {"xmlui": xmlui.toXml()})
         return d
@@ -158,7 +191,8 @@
         if category != "gateway":
             log.error(
                 _(
-                    'INTERNAL ERROR: identity category should always be "gateway" in _getTypeString, got "%s"'
+                    'INTERNAL ERROR: identity category should always be "gateway" in '
+                    '_getTypeString, got "%s"'
                 )
                 % category
             )
@@ -174,22 +208,83 @@
         self.host.presence_set(jid_, profile_key=profile)
 
     def _gateway_register(self, target_jid_s, profile_key=C.PROF_KEY_NONE):
-        d = self.gateway_register(jid.JID(target_jid_s), profile_key)
+        client = self.host.get_client(profile_key)
+        d = self.gateway_register(client, jid.JID(target_jid_s))
         d.addCallback(lambda xmlui: xmlui.toXml())
         return d
 
-    def gateway_register(self, target_jid, profile_key=C.PROF_KEY_NONE):
+    def gateway_register(
+        self, client: SatXMPPEntity, target_jid: jid.JID
+    ) -> defer.Deferred:
         """Register gateway using in-band registration, then log-in to gateway"""
-        profile = self.host.memory.get_profile_name(profile_key)
-        assert profile
-        d = self.host.plugins["XEP-0077"].in_band_register(
-            target_jid, self._registration_successful, profile
+        return defer.ensureDeferred(
+            self.host.plugins["XEP-0077"].in_band_register(
+                client, target_jid, self._registration_successful
+            )
         )
+
+    def _gateways_find(self, target_jid_s: str, profile_key: str) -> defer.Deferred[str]:
+        client = self.host.get_client(profile_key)
+        target_jid = jid.JID(target_jid_s) if target_jid_s else client.server_jid
+        d = defer.ensureDeferred(self.gateways_find(client, target_jid))
+        d.addCallback(lambda found_gateways: found_gateways.model_dump_json())
+        # The Deferred will actually return a str due to `model_dump_json`, but type
+        # checker doesn't get that.
+        d = cast(defer.Deferred[str], d)
         return d
 
-    def _infos_received(self, dl_result, items, target, client):
-        """Find disco infos about entity, to check if it is a gateway"""
+    async def gateways_find(
+        self, client: SatXMPPEntity, target: jid.JID
+    ) -> FoundGateways:
+        """Find gateways and convert FoundGateways instance."""
+        gateways_data = await self.gateways_find_raw(client, target)
+        available = []
+        unavailable = []
+        for gw_available, data in gateways_data:
+            if gw_available:
+                data = cast(tuple[jid.JID, list[tuple[tuple[str, str], str]]], data)
+                identities = []
+                for (category, type_), name in data[1]:
+                    identities.append(
+                        DiscoIdentity(name=name, category=category, type=type_)
+                    )
+                available.append(GatewayData(entity=data[0], identities=identities))
+            else:
+                disco_item = cast(disco.DiscoItem, data[1])
+                unavailable.append(disco_item.entity)
+        return FoundGateways(available=available, unavailable=unavailable)
 
+    def _gateways_find_xmlui(
+        self, target_jid_s: str, profile_key: str
+    ) -> defer.Deferred[str]:
+        target_jid = jid.JID(target_jid_s)
+        client = self.host.get_client(profile_key)
+        d = defer.ensureDeferred(self.gateways_find_raw(client, target_jid))
+        d.addCallback(self._gateways_result_2_xmlui, target_jid)
+        d.addCallback(lambda xmlui: xmlui.toXml())
+        d = cast(defer.Deferred[str], d)
+        return d
+
+    async def gateways_find_raw(self, client: SatXMPPEntity, target: jid.JID) -> list:
+        """Find gateways in the target JID, using discovery protocol"""
+        log.debug(
+            _("find gateways (target = {target}, profile = {profile})").format(
+                target=target.full(), profile=client.profile
+            )
+        )
+        disco = await client.disco.requestItems(target)
+        if len(disco._items) == 0:
+            log.debug(_("No gateway found"))
+            return []
+
+        defers_ = []
+        for item in disco._items:
+            log.debug(_("item found: {}").format(item.entity))
+            defers_.append(client.disco.requestInfo(item.entity))
+        deferred_list = defer.DeferredList(defers_)
+        dl_result = await deferred_list
+        reactor.callLater(GATEWAY_TIMEOUT, deferred_list.cancel)
+        items = disco._items
         ret = []
         for idx, (success, result) in enumerate(dl_result):
             if not success:
@@ -209,58 +304,20 @@
                     if identity[0] == "gateway"
                 ]
                 if gateways:
-                    log.info(
-                        _("Found gateway [%(jid)s]: %(identity_name)s")
-                        % {
-                            "jid": entity.full(),
-                            "identity_name": " - ".join(
+                    log.debug(
+                        _("Found gateway [{jid}]: {identity_name}").format(
+                            jid=entity.full(),
+                            identity_name=" - ".join(
                                 [gateway[1] for gateway in gateways]
                             ),
-                        }
+                        )
                     )
                     ret.append((success, (entity, gateways)))
                 else:
-                    log.info(
-                        _("Skipping [%(jid)s] which is not a gateway")
-                        % {"jid": entity.full()}
+                    log.debug(
+                        _("Skipping [{jid}] which is not a gateway").format(
+                            jid=entity.full()
+                        )
                     )
-        return ret
-
-    def _items_received(self, disco, target, client):
-        """Look for items with disco protocol, and ask infos for each one"""
-
-        if len(disco._items) == 0:
-            log.debug(_("No gateway found"))
-            return []
 
-        _defers = []
-        for item in disco._items:
-            log.debug(_("item found: %s") % item.entity)
-            _defers.append(client.disco.requestInfo(item.entity))
-        dl = defer.DeferredList(_defers)
-        dl.addCallback(
-            self._infos_received, items=disco._items, target=target, client=client
-        )
-        reactor.callLater(GATEWAY_TIMEOUT, dl.cancel)
-        return dl
-
-    def _find_gateways(self, target_jid_s, profile_key):
-        target_jid = jid.JID(target_jid_s)
-        profile = self.host.memory.get_profile_name(profile_key)
-        if not profile:
-            raise exceptions.ProfileUnknownError
-        d = self.gateways_find(target_jid, profile)
-        d.addCallback(self._gateways_result_2_xmlui, target_jid)
-        d.addCallback(lambda xmlui: xmlui.toXml())
-        return d
-
-    def gateways_find(self, target, profile):
-        """Find gateways in the target JID, using discovery protocol"""
-        client = self.host.get_client(profile)
-        log.debug(
-            _("find gateways (target = %(target)s, profile = %(profile)s)")
-            % {"target": target.full(), "profile": profile}
-        )
-        d = client.disco.requestItems(target)
-        d.addCallback(self._items_received, target=target, client=client)
-        return d
+        return ret