diff --git a/libervia/backend/plugins/plugin_xep_0077.py b/libervia/backend/plugins/plugin_xep_0077.py
index cf07af16..97bb82f9 100644
--- a/libervia/backend/plugins/plugin_xep_0077.py
+++ b/libervia/backend/plugins/plugin_xep_0077.py
@@ -17,34 +17,56 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from twisted.words.protocols.jabber import jid, xmlstream, client, error as jabber_error
+from typing import Awaitable, Callable, NamedTuple, cast
+
from twisted.internet import defer, reactor, ssl
-from wokkel import data_form
-from libervia.backend.core.i18n import _
-from libervia.backend.core.constants import Const as C
+from twisted.internet.interfaces import IReactorCore
+from twisted.python.failure import Failure
+from twisted.words.protocols.jabber import (
+ client,
+ error as jabber_error,
+ jid,
+ xmlstream as xmlstream_mod,
+)
+from twisted.words.xish import domish
+from wokkel import data_form, disco, iwokkel
+from zope.interface import implementer
+
from libervia.backend.core import exceptions
+from libervia.backend.core.constants import Const as C
+from libervia.backend.core.i18n import _
from libervia.backend.core.log import getLogger
from libervia.backend.core.xmpp import SatXMPPEntity
from libervia.backend.tools import xml_tools
+from libervia.backend.tools.utils import as_deferred, ensure_deferred
log = getLogger(__name__)
-NS_REG = "jabber:iq:register"
+NS_IQ_REGISTER = "jabber:iq:register"
+IQ_REGISTER_REQUEST = f'{C.IQ_GET}/query[@xmlns="{NS_IQ_REGISTER}"]'
+IQ_SUBMIT_REQUEST = f'{C.IQ_SET}/query[@xmlns="{NS_IQ_REGISTER}"]'
PLUGIN_INFO = {
- C.PI_NAME: "XEP 0077 Plugin",
+ C.PI_NAME: "In-Band Registration",
C.PI_IMPORT_NAME: "XEP-0077",
C.PI_TYPE: "XEP",
+ C.PI_MODES: C.PLUG_MODE_BOTH,
C.PI_PROTOCOLS: ["XEP-0077"],
C.PI_DEPENDENCIES: [],
C.PI_MAIN: "XEP_0077",
- C.PI_DESCRIPTION: _("""Implementation of in-band registration"""),
+ C.PI_DESCRIPTION: _("""Implementation of in-band registration."""),
+ C.PI_HANDLER: C.BOOL_TRUE,
}
# FIXME: this implementation is incomplete
-class RegisteringAuthenticator(xmlstream.ConnectAuthenticator):
+class RegistrationHandlers(NamedTuple):
+ form_handler: Callable
+ submit_handler: Callable
+
+
+class RegisteringAuthenticator(xmlstream_mod.ConnectAuthenticator):
# FIXME: request IQ is not send to check available fields,
# while XEP recommand to use it
# FIXME: doesn't handle data form or oob
@@ -52,28 +74,28 @@ class RegisteringAuthenticator(xmlstream.ConnectAuthenticator):
def __init__(self, jid_, password, email=None, check_certificate=True):
log.debug(_("Registration asked for {jid}").format(jid=jid_))
- xmlstream.ConnectAuthenticator.__init__(self, jid_.host)
+ xmlstream_mod.ConnectAuthenticator.__init__(self, jid_.host)
self.jid = jid_
self.password = password
self.email = email
self.check_certificate = check_certificate
self.registered = defer.Deferred()
- def associateWithStream(self, xs):
- xmlstream.ConnectAuthenticator.associateWithStream(self, xs)
- xs.addObserver(xmlstream.STREAM_AUTHD_EVENT, self.register)
+ def associateWithStream(self, xmlstream):
+ xmlstream_mod.ConnectAuthenticator.associateWithStream(self, xmlstream)
+ xmlstream.addObserver(xmlstream_mod.STREAM_AUTHD_EVENT, self.register)
- xs.initializers = [client.CheckVersionInitializer(xs)]
+ xmlstream.initializers = [client.CheckVersionInitializer(xmlstream)]
if self.check_certificate:
tls_required, configurationForTLS = True, None
else:
tls_required = False
configurationForTLS = ssl.CertificateOptions(trustRoot=None)
- tls_init = xmlstream.TLSInitiatingInitializer(
- xs, required=tls_required, configurationForTLS=configurationForTLS
+ tls_init = xmlstream_mod.TLSInitiatingInitializer(
+ xmlstream, required=tls_required, configurationForTLS=configurationForTLS
)
- xs.initializers.append(tls_init)
+ xmlstream.initializers.append(tls_init)
def register(self, xmlstream):
log.debug(
@@ -93,21 +115,23 @@ class RegisteringAuthenticator(xmlstream.ConnectAuthenticator):
def registration_cb(self, answer):
log.debug(_("Registration answer: {}").format(answer.toXml()))
+ assert self.xmlstream is not None
self.xmlstream.sendFooter()
def registration_eb(self, failure_):
log.info(_("Registration failure: {}").format(str(failure_.value)))
+ assert self.xmlstream is not None
self.xmlstream.sendFooter()
raise failure_
-class ServerRegister(xmlstream.XmlStreamFactory):
+class ServerRegister(xmlstream_mod.XmlStreamFactory):
def __init__(self, *args, **kwargs):
- xmlstream.XmlStreamFactory.__init__(self, *args, **kwargs)
- self.addBootstrap(xmlstream.STREAM_END_EVENT, self._disconnected)
+ xmlstream_mod.XmlStreamFactory.__init__(self, *args, **kwargs)
+ self.addBootstrap(xmlstream_mod.STREAM_END_EVENT, self._disconnected)
- def clientConnectionLost(self, connector, reason):
+ def clientConnectionLost(self, connector, unused_reason):
connector.disconnect()
def _disconnected(self, reason):
@@ -121,10 +145,12 @@ class ServerRegister(xmlstream.XmlStreamFactory):
self.authenticator.registered.errback(err)
-class XEP_0077(object):
+class XEP_0077:
+
def __init__(self, host):
- log.info(_("Plugin XEP_0077 initialization"))
+ log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization")
self.host = host
+ host.register_namespace("iq-register", NS_IQ_REGISTER)
host.bridge.add_method(
"in_band_register",
".plugin",
@@ -157,10 +183,162 @@ class XEP_0077(object):
method=self._change_password,
async_=True,
)
+ self.handlers = set()
+
+ def get_handler(self, client: SatXMPPEntity) -> xmlstream_mod.XMPPHandler:
+ return XEP_0077_handler(self)
+
+ def register_handler(
+ self,
+ form_handler: Callable[
+ [SatXMPPEntity, domish.Element],
+ Awaitable[tuple[bool, data_form.Form] | None]
+ | defer.Deferred[tuple[bool, data_form.Form] | None]
+ | tuple[bool, data_form.Form]
+ | None,
+ ],
+ submit_handler: Callable[
+ [SatXMPPEntity, domish.Element, data_form.Form | None],
+ Awaitable[bool | None] | defer.Deferred[bool | None] | bool | None,
+ ],
+ ) -> None:
+ """
+ Register a new handler.
+
+ Mostly useful for component which handle In-Band Registration.
+ @param form_handler: method to call on registration request to get a data form.
+ May be async.
+ The handler must return a data_form.Form instance if it can handle the
+ request, otherwise other handlers will be tried until one returns a data form.
+ @param submit_handler: method to call on registration request to submit the
+ handler.
+ In case of "unregister" request, None will be used instead of a
+ data_form.Form.
+ """
+ self.handlers.add(RegistrationHandlers(form_handler, submit_handler))
+
+ def _on_register_request(self, iq_elt: domish.Element, client: SatXMPPEntity) -> None:
+ defer.ensureDeferred(self._on_request(iq_elt, client, True))
+
+ def _on_submit_request(self, iq_elt: domish.Element, client: SatXMPPEntity) -> None:
+ defer.ensureDeferred(self._on_request(iq_elt, client, False))
+
+ async def _on_request(
+ self, iq_elt: domish.Element, client: SatXMPPEntity, is_register: bool
+ ) -> None:
+ """Handle a register or submit request.
+
+ @param iq_elt: The IQ element of the request.
+ @param client: Client session.
+ @param is_register: Whether this is a register request (True) or a submit request
+ (False).
+ """
+ iq_elt.handled = True
+
+ # Submit request must have a form with submitted values.
+ if is_register:
+ handler_type = "register"
+ submit_form = None
+ else:
+ handler_type = "submit"
+ remove_elt = next(iq_elt.query.elements(NS_IQ_REGISTER, "remove"), None)
+ if remove_elt is not None:
+ # This is a unregister request.
+ submit_form = None
+ else:
+ submit_form = data_form.findForm(iq_elt.query, NS_IQ_REGISTER)
+ if submit_form is None:
+ log.warning(f"Data form not found, invalid request: {iq_elt.toXml()}")
+ client.send(
+ jabber_error.StanzaError(
+ "bad-request", text="No data form found."
+ ).toResponse(iq_elt)
+ )
+ return
+
+ # We look and run relevant handler.
+ for handlers in self.handlers:
+ if is_register:
+ handler = handlers.form_handler
+ handler_call = as_deferred(handler, client, iq_elt)
+ else:
+ handler = handlers.submit_handler
+ handler_call = as_deferred(handler, client, iq_elt, submit_form)
+ try:
+ callback_ret = await handler_call
+ except jabber_error.StanzaError as e:
+ iq_error_elt = e.toResponse(iq_elt)
+ client.send(iq_error_elt)
+ return
+ except exceptions.PasswordError as e:
+ log.warning("Invalid login or password while registering to service.")
+ iq_error_elt = jabber_error.StanzaError(
+ "forbidden", text=str(e)
+ ).toResponse(iq_elt)
+ client.send(iq_error_elt)
+ return
+ except Exception:
+ log.exception(
+ f"Error while handling {handler_type} request with {handler}, "
+ "ignoring it."
+ )
+ continue
+ if callback_ret is not None:
+ if is_register:
+ try:
+ registered, registration_form = callback_ret
+ assert isinstance(registered, bool)
+ assert isinstance(registration_form, data_form.Form)
+ except (TypeError, ValueError, AssertionError) as e:
+ log.warning(
+ f"Invalid return value from {handler}, ignoring it: {e}"
+ )
+ continue
+ # We need to be sure to have the right namespace for the form.
+ registration_form.formNamespace = NS_IQ_REGISTER
+ iq_result_elt = xmlstream_mod.toResponse(iq_elt, "result")
+ query_elt = iq_result_elt.addElement((NS_IQ_REGISTER, "query"))
+
+ if registered:
+ # The requestor is already registered, we indicate it.
+ query_elt.addElement("registered")
+
+ query_elt.addChild(registration_form.toElement())
+ client.send(iq_result_elt)
+ else:
+ if callback_ret is True:
+ if submit_form is None:
+ log.info(f"User {iq_elt['from']} successfully unregistered.")
+ else:
+ log.info(f"User {iq_elt['from']} successfully registered.")
+ else:
+ log.error(
+ f"Unexpected return value from {handler}, was expecting "
+ '"True".'
+ )
+ client.send(
+ jabber_error.StanzaError(
+ "internal-server-error", text="Error in request handler."
+ ).toResponse(iq_elt)
+ )
+ return
+ iq_result_elt = xmlstream_mod.toResponse(iq_elt, "result")
+ client.send(iq_result_elt)
+ break
+ else:
+ log.warning(
+ f"No handler found for in-band registration {handler_type} request: "
+ f"{iq_elt.toXml()}."
+ )
+ iq_error_elt = jabber_error.StanzaError("service-unavailable").toResponse(
+ iq_elt
+ )
+ client.send(iq_error_elt)
+ return
@staticmethod
- def build_register_iq(xmlstream_, jid_, password, email=None):
- iq_elt = xmlstream.IQ(xmlstream_, "set")
+ def build_register_iq(xmlstream, jid_, password, email=None):
+ iq_elt = xmlstream_mod.IQ(xmlstream, "set")
iq_elt["to"] = jid_.host
query_elt = iq_elt.addElement(("jabber:iq:register", "query"))
username_elt = query_elt.addElement("username")
@@ -172,82 +350,116 @@ class XEP_0077(object):
email_elt.addContent(email)
return iq_elt
- def _reg_cb(self, answer, client, post_treat_cb):
- """Called after the first get IQ"""
- try:
- query_elt = next(answer.elements(NS_REG, "query"))
- except StopIteration:
- raise exceptions.DataError("Can't find expected query element")
-
- try:
- x_elem = next(query_elt.elements(data_form.NS_X_DATA, "x"))
- except StopIteration:
- # XXX: it seems we have an old service which doesn't manage data forms
- log.warning(_("Can't find data form"))
- raise exceptions.DataError(
- _("This gateway can't be managed by SàT, sorry :(")
- )
-
- def submit_form(data, profile):
- form_elt = xml_tools.xmlui_result_to_elt(data)
-
- iq_elt = client.IQ()
- iq_elt["id"] = answer["id"]
- iq_elt["to"] = answer["from"]
- query_elt = iq_elt.addElement("query", NS_REG)
- query_elt.addChild(form_elt)
- d = iq_elt.send()
- d.addCallback(self._reg_success, client, post_treat_cb)
- d.addErrback(self._reg_failure, client)
- return d
-
- form = data_form.Form.fromElement(x_elem)
- submit_reg_id = self.host.register_callback(
- submit_form, with_data=True, one_shot=True
- )
- return xml_tools.data_form_2_xmlui(form, submit_reg_id)
-
- def _reg_eb(self, failure, client):
- """Called when something is wrong with registration"""
- log.info(_("Registration failure: %s") % str(failure.value))
- raise failure
-
- def _reg_success(self, answer, client, post_treat_cb):
- log.debug(_("registration answer: %s") % answer.toXml())
- if post_treat_cb is not None:
- post_treat_cb(jid.JID(answer["from"]), client.profile)
- return {}
-
- def _reg_failure(self, failure, client):
- log.info(_("Registration failure: %s") % str(failure.value))
- if failure.value.condition == "conflict":
- raise exceptions.ConflictError(
- _("Username already exists, please choose an other one")
- )
- raise failure
-
def _in_band_register(self, to_jid_s, profile_key=C.PROF_KEY_NONE):
- return self.in_band_register, jid.JID(to_jid_s, profile_key)
+ client = self.host.get_client(profile_key)
+ return defer.ensureDeferred(self.in_band_register(client, jid.JID(to_jid_s)))
- def in_band_register(self, to_jid, post_treat_cb=None, profile_key=C.PROF_KEY_NONE):
- """register to a service
+ async def in_band_register(
+ self,
+ client: SatXMPPEntity,
+ to_jid: jid.JID,
+ post_treat_cb: Callable | None = None,
+ ) -> xml_tools.XMLUI:
+ """Register to a service.
- @param to_jid(jid.JID): jid of the service to register to
+ Send an IQ request to register with the given service and return the registration
+ form as XML UI.
+
+ @param client: client session.
+ @param to_jid: The JID of the service to register to.
+ @param post_treat_cb: A callback function to handle the registration result, if
+ provided.
+ @return: The registration form as XML UI.
"""
# FIXME: this post_treat_cb arguments seems wrong, check it
- client = self.host.get_client(profile_key)
log.debug(_("Asking registration for {}").format(to_jid.full()))
reg_request = client.IQ("get")
reg_request["from"] = client.jid.full()
reg_request["to"] = to_jid.full()
- reg_request.addElement("query", NS_REG)
- d = reg_request.send(to_jid.full()).addCallbacks(
- self._reg_cb,
- self._reg_eb,
- callbackArgs=[client, post_treat_cb],
- errbackArgs=[client],
- )
- return d
+ reg_request.addElement("query", NS_IQ_REGISTER)
+ try:
+ iq_result_elt = await reg_request.send(to_jid.full())
+ except Exception as e:
+ log.warning(_("Registration failure: {}").format(e))
+ raise e
+ else:
+ try:
+ query_elt = next(iq_result_elt.elements(NS_IQ_REGISTER, "query"))
+ except StopIteration:
+ raise exceptions.DataError(
+ f"Can't find expected query element: {iq_result_elt.toXml()}"
+ )
+
+ try:
+ x_elem = next(query_elt.elements(data_form.NS_X_DATA, "x"))
+ except StopIteration:
+ # XXX: it seems we have an old service which doesn't manage data forms
+ log.warning("Can't find data form: {iq_result_elt.toXml()}")
+ raise exceptions.DataError(
+ _(
+ "This gateway is outdated and can't be managed by Libervia, "
+ "sorry :("
+ )
+ )
+
+ form = data_form.Form.fromElement(x_elem)
+ xml_ui = xml_tools.data_form_2_xmlui(form, "")
+ d = xml_tools.deferred_ui(self.host, xml_ui)
+ d.addCallback(
+ self._on_xml_ui_cb,
+ client,
+ iq_result_elt["id"],
+ iq_result_elt["from"],
+ post_treat_cb,
+ )
+ d.addErrback(self._on_xml_ui_eb)
+ d.addTimeout(600, cast(IReactorCore, reactor))
+ return xml_ui
+
+ @ensure_deferred
+ async def _on_xml_ui_cb(
+ self,
+ data: dict,
+ client: SatXMPPEntity,
+ stanza_id: str,
+ to_: str,
+ post_treat_cb: Callable | None,
+ ) -> None:
+ """Handle the XML UI result of a registration form.
+
+ Process the filled registration form (from frontend) and send it back to complete
+ the registration process.
+
+ @param data: The filled registration form as a XMLUI Form result.
+ @param client: Client session.
+ @param stanza_id: The ID of the IQ stanza of the registration process.
+ @param to_: The JID of the service that sent the registration form.
+ @param post_treat_cb: A callback function to handle the registration result, if
+ provided.
+ """
+ form_elt = xml_tools.xmlui_result_to_elt(data, NS_IQ_REGISTER)
+
+ iq_elt = client.IQ()
+ iq_elt["id"] = stanza_id
+ iq_elt["to"] = to_
+ query_elt = iq_elt.addElement("query", NS_IQ_REGISTER)
+ query_elt.addChild(form_elt)
+ try:
+ answer = await iq_elt.send()
+ log.debug(_("registration answer: %s") % answer.toXml())
+ if post_treat_cb is not None:
+ post_treat_cb(jid.JID(answer["from"]), client.profile)
+ except jabber_error.StanzaError as e:
+ log.info(_("Registration failure: {}").format(e))
+ if e.condition == "conflict":
+ raise exceptions.ConflictError(
+ _("Username already exists, please choose another one.")
+ )
+ raise e
+
+ def _on_xml_ui_eb(self, failure_: Failure) -> None:
+ """Handle error during handling of registration form by frontend."""
+ log.warning(f"Error while handling registration form to frontend: {failure_}")
def _register_new_account(self, jid_, password, email, host, port):
kwargs = {}
@@ -260,15 +472,20 @@ class XEP_0077(object):
return self.register_new_account(jid.JID(jid_), password, **kwargs)
def register_new_account(
- self, jid_, password, email=None, host=None, port=C.XMPP_C2S_PORT
+ self,
+ jid_: jid.JID,
+ password: str,
+ email: str | None = None,
+ host: str | None = None,
+ port: int = C.XMPP_C2S_PORT,
):
- """register a new account on a XMPP server
+ """Register a new account on a XMPP server.
- @param jid_(jid.JID): request jid to register
- @param password(unicode): password of the account
- @param email(unicode): email of the account
- @param host(None, unicode): host of the server to register to
- @param port(int): port of the server to register to
+ @param jid_: request jid to register
+ @param password: password of the account
+ @param email: email of the account
+ @param host: host of the server to register to
+ @param port: port of the server to register to
"""
if host is None:
host = self.host.memory.config_get("", "xmpp_domain", "127.0.0.1")
@@ -300,7 +517,7 @@ class XEP_0077(object):
return self.unregister(client, jid.JID(to_jid_s))
def unregister(self, client: SatXMPPEntity, to_jid: jid.JID) -> defer.Deferred:
- """remove registration from a server/service
+ """Remove registration from a server/service.
BEWARE! if you remove registration from profile own server, this will
DELETE THE XMPP ACCOUNT WITHOUT WARNING
@@ -310,9 +527,36 @@ class XEP_0077(object):
iq_elt = client.IQ()
if to_jid is not None:
iq_elt["to"] = to_jid.full()
- query_elt = iq_elt.addElement((NS_REG, "query"))
+ query_elt = iq_elt.addElement((NS_IQ_REGISTER, "query"))
query_elt.addElement("remove")
d = iq_elt.send()
if not to_jid or to_jid == jid.JID(client.jid.host):
d.addCallback(lambda __: client.entity_disconnect())
return d
+
+
+@implementer(iwokkel.IDisco)
+class XEP_0077_handler(xmlstream_mod.XMPPHandler):
+
+ def __init__(self, plugin_parent: XEP_0077) -> None:
+ self.plugin_parent = plugin_parent
+
+ def connectionInitialized(self):
+ client = cast(SatXMPPEntity, self.parent)
+ if client.is_component:
+ self.xmlstream.addObserver(
+ IQ_REGISTER_REQUEST,
+ self.plugin_parent._on_register_request,
+ client=client,
+ )
+ self.xmlstream.addObserver(
+ IQ_SUBMIT_REQUEST,
+ self.plugin_parent._on_submit_request,
+ client=client,
+ )
+
+ def getDiscoInfo(self, requestor, target, nodeIdentifier=""):
+ return [disco.DiscoFeature(NS_IQ_REGISTER)]
+
+ def getDiscoItems(self, requestor, target, nodeIdentifier=""):
+ return []
diff --git a/libervia/backend/plugins/plugin_xep_0100.py b/libervia/backend/plugins/plugin_xep_0100.py
index b0e4169b..cb1d143f 100644
--- a/libervia/backend/plugins/plugin_xep_0100.py
+++ b/libervia/backend/plugins/plugin_xep_0100.py
@@ -17,15 +17,23 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-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.tools import xml_tools
+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
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 @@ PLUGIN_INFO = {
}
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 @@ TYPE_DESCRIPTIONS = {
"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 @@ class XEP_0100(object):
".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 @@ class XEP_0100(object):
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 @@ class XEP_0100(object):
)
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 @@ class XEP_0100(object):
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 @@ class XEP_0100(object):
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 @@ class XEP_0100(object):
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