diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py index a89b10f6..91f062a3 100644 --- a/slixmpp/plugins/__init__.py +++ b/slixmpp/plugins/__init__.py @@ -85,7 +85,11 @@ __all__ = [ 'xep_0323', # IoT Systems Sensor Data 'xep_0325', # IoT Systems Control 'xep_0332', # HTTP Over XMPP Transport + 'xep_0369', # MIX-CORE 'xep_0377', # Spam reporting + 'xep_0403', # MIX-Presence + 'xep_0404', # MIX-Anon + 'xep_0405', # MIX-PAM 'xep_0421', # Anonymous unique occupant identifiers for MUCs 'xep_0444', # Message Reactions ] diff --git a/slixmpp/plugins/xep_0369/__init__.py b/slixmpp/plugins/xep_0369/__init__.py new file mode 100644 index 00000000..2fa3a0ad --- /dev/null +++ b/slixmpp/plugins/xep_0369/__init__.py @@ -0,0 +1,13 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.plugins.base import register_plugin +from slixmpp.plugins.xep_0369.stanza import * +from slixmpp.plugins.xep_0369.mix_core import XEP_0369 + +register_plugin(XEP_0369) diff --git a/slixmpp/plugins/xep_0369/mix_core.py b/slixmpp/plugins/xep_0369/mix_core.py new file mode 100644 index 00000000..598a97f4 --- /dev/null +++ b/slixmpp/plugins/xep_0369/mix_core.py @@ -0,0 +1,288 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" +from typing import ( + Any, + Dict, + List, + Optional, + Set, + Tuple, +) + +from datetime import datetime +from slixmpp import JID, Iq +from slixmpp.exceptions import IqError, IqTimeout +from slixmpp.plugins import BasePlugin +from slixmpp.plugins.xep_0369 import stanza +from slixmpp.xmlstream.handler import Callback +from slixmpp.xmlstream.matcher import MatchXPath + +try: + from typing import TypedDict + InfoType = TypedDict( + 'InfoType', + { + 'Name': str, + 'Description': str, + 'Contact': Optional[List[JID]], + 'modified': datetime + }, + total=False, + ) +except ImportError: + # Placeholder until we drop python < 3.8 + InfoType = Dict[str, Any] + + +BASE_NODES = [ + 'urn:xmpp:mix:nodes:messages', + 'urn:xmpp:mix:nodes:participants', + 'urn:xmpp:mix:nodes:info', +] + + +class XEP_0369(BasePlugin): + '''XEP-0369: MIX-CORE''' + + name = 'xep_0369' + description = 'MIX-CORE' + dependencies = {'xep_0030', 'xep_0060', 'xep_0082', 'xep_0004'} + stanza = stanza + namespace = stanza.NS + + def plugin_init(self) -> None: + stanza.register_plugins() + self.xmpp.register_handler( + Callback( + "MIX message received", + MatchXPath('{%s}message[@type="groupchat"]/{%s}mix' % ( + self.xmpp.default_ns, self.namespace + )), + self._handle_mix_message, + ) + ) + + def _handle_mix_message(self, message): + self.xmpp.event('mix_message', message) + + def session_bind(self, jid): + self.xmpp.plugin['xep_0030'].add_feature(stanza.NS) + + def plugin_end(self): + self.xmpp.plugin['xep_0030'].del_feature(feature=stanza.NS) + + async def get_channel_info(self, channel: JID) -> InfoType: + """" + Get the contents of the channel info node. + :param JID channel: The MIX channel + :returns: a dict containing the last modified time and form contents + (Name, Description, Contact per the spec, YMMV) + """ + info = await self.xmpp['xep_0060'].get_items(channel, 'urn:xmpp:mix:nodes:info') + for item in info['pubsub']['items']: + time = item['id'] + fields = item['form'].get_values() + del fields['FORM_TYPE'] + fields['modified'] = self.xmpp['xep_0082'].parse(time) + contact = fields.get('Contact') + if contact: + if isinstance(contact, str): + contact = [contact] + elif isinstance(contact, list): + contact = [JID(cont) for cont in contact] + fields['Contact'] = contact + return fields + + async def join_channel(self, channel: JID, nick: str, subscribe: Optional[Set[str]] = None, *, + ifrom: Optional[JID] = None, **iqkwargs) -> Set[str]: + """ + Join a MIX channel. + + :param JID channel: JID of the MIX channel + :param str nick: Desired nickname on that channel + :param Set[str] subscribe: Set of notes to subscribe to when joining. + If empty, all nodes will be subscribed by default. + + :rtype: Set[str] + :return: The nodes that failed to subscribe, if any + """ + if not subscribe: + subscribe = set(BASE_NODES) + iq = self.xmpp.make_iq_set(ito=channel, ifrom=ifrom) + iq['mix_join']['nick'] = nick + for node in subscribe: + sub = stanza.Subscribe() + sub['node'] = node + iq['mix_join']['subscribe'].append(sub) + result = await iq.send(**iqkwargs) + result_nodes = {sub['node'] for sub in result['mix_join']} + return result_nodes.difference(subscribe) + + async def update_subscription(self, channel: JID, + subscribe: Optional[Set[str]] = None, + unsubscribe: Optional[Set[str]] = None, *, + ifrom: Optional[JID] = None, **iqkwargs) -> Tuple[Set[str], Set[str]]: + """ + Update a MIX channel subscription. + + :param JID channel: JID of the MIX channel + :param Set[str] subscribe: Set of notes to subscribe to additionally. + :param Set[str] unsubscribe: Set of notes to unsubscribe from. + :rtype: Tuple[Set[str], Set[str]] + :return: A tuple containing the set of nodes that failed to subscribe + and the set of nodes that failed to unsubscribe. + """ + if not subscribe and not unsubscribe: + raise ValueError("No nodes were provided.") + unsubscribe = unsubscribe or set() + subscribe = subscribe or set() + iq = self.xmpp.make_iq_set(ito=channel, ifrom=ifrom) + iq.enable('mix_updatesub') + for node in subscribe: + sub = stanza.Subscribe() + sub['node'] = node + iq['mix_updatesub'].append(sub) + for node in unsubscribe: + unsub = stanza.Unsubscribe() + unsub['node'] = node + iq['mix_updatesub'].append(unsub) + result = await iq.send(**iqkwargs) + for item in result['mix_updatesub']: + if isinstance(item, stanza.Subscribe): + subscribe.discard(item['node']) + elif isinstance(item, stanza.Unsubscribe): + unsubscribe.discard(item['node']) + return (subscribe, unsubscribe) + + async def leave_channel(self, channel: JID, *, + ifrom: Optional[JID] = None, **iqkwargs) -> None: + """" + Leave a MIX channel + :param JID channel: JID of the channel to leave + """ + iq = self.xmpp.make_iq_set(ito=channel, ifrom=ifrom) + iq.enable('mix_leave') + await iq.send(**iqkwargs) + + async def set_nick(self, channel: JID, nick: str, *, + ifrom: Optional[JID] = None, **iqkwargs) -> str: + """ + Set your nick on a channel. The returned nick MAY be different + from the one provided, depending on service configuration. + :param JID channel: MIX channel JID + :param str nick: desired nick + :rtype: str + :return: The nick saved on the MIX channel + """ + + iq = self.xmpp.make_iq_set(ito=channel, ifrom=ifrom) + iq['mix_setnick']['nick'] = nick + result = await iq.send(**iqkwargs) + result_nick = result['mix_setnick']['nick'] + return result_nick + + async def can_create_channel(self, service: JID) -> bool: + """ + Check if the current user can create a channel on the MIX service + + :param JID service: MIX service jid + :rtype: bool + """ + results_stanza = await self.xmpp['xep_0030'].get_info(service.server) + features = results_stanza['disco_info']['features'] + return 'urn:xmpp:mix:core:1#create-channel' in features + + async def create_channel(self, service: JID, channel: Optional[str] = None, *, + ifrom: Optional[JID] = None, **iqkwargs) -> str: + """ + Create a MIX channel. + + :param JID service: MIX service JID + :param Optional[str] channel: Channel name (or leave empty to let + the service generate it) + :returns: The channel name, as created by the service + """ + if '#' in channel: + raise ValueError("A channel name cannot contain hashes") + iq = self.xmpp.make_iq_set(ito=service.server, ifrom=ifrom) + iq.enable('mix_create') + if channel is not None: + iq['mix_create']['channel'] = channel + result = await iq.send(**iqkwargs) + return result['mix_create']['channel'] + + async def destroy_channel(self, channel: JID, *, + ifrom: Optional[JID] = None, **iqkwargs): + """ + Destroy a MIX channel. + :param JID channel: MIX channelJID + """ + iq = self.xmpp.make_iq_set(ito=channel.server, ifrom=ifrom) + iq['mix_destroy'] = channel.user + await iq.send(**iqkwargs) + + async def list_mix_nodes(self, channel: JID, + ifrom: Optional[JID] = None, **discokwargs) -> Set[str]: + """ + List mix nodes for a channel. + + :param JID channel: The MIX channel + :returns: List of nodes available + """ + result = await self.xmpp['xep_0030'].get_items( + channel, + node='mix', + ifrom=ifrom, + **discokwargs, + ) + nodes = set() + for item in result['disco_items']: + nodes.add(item['node']) + return nodes + + async def list_participants(self, channel: JID, *, + ifrom: Optional[JID] = None, **pubsubkwargs) -> List[Tuple[str, str, Optional[JID]]]: + """ + List the participants of a MIX channel + :param JID channel: The MIX channel + + :returns: A list of tuples containing the participant id, nick, and jid (if available) + """ + info = await self.xmpp['xep_0060'].get_items( + channel, + 'urn:xmpp:mix:nodes:participants', + ifrom=ifrom, + **pubsubkwargs + ) + participants = list() + for item in info['pubsub']['items']: + identifier = item['id'] + nick = item['mix_participant']['nick'] + jid = item['mix_participant']['jid'] or None + participants.append( + (identifier, nick, jid), + ) + return participants + + async def list_channels(self, service: JID, *, + ifrom: Optional[JID] =None, **discokwargs) -> List[Tuple[JID, str]]: + """ + List the channels on a MIX service + + :param JID service: MIX service JID + :returns: A list of channels with their JID and name + """ + results_stanza = await self.xmpp['xep_0030'].get_items( + service.server, + ifrom=ifrom, + **discokwargs, + ) + results = [] + for result in results_stanza['disco_items']: + results.append((result['jid'], result['name'])) + return results diff --git a/slixmpp/plugins/xep_0369/stanza.py b/slixmpp/plugins/xep_0369/stanza.py new file mode 100644 index 00000000..ca64b2c4 --- /dev/null +++ b/slixmpp/plugins/xep_0369/stanza.py @@ -0,0 +1,121 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permissio +""" + +import xml.etree.ElementTree as ET +from slixmpp import JID +from slixmpp.stanza import ( + Iq, + Message, +) +from slixmpp.xmlstream import ( + ElementBase, + register_stanza_plugin, +) + +from slixmpp.plugins.xep_0004.stanza import ( + Form, +) +from slixmpp.plugins.xep_0060.stanza import ( + EventItem, + Item, +) + +NS = 'urn:xmpp:mix:core:1' + + +class MIX(ElementBase): + name = 'mix' + namespace = NS + plugin_attrib = 'mix' + interfaces = {'nick', 'jid'} + sub_interfaces = {'nick', 'jid'} + + +class Setnick(ElementBase): + name = 'setnick' + namespace = NS + plugin_attrib = 'mix_setnick' + interfaces = {'nick'} + sub_interfaces = {'nick'} + + +class Join(ElementBase): + namespace = NS + name = 'join' + plugin_attrib = 'mix_join' + interfaces = {'nick', 'id'} + sub_interfaces = {'nick'} + + +class Leave(ElementBase): + namespace = NS + name = 'leave' + plugin_attrib = 'mix_leave' + + +class Subscribe(ElementBase): + namespace = NS + name = 'subscribe' + plugin_attrib = 'subscribe' + interfaces = {'node'} + + +class Unsubscribe(ElementBase): + namespace = NS + name = 'unsubscribe' + plugin_attrib = 'unsubscribe' + interfaces = {'node'} + +class UpdateSubscription(ElementBase): + namespace = NS + name = 'update-subscription' + plugin_attrib = 'mix_updatesub' + interfaces = {'jid'} + + +class Create(ElementBase): + name = 'create' + plugin_attrib = 'mix_create' + namespace = NS + interfaces = {'channel'} + + +class Participant(ElementBase): + namespace = NS + name = 'participant' + plugin_attrib = 'mix_participant' + interfaces = {'nick', 'jid'} + sub_interfaces = {'nick', 'jid'} + + +class Destroy(ElementBase): + name = 'destroy' + plugin_attrib = 'mix_destroy' + namespace = NS + interfaces = {'channel'} + + +def register_plugins(): + register_stanza_plugin(Item, Form) + register_stanza_plugin(EventItem, Form) + + register_stanza_plugin(EventItem, Participant) + register_stanza_plugin(Item, Participant) + + register_stanza_plugin(Join, Subscribe, iterable=True) + register_stanza_plugin(Iq, Join) + + register_stanza_plugin(UpdateSubscription, Subscribe, iterable=True) + register_stanza_plugin(UpdateSubscription, Unsubscribe, iterable=True) + register_stanza_plugin(Iq, UpdateSubscription) + + register_stanza_plugin(Iq, Leave) + register_stanza_plugin(Iq, Create) + register_stanza_plugin(Iq, Setnick) + + register_stanza_plugin(Message, MIX) diff --git a/slixmpp/plugins/xep_0403/__init__.py b/slixmpp/plugins/xep_0403/__init__.py new file mode 100644 index 00000000..0526276e --- /dev/null +++ b/slixmpp/plugins/xep_0403/__init__.py @@ -0,0 +1,13 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.plugins.base import register_plugin +from slixmpp.plugins.xep_0403.stanza import * +from slixmpp.plugins.xep_0403.mix_presence import XEP_0403 + +register_plugin(XEP_0403) diff --git a/slixmpp/plugins/xep_0403/mix_presence.py b/slixmpp/plugins/xep_0403/mix_presence.py new file mode 100644 index 00000000..995439b9 --- /dev/null +++ b/slixmpp/plugins/xep_0403/mix_presence.py @@ -0,0 +1,47 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" +from typing import ( + Optional, + Set, +) + +from slixmpp import JID, Iq +from slixmpp.exceptions import IqError, IqTimeout +from slixmpp.plugins import BasePlugin +from slixmpp.plugins.xep_0403 import stanza +from slixmpp.xmlstream.matcher import MatchXPath +from slixmpp.xmlstream.handler import Callback + + +NODES = [ + 'urn:xmpp:mix:nodes:presence' +] + + +class XEP_0403(BasePlugin): + '''XEP-0403: MIX-Presence''' + + name = 'xep_0403' + description = 'MIX-Presence' + dependencies = {'xep_0369'} + stanza = stanza + namespace = stanza.NS + + def plugin_init(self) -> None: + stanza.register_plugins() + + self.xmpp.register_handler( + Callback( + 'MIX Presence received', + MatchXPath('{%s}presence/{%s}mix' % (self.xmpp.default_ns, stanza.NS)), + self._handle_mix_presence, + ) + ) + + def _handle_mix_presence(self, presence): + self.xmpp.event('mix_presence', presence) diff --git a/slixmpp/plugins/xep_0403/stanza.py b/slixmpp/plugins/xep_0403/stanza.py new file mode 100644 index 00000000..3e5b9cde --- /dev/null +++ b/slixmpp/plugins/xep_0403/stanza.py @@ -0,0 +1,37 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permissio +""" + +from xml.etree import ElementTree as ET +from slixmpp import JID +from slixmpp.stanza import Presence +from slixmpp.xmlstream import ( + register_stanza_plugin, + ElementBase, +) + +from slixmpp.plugins.xep_0060.stanza import ( + Item, + EventItem, +) + + +NS = 'urn:xmpp:mix:presence:0' + + +class MIXPresence(ElementBase): + namespace = NS + name = 'mix' + plugin_attrib = 'mix' + interfaces = {'jid', 'nick'} + sub_interfaces = {'jid', 'nick'} + + +def register_plugins(): + register_stanza_plugin(Presence, MIXPresence) + register_stanza_plugin(Item, Presence) + register_stanza_plugin(EventItem, Presence) diff --git a/slixmpp/plugins/xep_0404/__init__.py b/slixmpp/plugins/xep_0404/__init__.py new file mode 100644 index 00000000..21dd6814 --- /dev/null +++ b/slixmpp/plugins/xep_0404/__init__.py @@ -0,0 +1,13 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.plugins.base import register_plugin +from slixmpp.plugins.xep_0404.stanza import Participant +from slixmpp.plugins.xep_0404.mix_anon import XEP_0404 + +register_plugin(XEP_0404) diff --git a/slixmpp/plugins/xep_0404/mix_anon.py b/slixmpp/plugins/xep_0404/mix_anon.py new file mode 100644 index 00000000..d8c42381 --- /dev/null +++ b/slixmpp/plugins/xep_0404/mix_anon.py @@ -0,0 +1,101 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" +from typing import ( + Dict, + Optional, + Set, + Tuple, +) + +from slixmpp import JID, Message, Iq +from slixmpp.exceptions import IqError, IqTimeout +from slixmpp.plugins import BasePlugin +from slixmpp.xmlstream import register_stanza_plugin +from slixmpp.xmlstream.matcher import MatchXPath +from slixmpp.xmlstream.handler import Callback +from slixmpp.plugins.xep_0404 import stanza +from slixmpp.plugins.xep_0004.stanza import Form + + +NODES = [ + 'urn:xmpp:mix:nodes:jidmap', +] + + +class XEP_0404(BasePlugin): + '''XEP-0404: MIX JID Hidden Channels''' + + name = 'xep_0404' + description = 'MIX-ANON' + dependencies = {'xep_0369'} + stanza = stanza + namespace = stanza.NS + + def plugin_init(self) -> None: + stanza.register_plugins() + + async def get_anon_raw(self, channel: JID, *, + ifrom: Optional[JID] = None, **pubsubkwargs) -> Iq: + """ + Get the jid-participant mapping result (raw). + :param JID channel: MIX channel JID + """ + return await self.xmpp['xep_0030'].get_items( + channel.bare, + ifrom=ifrom, + **pubsubkwargs + ) + + async def get_anon_by_jid(self, channel: JID, *, + ifrom: Optional[JID] = None, **pubsubkwargs) -> Dict[JID, str]: + """ + Get the jid-participant mapping, by JID + + :param JID channel: MIX channel JID + """ + raw = await self.get_anon_raw(channel, ifrom=ifrom, **pubsubkwargs) + mapping = {} + for item in raw['pubsub']['items']: + mapping[item['anon_participant']['jid']] = item['id'] + return mapping + + async def get_anon_by_id(self, channel: JID, *, + ifrom: Optional[JID] = None, **pubsubkwargs) -> Dict[str, JID]: + """ + Get the jid-participant mapping, by participant id + + :param JID channel: MIX channel JID + """ + raw = await self.get_anon_raw(channel, ifrom=ifrom, **pubsubkwargs) + mapping = {} + for item in raw['pubsub']['items']: + mapping[item['id']] = item['anon_participant']['jid'] + return mapping + + async def get_preferences(self, channel: JID, *, + ifrom: Optional[JID] = None, **iqkwargs) -> Form: + """ + Get channel preferences with default values. + :param JID channel: MIX channel JID + """ + iq = self.xmpp.make_iq_get(ito=channel.bare, ifrom=ifrom) + iq.enable('user_preference') + prefs_stanza = await iq.send(**iqkwargs) + return prefs_stanza['user_preference']['form'] + + async def set_preferences(self, channel: JID, form: Form, *, + ifrom: Optional[JID] = None, **iqkwargs) -> Form: + """ + Set channel preferences + :param JID channel: MIX channel JID + :param Form form: A 0004 form with updated preferences + """ + iq = self.xmpp.make_iq_set(ito=channel.bare, ifrom=ifrom) + iq['user_preference']['form'] = form + prefs_result = await iq.send(**iqkwargs) + return prefs_result['user_preference']['form'] diff --git a/slixmpp/plugins/xep_0404/stanza.py b/slixmpp/plugins/xep_0404/stanza.py new file mode 100644 index 00000000..9bb9308e --- /dev/null +++ b/slixmpp/plugins/xep_0404/stanza.py @@ -0,0 +1,43 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permissio +""" + +from slixmpp.xmlstream import ( + ElementBase, + register_stanza_plugin, +) +from slixmpp import Iq + +from slixmpp.plugins.xep_0004.stanza import Form +from slixmpp.plugins.xep_0060.stanza import ( + EventItem, + Item, +) + +NS = 'urn:xmpp:mix:anon:0' + + +class Participant(ElementBase): + namespace = NS + name = 'participant' + plugin_attrib = 'anon_participant' + interfaces = {'jid'} + sub_interfaces = {'jid'} + + +class UserPreference(ElementBase): + namespace = NS + name = 'user-preference' + plugin_attrib = 'user_preference' + + +def register_plugins(): + register_stanza_plugin(EventItem, Participant) + register_stanza_plugin(Item, Participant) + + register_stanza_plugin(Iq, UserPreference) + register_stanza_plugin(UserPreference, Form) diff --git a/slixmpp/plugins/xep_0405/__init__.py b/slixmpp/plugins/xep_0405/__init__.py new file mode 100644 index 00000000..0a877682 --- /dev/null +++ b/slixmpp/plugins/xep_0405/__init__.py @@ -0,0 +1,13 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.plugins.base import register_plugin +from slixmpp.plugins.xep_0405.stanza import * +from slixmpp.plugins.xep_0405.mix_pam import XEP_0405 + +register_plugin(XEP_0405) diff --git a/slixmpp/plugins/xep_0405/mix_pam.py b/slixmpp/plugins/xep_0405/mix_pam.py new file mode 100644 index 00000000..cff22b51 --- /dev/null +++ b/slixmpp/plugins/xep_0405/mix_pam.py @@ -0,0 +1,88 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" +from typing import ( + Optional, + Set, +) + +from slixmpp import JID, Iq +from slixmpp.exceptions import IqError, IqTimeout +from slixmpp.plugins import BasePlugin +from slixmpp.plugins.xep_0405 import stanza +from slixmpp.plugins.xep_0369 import stanza as mix_stanza + + +BASE_NODES = [ + 'urn:xmpp:mix:nodes:messages', + 'urn:xmpp:mix:nodes:participants', + 'urn:xmpp:mix:nodes:info', +] + + +class XEP_0405(BasePlugin): + '''XEP-0405: MIX-PAM''' + + name = 'xep_0405' + description = 'MIX-PAM' + dependencies = {'xep_0369'} + stanza = stanza + namespace = stanza.NS + + def plugin_init(self) -> None: + stanza.register_plugins() + + async def check_server_capability(self) -> bool: + """Check if the server is MIX-PAM capable""" + result = await self.xmpp.plugin['xep_0030'].get_info(jid=self.xmpp.boundjid.bare) + features = result['disco_info']['features'] + return stanza.NS in features + + async def join_channel(self, room: JID, nick: str, subscribe: Optional[Set[str]] = None, *, + ito: Optional[JID] = None, + ifrom: Optional[JID] = None, + **iqkwargs) -> Set[str]: + """ + Join a MIX channel. + + :param JID room: JID of the MIX channel + :param str nick: Desired nickname on that channel + :param Set[str] subscribe: Set of nodes to subscribe to when joining. + If empty, all nodes will be subscribed by default. + + :rtype: Set[str] + :return: The nodes that failed to subscribe, if any + """ + if subscribe is None: + subscribe = set(BASE_NODES) + if ito is None: + ito = self.xmpp.boundjid.bare + iq = self.xmpp.make_iq_set(ito=ito, ifrom=ifrom) + iq['client_join']['channel'] = room + iq['client_join']['mix_join']['nick'] = nick + for node in subscribe: + sub = mix_stanza.Subscribe() + sub['node'] = node + iq['client_join']['mix_join'].append(sub) + result = await iq.send(**iqkwargs) + result_nodes = {sub['node'] for sub in result['client_join']['mix_join']} + return result_nodes.difference(subscribe) + + async def leave_channel(self, room: JID, *, + ito: Optional[JID] = None, + ifrom: Optional[JID] = None, + **iqkwargs) -> Iq: + """" + Leave a MIX channel + :param JID room: JID of the channel to leave + """ + if ito is None: + ito = self.xmpp.boundjid.bare + iq = self.xmpp.make_iq_set(ito=ito, ifrom=ifrom) + iq['client_leave']['channel'] = room + iq['client_leave'].enable('mix_leave') + return await iq.send(**iqkwargs) diff --git a/slixmpp/plugins/xep_0405/stanza.py b/slixmpp/plugins/xep_0405/stanza.py new file mode 100644 index 00000000..fe221bd6 --- /dev/null +++ b/slixmpp/plugins/xep_0405/stanza.py @@ -0,0 +1,43 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2020 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permissio +""" + +from slixmpp import JID +from slixmpp.stanza import Iq +from slixmpp.xmlstream import ( + ElementBase, + register_stanza_plugin, +) + +from slixmpp.plugins.xep_0369.stanza import ( + Join, + Leave, +) + +NS = 'urn:xmpp:mix:pam:2' + + +class ClientJoin(ElementBase): + namespace = NS + name = 'client-join' + plugin_attrib = 'client_join' + interfaces = {'channel'} + + +class ClientLeave(ElementBase): + namespace = NS + name = 'client-leave' + plugin_attrib = 'client_leave' + interfaces = {'channel'} + + +def register_plugins(): + register_stanza_plugin(Iq, ClientJoin) + register_stanza_plugin(ClientJoin, Join) + + register_stanza_plugin(Iq, ClientLeave) + register_stanza_plugin(ClientLeave, Leave) diff --git a/slixmpp/xmlstream/stanzabase.py b/slixmpp/xmlstream/stanzabase.py index 7eaf78a5..925f2abc 100644 --- a/slixmpp/xmlstream/stanzabase.py +++ b/slixmpp/xmlstream/stanzabase.py @@ -745,6 +745,8 @@ class ElementBase(object): getattr(self, set_method)(value, **kwargs) else: if attrib in self.sub_interfaces: + if isinstance(value, JID): + value = str(value) if lang == '*': return self._set_all_sub_text(attrib, value, @@ -863,6 +865,8 @@ class ElementBase(object): if value is None or value == '': self.__delitem__(name) else: + if isinstance(value, JID): + value = str(value) self.xml.attrib[name] = value def _del_attr(self, name): diff --git a/tests/test_stanza_xep_0369.py b/tests/test_stanza_xep_0369.py new file mode 100644 index 00000000..8c3e2a6b --- /dev/null +++ b/tests/test_stanza_xep_0369.py @@ -0,0 +1,117 @@ +import unittest +from slixmpp import Iq, Message, JID +from slixmpp.test import SlixTest +from slixmpp.plugins.xep_0369 import stanza +from slixmpp.plugins.xep_0060 import stanza as pstanza +from slixmpp.plugins.xep_0369.mix_core import BASE_NODES + + +class TestMIXStanza(SlixTest): + + def setUp(self): + stanza.register_plugins() + + def testMIXJoin(self): + """Test that data is converted to base64""" + iq = Iq() + iq['type'] = 'set' + for node in BASE_NODES: + sub = stanza.Subscribe() + sub['node'] = node + iq['mix_join'].append(sub) + iq['mix_join']['nick'] = 'Toto' + + self.check(iq, """ + + + + + + Toto + + + """) + + def testMIXUpdateSub(self): + iq = Iq() + iq['type'] = 'set' + iq.enable('mix_updatesub') + sub = stanza.Subscribe() + sub['node'] = 'urn:xmpp:mix:nodes:someothernode' + iq['mix_updatesub'].append(sub) + + self.check(iq, """ + + + + + + """) + + def testMIXLeave(self): + iq = Iq() + iq['type'] = 'set' + iq.enable('mix_leave') + + self.check(iq, """ + + + + """) + + def testMIXSetNick(self): + iq = Iq() + iq['type'] = 'set' + iq['mix_setnick']['nick'] = 'A nick' + + self.check(iq, """ + + + A nick + + + """) + + def testMIXMessage(self): + msg = Message() + msg['type'] = 'groupchat' + msg['body'] = 'This is a message body' + msg['mix']['nick'] = 'A nick' + msg['mix']['jid'] = JID('toto@example.com') + + self.check(msg, """ + + This is a message body + + A nick + toto@example.com + + + """) + + def testMIXNewParticipant(self): + msg = Message() + msg['pubsub_event']['items']['node'] = 'urn:xmpp:mix:nodes:participants' + item = pstanza.EventItem() + item['id'] = '123456' + item['mix_participant']['jid'] = JID('titi@example.com') + item['mix_participant']['nick'] = 'Titi' + msg['pubsub_event']['items'].append(item) + + self.check(msg, """ + + + + + + titi@example.com + Titi + + + + + + """, use_values=False) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestMIXStanza) diff --git a/tests/test_stanza_xep_0405.py b/tests/test_stanza_xep_0405.py new file mode 100644 index 00000000..5d834cf1 --- /dev/null +++ b/tests/test_stanza_xep_0405.py @@ -0,0 +1,55 @@ +import unittest +from slixmpp import Iq, Message, JID +from slixmpp.test import SlixTest +from slixmpp.plugins.xep_0405 import stanza +from slixmpp.plugins.xep_0369 import stanza as mstanza +from slixmpp.plugins.xep_0405.mix_pam import BASE_NODES + + +class TestMIXPAMStanza(SlixTest): + + def setUp(self): + stanza.register_plugins() + mstanza.register_plugins() + + def testMIXPAMJoin(self): + """Test that data is converted to base64""" + iq = Iq() + iq['type'] = 'set' + iq['client_join']['channel'] = JID('mix@example.com') + for node in BASE_NODES: + sub = mstanza.Subscribe() + sub['node'] = node + iq['client_join']['mix_join'].append(sub) + iq['client_join']['mix_join']['nick'] = 'Toto' + + self.check(iq, """ + + + + + + + Toto + + + + """) + + + def testMIXPAMLeave(self): + iq = Iq() + iq['type'] = 'set' + iq['client_leave']['channel'] = JID('mix@example.com') + iq['client_leave'].enable('mix_leave') + + self.check(iq, """ + + + + + + """) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestMIXPAMStanza)