changeset 4342:17fa953c8cd7

core (types): improve `SatXMPPEntity` core type and type hints.
author Goffi <goffi@goffi.org>
date Mon, 13 Jan 2025 01:23:10 +0100
parents e9971a4b0627
children 627f872bc16e
files libervia/backend/core/core_types.py libervia/backend/core/xmpp.py libervia/backend/memory/disco.py libervia/backend/memory/encryption.py
diffstat 4 files changed, 241 insertions(+), 32 deletions(-) [+]
line wrap: on
line diff
--- a/libervia/backend/core/core_types.py	Wed Dec 11 01:17:09 2024 +0200
+++ b/libervia/backend/core/core_types.py	Mon Jan 13 01:23:10 2025 +0100
@@ -16,8 +16,11 @@
 # 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 abc import ABC, abstractmethod
 from collections import namedtuple
 from typing import Dict, Callable, Optional
+from twisted.internet import defer
+from twisted.python import failure
 from typing_extensions import TypedDict
 
 from twisted.words.protocols.jabber import jid as t_jid
@@ -25,22 +28,7 @@
 from twisted.words.xish import domish
 from wokkel import disco
 
-
-class SatXMPPEntity:
-
-    profile: str
-    jid: t_jid.JID
-    is_admin: bool
-    is_component: bool
-    server_jid: t_jid.JID
-    IQ: Callable[[Optional[str], Optional[int]], xmlstream.IQ]
-    identities: list[disco.DiscoIdentity]
-
-
-class SatXMPPComponent(SatXMPPEntity):
-
-    def is_local(self, jid_: t_jid.JID) -> bool:
-        ...
+from libervia.backend.core import exceptions
 
 
 EncryptionPlugin = namedtuple(
@@ -74,3 +62,205 @@
     },
     total=False,
 )
+
+
+class SatXMPPEntity(ABC):
+    """Base class for Client and Component."""
+
+    profile: str
+    jid: t_jid.JID
+    is_component: bool
+    server_jid: t_jid.JID
+    identities: list[disco.DiscoIdentity]
+    xmlstream: xmlstream.XmlStream
+
+    @classmethod
+    @abstractmethod
+    async def start_connection(cls, host, profile, max_retries):
+        raise NotImplementedError
+
+    @abstractmethod
+    def disconnect_profile(self, reason: failure.Failure | None) -> None:
+        """Disconnect the profile."""
+        raise NotImplementedError
+
+    @abstractmethod
+    def is_connected(self) -> bool:
+        """Return True is client is fully connected
+
+        client is considered fully connected if transport is started and all plugins
+        are initialised
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def entity_disconnect(self) -> defer.Deferred[None]:
+        raise NotImplementedError
+
+    ## sending ##
+
+    @abstractmethod
+    def IQ(self, type_="set", timeout=60) -> xmlstream.IQ:
+        """shortcut to create an IQ element managing deferred
+
+        @param type_(unicode): IQ type ('set' or 'get')
+        @param timeout(None, int): timeout in seconds
+        @return((D)domish.Element: result stanza
+            errback is called if an error stanza is returned
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def sendError(
+        self,
+        iq_elt: domish.Element,
+        condition: str,
+        text: str | None = None,
+        appCondition: str | None = None,
+    ) -> None:
+        """Send error stanza build from iq_elt
+
+        @param iq_elt(domish.Element): initial IQ element
+        @param condition(unicode): error condition
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def generate_message_xml(
+        self,
+        data: MessageData,
+        post_xml_treatments: Optional[defer.Deferred] = None,
+    ) -> MessageData:
+        """Generate <message/> stanza from message data
+
+        @param data: message data
+            domish element will be put in data['xml']
+            following keys are needed:
+                - from
+                - to
+                - uid: can be set to '' if uid attribute is not wanted
+                - message
+                - type
+                - subject
+                - extra
+        @param post_xml_treatments: a Deferred which will be called with data once XML is
+            generated
+        @return: message data
+        """
+        raise NotImplementedError
+
+    @property
+    @abstractmethod
+    def is_admin(self) -> bool:
+        """True if a client is an administrator with extra privileges"""
+        raise NotImplementedError
+
+    @abstractmethod
+    def add_post_xml_callbacks(self, post_xml_treatments) -> None:
+        """Used to add class level callbacks at the end of the workflow
+
+        @param post_xml_treatments(D): the same Deferred as in sendMessage trigger
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    async def a_send(self, obj: domish.Element) -> None:
+        raise NotImplementedError
+
+    @abstractmethod
+    def send(self, obj: domish.Element) -> None:
+        # We need to call super() due to mulitple inheritance: Wokkel XMPPClient or
+        # Component's `send` needs to be called.
+        super().send(obj) # type: ignore
+
+    @abstractmethod
+    async def send_message_data(self, mess_data: MessageData) -> MessageData:
+        """Convenient method to send message data to stream
+
+        This method will send mess_data[u'xml'] to stream, but a trigger is there
+        The trigger can't be cancelled, it's a good place for e2e encryption which
+        don't handle full stanza encryption
+        This trigger can return a Deferred (it's an async_point)
+        @param mess_data(dict): message data as constructed by onMessage workflow
+        @return (dict): mess_data (so it can be used in a deferred chain)
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def sendMessage(
+        self,
+        to_jid,
+        message,
+        subject=None,
+        mess_type="auto",
+        extra=None,
+        uid=None,
+        no_trigger=False,
+    ):
+        """Send a message to an entity
+
+        @param to_jid(jid.JID): destinee of the message
+        @param message(dict): message body, key is the language (use '' when unknown)
+        @param subject(dict): message subject, key is the language (use '' when unknown)
+        @param mess_type(str): one of standard message type (cf RFC 6121 §5.2.2) or:
+            - auto: for automatic type detection
+            - info: for information ("info_type" can be specified in extra)
+        @param extra(dict, None): extra data. Key can be:
+            - info_type: information type, can be
+                TODO
+        @param uid(unicode, None): unique id:
+            should be unique at least in this XMPP session
+            if None, an uuid will be generated
+        @param no_trigger (bool): if True, sendMessage[suffix] trigger will no be used
+            useful when a message need to be sent without any modification
+            ⚠ this will also skip encryption methods!
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def is_message_printable(self, mess_data: MessageData) -> bool:
+        """Return True if a message contain payload to show in frontends"""
+        raise NotImplementedError
+
+    @abstractmethod
+    async def message_add_to_history(self, data):
+        """Store message into database (for local history)
+
+        @param data: message data dictionnary
+        @param client: profile's client
+        """
+        raise NotImplementedError
+
+    @abstractmethod
+    def message_get_bridge_args(self, data):
+        """Generate args to use with bridge from data dict"""
+        raise NotImplementedError
+
+    @abstractmethod
+    def message_send_to_bridge(self, data):
+        """Send message to bridge, so frontends can display it
+
+        @param data: message data dictionnary
+        """
+        raise NotImplementedError
+
+    ## helper methods ##
+
+    @abstractmethod
+    def p(self, plugin_name, missing=exceptions.MissingModule):
+        """Get a plugin if available
+
+        @param plugin_name(str): name of the plugin
+        @param missing(object): value to return if plugin is missing
+            if it is a subclass of Exception, it will be raised with a helping str as
+            argument.
+        @return (object): requested plugin wrapper, or default value
+            The plugin wrapper will return the method with client set as first
+            positional argument
+        """
+        raise NotImplementedError
+
+
+class SatXMPPComponent(SatXMPPEntity):
+
+    def is_local(self, jid_: t_jid.JID) -> bool: ...
--- a/libervia/backend/core/xmpp.py	Wed Dec 11 01:17:09 2024 +0200
+++ b/libervia/backend/core/xmpp.py	Mon Jan 13 01:23:10 2025 +0100
@@ -518,7 +518,7 @@
             xs.add_hook(C.STREAM_HOOK_SEND, hook)
         super(SatXMPPEntity, self)._connected(xs)
 
-    def disconnect_profile(self, reason):
+    def disconnect_profile(self, reason: failure.Failure|None) -> None:
         if self._connected_d is not None:
             self.host_app.bridge.disconnected(
                 self.profile
@@ -581,7 +581,7 @@
             if disconnected_cb is not None:
                 yield disconnected_cb(self)
 
-    def is_connected(self):
+    def is_connected(self) -> bool:
         """Return True is client is fully connected
 
         client is considered fully connected if transport is started and all plugins
@@ -594,7 +594,7 @@
 
         return self._connected_d is not None and transport_connected
 
-    def entity_disconnect(self):
+    def entity_disconnect(self) -> defer.Deferred[None]:
         if not self.host_app.trigger.point("disconnecting", self):
             return
         log.info(_("Disconnecting..."))
@@ -606,7 +606,7 @@
 
     ## sending ##
 
-    def IQ(self, type_="set", timeout=60):
+    def IQ(self, type_="set", timeout=60) -> xmlstream.IQ:
         """shortcut to create an IQ element managing deferred
 
         @param type_(unicode): IQ type ('set' or 'get')
@@ -618,7 +618,13 @@
         iq_elt.timeout = timeout
         return iq_elt
 
-    def sendError(self, iq_elt, condition, text=None, appCondition=None):
+    def sendError(
+            self,
+            iq_elt: domish.Element,
+            condition: str,
+            text: str|None = None,
+            appCondition: str|None = None
+    ) -> None:
         """Send error stanza build from iq_elt
 
         @param iq_elt(domish.Element): initial IQ element
@@ -854,9 +860,9 @@
         """A message sending can be cancelled by a plugin treatment"""
         failure.trap(exceptions.CancelError)
 
-    def is_message_printable(self, mess_data):
+    def is_message_printable(self, mess_data: MessageData) -> bool:
         """Return True if a message contain payload to show in frontends"""
-        return (
+        return bool(
             mess_data["message"]
             or mess_data["subject"]
             or mess_data["extra"].get(C.KEY_ATTACHMENTS)
--- a/libervia/backend/memory/disco.py	Wed Dec 11 01:17:09 2024 +0200
+++ b/libervia/backend/memory/disco.py	Mon Jan 13 01:23:10 2025 +0100
@@ -204,12 +204,18 @@
         disco_infos = await self.get_infos(client, jid_, node)
         return (category, type_) in disco_infos.identities
 
-    def get_infos(self, client, jid_=None, node="", use_cache=True):
+    def get_infos(
+        self,
+        client: SatXMPPEntity,
+        jid_: jid.JID|None=None,
+        node: str="",
+        use_cache: bool=True
+    ) -> defer.Deferred[disco.DiscoInfo]:
         """get disco infos from jid_, filling capability hash if needed
 
         @param jid_: jid of the target, or None for profile's server
-        @param node(unicode): optional node to use for disco request
-        @param use_cache(bool): if True, use cached data if available
+        @param node: optional node to use for disco request
+        @param use_cache: if True, use cached data if available
         @return: a Deferred which fire disco.DiscoInfo
         """
         if jid_ is None:
--- a/libervia/backend/memory/encryption.py	Wed Dec 11 01:17:09 2024 +0200
+++ b/libervia/backend/memory/encryption.py	Mon Jan 13 01:23:10 2025 +0100
@@ -77,7 +77,14 @@
             log.info(_("encryption sessions restored"))
 
     @classmethod
-    def register_plugin(cls, plg_instance, name, namespace, priority=0, directed=False):
+    def register_plugin(
+        cls,
+        plg_instance,
+        name: str,
+        namespace: str,
+        priority: int=0,
+        directed: bool=False
+    ) -> None:
         """Register a plugin handling an encryption algorithm
 
         @param plg_instance(object): instance of the plugin
@@ -92,12 +99,12 @@
                     entity(jid.JID): entity to stop encrypted session with
             if they don't exists, those 2 methods will be ignored.
 
-        @param name(unicode): human readable name of the encryption algorithm
-        @param namespace(unicode): namespace of the encryption algorithm
-        @param priority(int): priority of this plugin to encrypt an message when not
+        @param name: human readable name of the encryption algorithm
+        @param namespace: namespace of the encryption algorithm
+        @param priority: priority of this plugin to encrypt an message when not
             selected manually
-        @param directed(bool): True if this plugin is directed (if it works with one
-                               device only at a time)
+        @param directed: True if this plugin is directed (if it works with one
+            device only at a time)
         """
         existing_ns = set()
         existing_names = set()