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
This commit is contained in:
Goffi 2024-09-06 18:01:31 +02:00
commit c0a9fa611a

View file

@ -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.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