diff --git a/docs/api/plugins/xep_0100.rst b/docs/api/plugins/xep_0100.rst
new file mode 100644
index 00000000..15c99ed1
--- /dev/null
+++ b/docs/api/plugins/xep_0100.rst
@@ -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
diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py
index 70ec55d2..d087f92b 100644
--- a/slixmpp/plugins/__init__.py
+++ b/slixmpp/plugins/__init__.py
@@ -42,6 +42,7 @@ __all__ = [
'xep_0092', # Software Version
# 'xep_0095', # Legacy Stream Initiation. Don’t automatically load
# 'xep_0096', # Legacy SI File Transfer. Don’t automatically load
+ 'xep_0100', # Gateway interaction
'xep_0106', # JID Escaping
'xep_0107', # User Mood
'xep_0108', # User Activity
diff --git a/slixmpp/plugins/xep_0100/__init__.py b/slixmpp/plugins/xep_0100/__init__.py
new file mode 100644
index 00000000..101b8db0
--- /dev/null
+++ b/slixmpp/plugins/xep_0100/__init__.py
@@ -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)
diff --git a/slixmpp/plugins/xep_0100/gateway.py b/slixmpp/plugins/xep_0100/gateway.py
new file mode 100644
index 00000000..8ff102bb
--- /dev/null
+++ b/slixmpp/plugins/xep_0100/gateway.py
@@ -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__)
diff --git a/tests/test_stream_xep_0100.py b/tests/test_stream_xep_0100.py
new file mode 100644
index 00000000..24e35f9e
--- /dev/null
+++ b/tests/test_stream_xep_0100.py
@@ -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(
+ """
+
+
+
+ """
+ )
+ self.send(
+ """
+
+
+
+
+
+
+
+
+
+ """
+ )
+
+ 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(
+ """
+
+
+ RomeoMyRomeo
+ ILoveJuliet
+
+
+ """
+ )
+ # 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(
+ """
+
+ """
+ )
+ # 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(
+ """
+
+ """
+ )
+ # Jabber User sends subscription request to Gateway (i.e., by sending a presence stanza
+ # of type "subscribe" to Gateway).
+ self.recv(
+ """
+
+ """
+ )
+ # 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(
+ """
+
+ """
+ )
+ 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(
+ """
+
+
+ RomeoMyRomeo
+ ILoveJuliet
+
+
+ """
+ )
+ # xmlns="jabber:client" in error substanza, bug in XEP-0077 plugin or OK?
+ self.send(
+ """
+
+
+ RomeoMyRomeo
+ ILoveJuliet
+
+
+
+ Not good
+
+
+ """,
+ 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(
+ """
+
+
+ """
+ )
+ # Gateway sends presence stanza to Jabber User expressing availability.
+ # https://xmpp.org/extensions/xep-0100.html#example-27
+ self.send(
+ """
+
+ 0
+
+ """
+ )
+ 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(
+ """
+
+ """
+ )
+ # Gateway sends presence stanza of type "unavailable" to Jabber User.
+ # https://xmpp.org/extensions/xep-0100.html#example-33
+ self.send(
+ """
+
+ 0
+
+ """
+ )
+ 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(
+ """
+
+ """
+ )
+ # If Legacy User approves subscription request, Gateway sends presence stanza of
+ # type "subscribed" to Jabber User on behalf of Legacy User. [A1]
+ self.send(
+ """
+
+ """
+ )
+ # Had to remove the resource here
+ self.send(
+ """
+
+ """
+ )
+ self.send(
+ """
+
+ """
+ )
+ self.recv(
+ """
+
+ """
+ )
+
+ 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(
+ """
+
+ """
+ )
+ self.send(
+ """
+
+ """
+ )
+ 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
+ """
+
+ """
+ )
+ for ptype in "unsubscribe", "unsubscribed", "unavailable":
+ self.send( # server sends this
+ f"""
+
+ """
+ )
+
+ 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(
+ """
+
+ Art thou not Romeo, and a Montague?
+
+ """
+ )
+
+ 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(
+ """
+
+ Something shakespearian
+
+ """
+ )
+ 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)