diff --git a/doap.xml b/doap.xml index 20ad179d..6774a786 100644 --- a/doap.xml +++ b/doap.xml @@ -941,6 +941,14 @@ 1.8.6 + + + + complete + 0.1.0 + 1.8.7 + + diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py index 589cef46..d57be35c 100644 --- a/slixmpp/plugins/__init__.py +++ b/slixmpp/plugins/__init__.py @@ -122,6 +122,7 @@ PLUGINS = [ 'xep_0447', # Stateless file sharing 'xep_0461', # Message Replies 'xep_0469', # Bookmarks Pinning + 'xep_0482', # Call Invites 'xep_0490', # Message Displayed Synchronization 'xep_0492', # Chat Notification Settings # Meant to be imported by plugins diff --git a/slixmpp/plugins/xep_0482/__init__.py b/slixmpp/plugins/xep_0482/__init__.py new file mode 100644 index 00000000..66105db0 --- /dev/null +++ b/slixmpp/plugins/xep_0482/__init__.py @@ -0,0 +1,11 @@ +# Slixmpp: The Slick XMPP Library +# Copyright (C) 2025 Mathieu Pasquet +# This file is part of Slixmpp. +# See the file LICENSE for copying permissio +from slixmpp.plugins.base import register_plugin + +from slixmpp.plugins.xep_0482 import stanza +from slixmpp.plugins.xep_0482.call_invites import XEP_0482 + + +register_plugin(XEP_0482) diff --git a/slixmpp/plugins/xep_0482/call_invites.py b/slixmpp/plugins/xep_0482/call_invites.py new file mode 100644 index 00000000..a118f3eb --- /dev/null +++ b/slixmpp/plugins/xep_0482/call_invites.py @@ -0,0 +1,55 @@ +# Slixmpp: The Slick XMPP Library +# Copyright (C) 2025 Mathieu Pasquet +# This file is part of Slixmpp. +# See the file LICENSE for copying permissio +import logging +from typing import Optional + +from slixmpp.stanza import Message +from slixmpp.jid import JID +from slixmpp.xmlstream.handler import Callback +from slixmpp.xmlstream.matcher import StanzaPath +from slixmpp.xmlstream import register_stanza_plugin +from slixmpp.plugins import BasePlugin +from slixmpp.plugins.xep_0482 import stanza + + +log = logging.getLogger(__name__) + + +class XEP_0482(BasePlugin): + + """ + XEP-0482: Call Invites + + This plugin defines the stanza elements for Call Invites, as well as new + events: + + - `call-invite` + - `call-reject` + - `call-retract` + - `call-leave` + - `call-left` + """ + + name = 'xep_0482' + description = 'XEP-0482: Call Invites' + dependencies = set() + stanza = stanza + + def plugin_init(self): + stanza.register_plugins() + + for event in ('invite', 'reject', 'retract', 'leave', 'left'): + self.xmpp.register_handler( + Callback(f'Call {event}', + StanzaPath(f'message/call-{event}'), + self._handle_event)) + def _handle_event(self, message): + for event in ('invite', 'reject', 'retract', 'leave', 'left'): + if message.get_plugin(f'call-{event}', check=True): + self.xmpp.event(f'call-{event}') + + def plugin_end(self): + for event in ('invite', 'reject', 'retract', 'leave', 'left'): + self.xmpp.remove_handler(f'Call {event}') diff --git a/slixmpp/plugins/xep_0482/stanza.py b/slixmpp/plugins/xep_0482/stanza.py new file mode 100644 index 00000000..3ca3c630 --- /dev/null +++ b/slixmpp/plugins/xep_0482/stanza.py @@ -0,0 +1,102 @@ +# Slixmpp: The Slick XMPP Library +# Copyright (C) 2025 Mathieu Pasquet +# This file is part of Slixmpp. +# See the file LICENSE for copying permission + +from typing import Tuple, List, Optional +from slixmpp import Message +from slixmpp.jid import JID +from slixmpp.xmlstream import ElementBase, register_stanza_plugin + +NS = 'urn:xmpp:call-invites:0' + + +class Jingle(ElementBase): + name = 'jingle' + namespace = NS + plugin_attrib = 'jingle' + plugin_multi_attrib = 'jingles' + interfaces = {'sid', 'jid'} + + def set_jid(self, value: JID) -> None: + if not isinstance(value, JID): + try: + value = JID(value) + except ValueError: + raise ValueError(f'"jid" must be a valid JID object') + self.xml.attrib['jid'] = value.full + + def get_jid(self) -> Optional[JID]: + try: + return JID(self.xml.attrib.get('jid', '')) + except ValueError: + return None + + +class External(ElementBase): + name = 'external' + namespace = NS + plugin_attrib = 'external' + plugin_multi_attrib = 'externals' + interfaces = {'uri'} + + +class Invite(ElementBase): + name = 'invite' + namespace = NS + plugin_attrib = 'call-invite' + interfaces = {'video'} + + def get_methods(self) -> Tuple[List[Jingle], List[External]]: + return (self['jingles'], self['externals']) + + def set_video(self, value: bool) -> None: + if not isinstance(value, bool): + raise ValueError(f'Invalid value for the video attribute: {value}') + self.xml.attrib['video'] = str(value).lower() + + def get_video(self) -> bool: + vid = self.xml.attrib.get('video', 'false').lower() + return vid == 'true' + + +class Retract(ElementBase): + name = 'retract' + namespace = NS + plugin_attrib = 'call-retract' + interfaces = {'id'} + + +class Accept(ElementBase): + name = 'accept' + namespace = NS + plugin_attrib = 'call-accept' + interfaces = {'id'} + + +class Reject(ElementBase): + name = 'reject' + namespace = NS + plugin_attrib = 'call-reject' + interfaces = {'id'} + + +class Left(ElementBase): + name = 'left' + namespace = NS + plugin_attrib = 'call-left' + interfaces = {'id'} + + +def register_plugins() -> None: + register_stanza_plugin(Message, Invite) + register_stanza_plugin(Message, Retract) + register_stanza_plugin(Message, Accept) + register_stanza_plugin(Message, Reject) + register_stanza_plugin(Message, Left) + + register_stanza_plugin(Invite, Jingle, iterable=True) + register_stanza_plugin(Invite, External, iterable=True) + + register_stanza_plugin(Accept, Jingle) + register_stanza_plugin(Accept, External) diff --git a/tests/test_stanza_xep_0482.py b/tests/test_stanza_xep_0482.py new file mode 100644 index 00000000..a3572793 --- /dev/null +++ b/tests/test_stanza_xep_0482.py @@ -0,0 +1,42 @@ +import unittest +from slixmpp import Message +from slixmpp.jid import JID +from slixmpp.test import SlixTest +from slixmpp.plugins.xep_0482 import stanza +from slixmpp.plugins.xep_0482.stanza import External, Jingle +from slixmpp.xmlstream import register_stanza_plugin + + +class TestCallInviteStanza(SlixTest): + + def setUp(self): + stanza.register_plugins() + + def test_invite(self): + """Test that the element is created correctly.""" + msg = Message() + msg['call-invite']['video'] = True + jingle = Jingle() + jingle['sid'] = 'toto' + jingle['jid'] = JID('toto@example.com/m') + external = External() + external['uri'] = "https://example.com/call" + msg['call-invite'].append(jingle) + msg['call-invite'].append(external) + + self.check(msg, """ + + + + + + + """) + + self.assertEqual( + msg['call-invite'].get_methods(), + ([jingle], [external]), + ) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestCallInviteStanza)