- 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
323 lines
12 KiB
Python
323 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
|
|
|
|
# SAT plugin for managing gateways (xep-0100)
|
|
# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# 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 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
|
|
|
|
log = getLogger(__name__)
|
|
|
|
|
|
PLUGIN_INFO = {
|
|
C.PI_NAME: "Gateways Plugin",
|
|
C.PI_IMPORT_NAME: "XEP-0100",
|
|
C.PI_TYPE: "XEP",
|
|
C.PI_PROTOCOLS: ["XEP-0100"],
|
|
C.PI_DEPENDENCIES: ["XEP-0077"],
|
|
C.PI_MAIN: "XEP_0100",
|
|
C.PI_DESCRIPTION: _("""Implementation of Gateways protocol"""),
|
|
}
|
|
|
|
WARNING_MSG = D_(
|
|
"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
|
|
|
|
TYPE_DESCRIPTIONS = {
|
|
"irc": D_("Internet Relay Chat"),
|
|
"xmpp": D_("XMPP"),
|
|
"qq": D_("Tencent QQ"),
|
|
"simple": D_("SIP/SIMPLE"),
|
|
"icq": D_("ICQ"),
|
|
"yahoo": D_("Yahoo! Messenger"),
|
|
"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"))
|
|
self.host = host
|
|
self.__gateways = (
|
|
{}
|
|
) # dict used to construct the answer to gateways_find. Key = target jid
|
|
host.bridge.add_method(
|
|
"gateways_find",
|
|
".plugin",
|
|
in_sign="ss",
|
|
out_sign="s",
|
|
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",
|
|
".plugin",
|
|
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(
|
|
self._gateway_selected_cb, with_data=True
|
|
)
|
|
host.import_menu(
|
|
(D_("Service"), D_("Gateways")),
|
|
self._gateways_menu,
|
|
security_limit=1,
|
|
help_string=D_("Find gateways"),
|
|
)
|
|
|
|
def _gateways_menu(self, data, profile):
|
|
"""XMLUI activated by menu: return Gateways UI
|
|
|
|
@param profile: %(doc_profile)s
|
|
"""
|
|
client = self.host.get_client(profile)
|
|
try:
|
|
jid_ = jid.JID(
|
|
data.get(xml_tools.form_escape("external_jid"), client.jid.host)
|
|
)
|
|
except RuntimeError:
|
|
raise exceptions.DataError(_("Invalid JID"))
|
|
d = self.gateways_find_raw(jid_, profile)
|
|
d.addCallback(self._gateways_result_2_xmlui, jid_)
|
|
d.addCallback(lambda xmlui: {"xmlui": xmlui.toXml()})
|
|
return d
|
|
|
|
def _gateways_result_2_xmlui(self, result, entity):
|
|
xmlui = xml_tools.XMLUI(title=_("Gateways manager (%s)") % entity.full())
|
|
xmlui.addText(_(WARNING_MSG))
|
|
xmlui.addDivider("dash")
|
|
adv_list = xmlui.change_container(
|
|
"advanced_list",
|
|
columns=3,
|
|
selectable="single",
|
|
callback_id=self.__selected_id,
|
|
)
|
|
for success, gateway_data in result:
|
|
if not success:
|
|
fail_cond, disco_item = gateway_data
|
|
xmlui.addJid(disco_item.entity)
|
|
xmlui.addText(_("Failed (%s)") % fail_cond)
|
|
xmlui.addEmpty()
|
|
else:
|
|
jid_, data = gateway_data
|
|
for datum in data:
|
|
identity, name = datum
|
|
adv_list.set_row_index(jid_.full())
|
|
xmlui.addJid(jid_)
|
|
xmlui.addText(name)
|
|
xmlui.addText(self._get_identity_desc(identity))
|
|
adv_list.end()
|
|
xmlui.addDivider("blank")
|
|
xmlui.change_container("advanced_list", columns=3)
|
|
xmlui.addLabel(_("Use external XMPP server"))
|
|
xmlui.addString("external_jid")
|
|
xmlui.addButton(self.__menu_id, _("Go !"), fields_back=("external_jid",))
|
|
return xmlui
|
|
|
|
def _gateway_selected_cb(self, data, profile):
|
|
try:
|
|
target_jid = jid.JID(data["index"])
|
|
except (KeyError, RuntimeError):
|
|
log.warning(_("No gateway index selected"))
|
|
return {}
|
|
|
|
d = self.gateway_register(target_jid, profile)
|
|
d.addCallback(lambda xmlui: {"xmlui": xmlui.toXml()})
|
|
return d
|
|
|
|
def _get_identity_desc(self, identity):
|
|
"""Return a human readable description of identity
|
|
@param identity: tuple as returned by Disco identities (category, type)
|
|
|
|
"""
|
|
category, type_ = identity
|
|
if category != "gateway":
|
|
log.error(
|
|
_(
|
|
'INTERNAL ERROR: identity category should always be "gateway" in '
|
|
'_getTypeString, got "%s"'
|
|
)
|
|
% category
|
|
)
|
|
try:
|
|
return _(TYPE_DESCRIPTIONS[type_])
|
|
except KeyError:
|
|
return _("Unknown IM")
|
|
|
|
def _registration_successful(self, jid_, profile):
|
|
"""Called when in_band registration is ok, we must now follow the rest of procedure"""
|
|
log.debug(_("Registration successful, doing the rest"))
|
|
self.host.contact_add(jid_, profile_key=profile)
|
|
self.host.presence_set(jid_, profile_key=profile)
|
|
|
|
def _gateway_register(self, target_jid_s, profile_key=C.PROF_KEY_NONE):
|
|
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, client: SatXMPPEntity, target_jid: jid.JID
|
|
) -> defer.Deferred:
|
|
"""Register gateway using in-band registration, then log-in to gateway"""
|
|
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
|
|
|
|
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:
|
|
if isinstance(result.value, defer.CancelledError):
|
|
msg = _("Timeout")
|
|
else:
|
|
try:
|
|
msg = result.value.condition
|
|
except AttributeError:
|
|
msg = str(result)
|
|
ret.append((success, (msg, items[idx])))
|
|
else:
|
|
entity = items[idx].entity
|
|
gateways = [
|
|
(identity, result.identities[identity])
|
|
for identity in result.identities
|
|
if identity[0] == "gateway"
|
|
]
|
|
if gateways:
|
|
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.debug(
|
|
_("Skipping [{jid}] which is not a gateway").format(
|
|
jid=entity.full()
|
|
)
|
|
)
|
|
|
|
return ret
|