Mercurial > libervia-backend
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()