Add XEP-0100 (Gateway Interaction) plugin

Remove usused prompt_future attribute

Add plugin_end

Update with mathieui's comments

Add option to transfer messages from unregistered users

XEP 0100 plugin
This commit is contained in:
Nicoco K 2021-03-02 18:54:22 +01:00 committed by mathieui
parent 2dac77e680
commit 9b5f3d9df0
5 changed files with 689 additions and 0 deletions

View file

@ -0,0 +1,9 @@
XEP-0106: Gateway interaction
=============================
.. module:: slixmpp.plugins.xep_0100
.. autoclass:: XEP_0100
:members:
:exclude-members: session_bind, plugin_init, plugin_end

View file

@ -42,6 +42,7 @@ __all__ = [
'xep_0092', # Software Version
# 'xep_0095', # Legacy Stream Initiation. Dont automatically load
# 'xep_0096', # Legacy SI File Transfer. Dont automatically load
'xep_0100', # Gateway interaction
'xep_0106', # JID Escaping
'xep_0107', # User Mood
'xep_0108', # User Activity

View file

@ -0,0 +1,6 @@
from slixmpp.plugins.base import register_plugin
from slixmpp.plugins.xep_0100.gateway import XEP_0100, LegacyError
register_plugin(XEP_0100)

View file

@ -0,0 +1,257 @@
import asyncio
import logging
from functools import partial
import typing
from slixmpp import Message, Iq, Presence, JID
from slixmpp.xmlstream.handler import Callback
from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.plugins import BasePlugin
class XEP_0100(BasePlugin):
"""
XEP-0100: Gateway interaction
Does not cover the deprecated Agent Information and 'jabber:iq:gateway' protocols
Events registered by this plugin:
- legacy_login: Jabber user got online or just registered
- legacy_logout: Jabber user got offline or just unregistered
- legacy_presence_unavailable: Jabber user sent an unavailable presence to a legacy contact
- gateway_message: Jabber user sent a direct message to the gateway component
- legacy_message: Jabber user sent a message to the legacy network
Plugin Parameters:
- `component_name`: (str) Name of the entity
- `type`: (str) Type of the gateway identity. Should be the name of the legacy service
- `needs_registration`: (bool) If set to True, messages received from unregistered users will
not be transmitted to the legacy service
API:
- legacy_contact_add(jid, node, ifrom: JID, args: JID): Add contact on the legacy service.
Should raise LegacyError if anything goes wrong in the process.
`ifrom` is the gateway user's JID and `args` is the legacy contact's JID.
- legacy_contact_remove(jid, node, ifrom: JID, args: JID): Remove a contact.
"""
name = "xep_0100"
description = "XEP-0100: Gateway interaction"
dependencies = {
"xep_0030", # Service discovery
"xep_0077", # In band registration
}
default_config = {
"component_name": "SliXMPP gateway",
"type": "xmpp",
"needs_registration": True,
}
def plugin_init(self):
if not self.xmpp.is_component:
log.error("Only components can be gateways, aborting plugin load")
return
self.xmpp["xep_0030"].add_identity(
name=self.component_name, category="gateway", itype=self.type
)
self.api.register(self._legacy_contact_remove, "legacy_contact_remove")
self.api.register(self._legacy_contact_add, "legacy_contact_add")
# Without that BaseXMPP sends unsub/unavailable on sub requests and we don't want that
self.xmpp.client_roster.auto_authorize = True
self.xmpp.client_roster.auto_subscribe = False
self.xmpp.add_event_handler("user_register", self.on_user_register)
self.xmpp.add_event_handler("user_unregister", self.on_user_unregister)
self.xmpp.add_event_handler("presence_available", self.on_presence_available)
self.xmpp.add_event_handler(
"presence_unavailable", self.on_presence_unavailable
)
self.xmpp.add_event_handler("presence_subscribe", self.on_presence_subscribe)
self.xmpp.add_event_handler(
"presence_unsubscribe", self.on_presence_unsubscribe
)
self.xmpp.add_event_handler("message", self.on_message)
def plugin_end(self):
if not self.xmpp.is_component:
return
self.xmpp.del_event_handler("user_register", self.on_user_register)
self.xmpp.del_event_handler("user_unregister", self.on_user_unregister)
self.xmpp.del_event_handler("presence_available", self.on_presence_available)
self.xmpp.del_event_handler(
"presence_unavailable", self.on_presence_unavailable
)
self.xmpp.del_event_handler("presence_subscribe", self.on_presence_subscribe)
self.xmpp.del_event_handler("message", self.on_message)
self.xmpp.del_event_handler(
"presence_unsubscribe", self.on_presence_unsubscribe
)
async def get_user(self, stanza):
return await self.xmpp["xep_0077"].api["user_get"](None, None, None, stanza)
def send_presence(self, pto, ptype=None, pstatus=None, pfrom=None):
self.xmpp.send_presence(
pfrom=self.xmpp.boundjid.bare,
ptype=ptype,
pto=pto,
pstatus=pstatus,
)
async def on_user_register(self, iq: Iq):
user_jid = iq["from"]
user = await self.get_user(iq)
if user is None: # This should not happen
log.warning(f"{user_jid} has registered but cannot find them in user store")
else:
log.debug(f"Sending subscription request to {user_jid}")
self.xmpp.client_roster.subscribe(user_jid)
def on_user_unregister(self, iq: Iq):
user_jid = iq["from"]
log.debug(f"Sending subscription request to {user_jid}")
self.xmpp.event("legacy_logout", iq)
self.xmpp.client_roster.unsubscribe(iq["from"])
self.xmpp.client_roster.remove(iq["from"])
log.debug(f"roster: {self.xmpp.client_roster}")
async def on_presence_available(self, presence: Presence):
user_jid = presence["from"]
user = await self.get_user(presence)
if user is None:
log.warning(
f"{user_jid} has gotten online but cannot find them in user store"
)
else:
self.xmpp.event("legacy_login", presence)
log.debug(f"roster: {self.xmpp.client_roster}")
self.send_presence(pto=user_jid.bare, ptype="available")
async def on_presence_unavailable(self, presence: Presence):
user_jid = presence["from"]
user = await self.get_user(presence)
if user is None: # This should not happen
log.warning(
f"{user_jid} has gotten offline but but cannot find them in user store"
)
return
if presence["to"] == self.xmpp.boundjid.bare:
self.xmpp.event("legacy_logout", presence)
self.send_presence(pto=user_jid, ptype="unavailable")
else:
self.xmpp.event("legacy_presence_unavailable", presence)
async def _legacy_contact_add(self, jid, node, ifrom, contact_jid: JID):
pass
async def on_presence_subscribe(self, presence: Presence):
user_jid = presence["from"]
user = await self.get_user(presence)
if user is None and self.needs_registration:
return
if presence["to"] == self.xmpp.boundjid.bare:
return
try:
await self.api["legacy_contact_add"](
ifrom=user_jid,
args=presence["to"],
)
except LegacyError:
self.xmpp.send_presence(
pfrom=presence["to"],
ptype="unsubscribed",
pto=user_jid,
)
return
self.xmpp.send_presence(
pfrom=presence["to"],
ptype="subscribed",
pto=user_jid,
)
self.xmpp.send_presence(
pfrom=presence["to"],
pto=user_jid,
)
self.xmpp.send_presence(
pfrom=presence["to"],
ptype="subscribe",
pto=user_jid,
) # TODO: handle resulting subscribed presences
async def on_presence_unsubscribe(self, presence: Presence):
if presence["to"] == self.xmpp.boundjid.bare:
# should we trigger unregistering here?
return
user_jid = presence["from"]
user = await self.get_user(presence)
if user is None:
log.debug("Received remove subscription from unregistered user")
if self.needs_registration:
return
await self.api["legacy_contact_remove"](ifrom=user_jid, args=presence["to"])
for ptype in "unsubscribe", "unsubscribed", "unavailable":
self.xmpp.send_presence(
pfrom=presence["to"],
ptype=ptype,
pto=user_jid,
)
async def _legacy_contact_remove(self, jid, node, ifrom, contact_jid: JID):
pass
async def on_message(self, msg: Message):
if msg["type"] == "groupchat":
return # groupchat messages are out of scope of XEP-0100
if msg["to"] == self.xmpp.boundjid.bare:
# It may be useful to exchange direct messages with the component
self.xmpp.event("gateway_message", msg)
return
if self.needs_registration and await self.get_user(msg) is None:
return
self.xmpp.event("legacy_message", msg)
def transform_legacy_message(
self,
jabber_user_jid: typing.Union[JID, str],
legacy_contact_id: str,
body: str,
mtype: typing.Optional[str] = None,
):
"""
Transform a legacy message to an XMPP message
"""
# Should escaping legacy IDs to valid JID local parts be handled here?
# Maybe by internal API stuff?
self.xmpp.send_message(
mfrom=JID(f"{legacy_contact_id}@{self.xmpp.boundjid.bare}"),
mto=JID(jabber_user_jid).bare,
mbody=body,
mtype=mtype,
)
class LegacyError(Exception):
pass
log = logging.getLogger(__name__)

View file

@ -0,0 +1,416 @@
import unittest
import logging
from slixmpp import JID
from slixmpp.test import SlixTest
from slixmpp.plugins import xep_0100
from slixmpp.plugins.xep_0100 import LegacyError
class TestStreamGateway(SlixTest):
def setUp(self):
self.stream_start(
mode="component",
plugins=["xep_0077", "xep_0100"],
jid="aim.shakespeare.lit",
server="shakespeare.lit",
plugin_config={
"xep_0100": {"component_name": "AIM Gateway", "type": "aim"}
},
)
def next_sent(self):
self.wait_for_send_queue()
sent = self.xmpp.socket.next_sent(timeout=0.5)
if sent is None:
return None
xml = self.parse_xml(sent)
self.fix_namespaces(xml, "jabber:component:accept")
sent = self.xmpp._build_stanza(xml, "jabber:component:accept")
return sent
def testDisco(self):
# https://xmpp.org/extensions/xep-0100.html#example-3
self.recv(
"""
<iq type='get'
from='romeo@montague.lit/orchard'
to='aim.shakespeare.lit'
id='disco1'>
<query xmlns='http://jabber.org/protocol/disco#info'/>
</iq>
"""
)
self.send(
"""
<iq type="result"
from="aim.shakespeare.lit"
to="romeo@montague.lit/orchard"
id="disco1">
<query xmlns="http://jabber.org/protocol/disco#info">
<identity category="gateway" type="aim" name="AIM Gateway" />
<feature var="jabber:iq:register" />
<feature var="jabber:x:data" />
<feature var="jabber:iq:oob" />
<feature var="jabber:x:oob" />
</query>
</iq>
"""
)
def testRegister(self):
event_result = {}
def legacy_login(iq):
event_result["user"] = iq["from"]
self.xmpp.add_event_handler("legacy_login", legacy_login)
# Jabber User sends IQ-set qualified by the 'jabber:iq:register' namespace to Gateway,
# containing information required to register.
# https://xmpp.org/extensions/xep-0100.html#example-7
self.recv(
"""
<iq type='set'
from='romeo@montague.lit/orchard'
to='aim.shakespeare.lit'
id='reg2'>
<query xmlns='jabber:iq:register'>
<username>RomeoMyRomeo</username>
<password>ILoveJuliet</password>
</query>
</iq>
"""
)
# Gateway verifies that registration information provided by Jabber User is valid
# (using whatever means appropriate for the Legacy Service) and informs Jabber User of success [A1].
# https://xmpp.org/extensions/xep-0100.html#example-8
self.send(
"""
<iq type='result'
from='aim.shakespeare.lit'
to='romeo@montague.lit/orchard'
id='reg2'/>
"""
)
# Gateway sends subscription request to Jabber User (i.e., by sending a presence stanza
# of type "subscribe" to Jabber User's bare JID).
# https://xmpp.org/extensions/xep-0100.html#example-11
sent = self.next_sent()
self.check(
sent, "/presence@type=subscribe@from=aim.shakespeare.lit", "stanzapath"
)
self.assertTrue(
sent["to"] == "romeo@montague.lit"
) # cannot use stanzapath because of @
# Jabber User's client SHOULD approve the subscription request (i.e., by sending a presence stanza
# of type "subscribed" to Gateway).
self.recv(
"""
<presence type='subscribed'
from='romeo@montague.lit'
to='aim.shakespeare.lit'/>
"""
)
# Jabber User sends subscription request to Gateway (i.e., by sending a presence stanza
# of type "subscribe" to Gateway).
self.recv(
"""
<presence type='subscribe'
from='romeo@montague.lit'
to='aim.shakespeare.lit'/>
"""
)
# Gateway sends approves subscription request (i.e., by sending a presence stanza of type
# "subscribed" to Jabber User's bare JID).
sent = self.next_sent()
self.check(
sent, "/presence@type=subscribed@from=aim.shakespeare.lit", "stanzapath"
)
self.assertTrue(
sent["to"] == "romeo@montague.lit"
) # cannot use stanzapath because of @
self.assertTrue(
self.xmpp.client_roster["romeo@montague.lit"]["subscription"] == "both"
)
self.recv(
"""
<presence from='romeo@montague.lit/orchard'
to='aim.shakespeare.lit'/>
"""
)
self.assertTrue(event_result["user"] == "romeo@montague.lit/orchard")
def testBadCredentials(self):
def raise_v(*a, **kwa):
raise ValueError("Not good")
self.xmpp["xep_0077"].api.register(raise_v, "user_validate")
self.recv(
"""
<iq type='set'
from='romeo@montague.lit/orchard'
to='aim.shakespeare.lit'
id='reg2'>
<query xmlns='jabber:iq:register'>
<username>RomeoMyRomeo</username>
<password>ILoveJuliet</password>
</query>
</iq>
"""
)
# xmlns="jabber:client" in error substanza, bug in XEP-0077 plugin or OK?
self.send(
"""
<iq type='error'
from='aim.shakespeare.lit'
to='romeo@montague.lit/orchard'
id='reg2'>
<query xmlns='jabber:iq:register'>
<username>RomeoMyRomeo</username>
<password>ILoveJuliet</password>
</query>
<error code='406' type='modify' xmlns="jabber:client">
<not-acceptable
xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Not good</text>
</error>
</iq>
""",
use_values=False,
)
def testLogin(self):
event_result = {}
def legacy_login(presence):
event_result["user"] = presence["from"]
self.xmpp.add_event_handler("legacy_login", legacy_login)
self.xmpp["xep_0077"].api["user_validate"](
None,
None,
JID("romeo@montague.lit"),
{"username": "RomeoMyRomeo", "password": "ILoveJuliet"},
)
# Jabber User sends available presence broadcast to Server or sends
# directed presence to Gateway or a Legacy User.
# https://xmpp.org/extensions/xep-0100.html#example-26
self.recv(
"""
<presence from='romeo@montague.lit/orchard'
to='juliet@aim.shakespeare.lit'/>
<presence from='romeo@montague.lit/orchard'
to='aim.shakespeare.lit'/>
"""
)
# Gateway sends presence stanza to Jabber User expressing availability.
# https://xmpp.org/extensions/xep-0100.html#example-27
self.send(
"""
<presence from='aim.shakespeare.lit'
to='romeo@montague.lit'>
<priority>0</priority>
</presence>
"""
)
self.assertTrue(event_result["user"] == "romeo@montague.lit/orchard")
def testLogout(self):
self.add_user()
event_result = {}
def legacy_logout(presence):
event_result["user"] = presence["from"]
self.xmpp.add_event_handler("legacy_logout", legacy_logout)
# Jabber User sends available presence broadcast to Server or sends
# directed presence to Gateway or a Legacy User.
# https://xmpp.org/extensions/xep-0100.html#example-32
self.recv(
"""
<presence type='unavailable'
from='romeo@montague.lit/orchard'
to='aim.shakespeare.lit'/>
"""
)
# Gateway sends presence stanza of type "unavailable" to Jabber User.
# https://xmpp.org/extensions/xep-0100.html#example-33
self.send(
"""
<presence type='unavailable'
from='aim.shakespeare.lit'
to='romeo@montague.lit/orchard'>
<priority>0</priority>
</presence>
"""
)
self.assertTrue(event_result["user"] == "romeo@montague.lit/orchard")
def testAddContact(self):
self.add_user()
# Had to lowercase capuletnurse everywhere
# Jabber User sends presence stanza of type "subscribe" to Legacy User.
self.recv(
"""
<presence type='subscribe'
from='romeo@montague.lit'
to='capuletnurse@aim.shakespeare.lit'/>
"""
)
# If Legacy User approves subscription request, Gateway sends presence stanza of
# type "subscribed" to Jabber User on behalf of Legacy User. [A1]
self.send(
"""
<presence type='subscribed'
from='capuletnurse@aim.shakespeare.lit'
to='romeo@montague.lit'/>
"""
)
# Had to remove the resource here
self.send(
"""
<presence from='capuletnurse@aim.shakespeare.lit'
to='romeo@montague.lit'/>
"""
)
self.send(
"""
<presence type='subscribe'
from='capuletnurse@aim.shakespeare.lit'
to='romeo@montague.lit'/>
"""
)
self.recv(
"""
<presence type='subscribed'
from='romeo@montague.lit'
to='capuletnurse@aim.shakespeare.lit'/>
"""
)
def testAddContactFail(self):
self.add_user()
res = {}
async def legacy_contact_add(jid, node, ifrom, contact_jid):
res.update(**locals())
raise LegacyError
self.xmpp["xep_0100"].api.register(
legacy_contact_add, "legacy_contact_add"
)
self.recv(
"""
<presence type='subscribe'
from='romeo@montague.lit'
to='juliet@aim.shakespeare.lit'/>
"""
)
self.send(
"""
<presence type='unsubscribed'
from='juliet@aim.shakespeare.lit'
to='romeo@montague.lit'/>
"""
)
self.assertTrue(res["ifrom"] == "romeo@montague.lit")
self.assertTrue(res["contact_jid"] == "juliet@aim.shakespeare.lit")
def testRemoveContact(self):
self.add_user()
result = {}
# Jabber User sends IQ-set qualified by the 'jabber:iq:roster' namespace, containing subscription
# attribute with value of "remove".
async def legacy_contact_remove(jid, node, ifrom, contact_jid):
result.update(**locals())
self.xmpp["xep_0100"].api.register(
legacy_contact_remove, "legacy_contact_remove"
)
# Jabber User sends IQ-set qualified by the 'jabber:iq:roster' namespace, containing subscription
# attribute with value of "remove".
self.recv( # server sends this
"""
<presence type='unsubscribe'
to='capuletnurse@aim.shakespeare.lit'
from='romeo@montague.lit'/>
"""
)
for ptype in "unsubscribe", "unsubscribed", "unavailable":
self.send( # server sends this
f"""
<presence type='{ptype}'
from='capuletnurse@aim.shakespeare.lit'
to='romeo@montague.lit'/>
"""
)
self.assertTrue(result["ifrom"] == "romeo@montague.lit")
self.assertTrue(
result["contact_jid"] == JID("CapuletNurse@aim.shakespeare.lit")
)
def testSendMessage(self):
self.xmpp["xep_0100"].transform_legacy_message(
jabber_user_jid="romeo@montague.lit",
legacy_contact_id="juliet",
body="Art thou not Romeo, and a Montague?",
)
self.send(
"""
<message from='juliet@aim.shakespeare.lit'
to='romeo@montague.lit'>
<body>Art thou not Romeo, and a Montague?</body>
</message>
"""
)
def testLegacyMessage(self):
self.add_user()
result = {}
def legacy_message(msg):
result["msg"] = msg
self.xmpp.add_event_handler("legacy_message", legacy_message)
self.recv(
"""
<message to='juliet@aim.shakespeare.lit'
from='romeo@montague.lit'>
<body>Something shakespearian</body>
</message>
"""
)
self.wait_for_send_queue()
self.assertTrue(result["msg"]["from"] == "romeo@montague.lit")
self.assertTrue(result["msg"]["to"] == "juliet@aim.shakespeare.lit")
def testPluginEnd(self):
exc = False
try:
self.xmpp.plugin.disable("xep_0100")
except Exception:
exc = True
self.assertFalse(exc)
def add_user(self):
self.xmpp.loop.run_until_complete(
self.xmpp["xep_0077"].api["user_validate"](
None,
None,
JID("romeo@montague.lit"),
{"username": "RomeoMyRomeo", "password": "ILoveJuliet"},
)
)
# TODO: edit reg
# TODO: unregister
# TODO: login fails
logging.basicConfig(level=logging.DEBUG)
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamGateway)