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