From 119f59ecbe91f79c1fe42887d24720fc70f506d0 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sat, 30 Jan 2021 17:42:20 +0100 Subject: [PATCH 1/3] XEP-0369: Add events for channel/participants --- slixmpp/plugins/xep_0369/mix_core.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/slixmpp/plugins/xep_0369/mix_core.py b/slixmpp/plugins/xep_0369/mix_core.py index 19450c12..529026b5 100644 --- a/slixmpp/plugins/xep_0369/mix_core.py +++ b/slixmpp/plugins/xep_0369/mix_core.py @@ -72,9 +72,22 @@ class XEP_0369(BasePlugin): def session_bind(self, jid): self.xmpp.plugin['xep_0030'].add_feature(stanza.NS) + self.xmpp.plugin['xep_0060'].map_node_event( + 'urn:xmpp:mix:nodes:participants', + 'mix_participant_info', + ) + self.xmpp.plugin['xep_0060'].map_node_event( + 'urn:xmpp:mix:nodes:info', + 'mix_channel_info', + ) def plugin_end(self): self.xmpp.plugin['xep_0030'].del_feature(feature=stanza.NS) + node_map = self.xmpp.plugin['xep_0060'].node_event_map + if 'urn:xmpp:mix:nodes:info' in node_map: + del node_map['urn:xmpp:mix:nodes:info'] + if 'urn:xmpp:mix:nodes:participants' in node_map: + del node_map['urn:xmpp:mix:nodes:participants'] async def get_channel_info(self, channel: JID) -> InfoType: """" From 04a3f609e2b1fe7ae0ecaa2e1b251c7fcead33b1 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sat, 30 Jan 2021 18:17:12 +0100 Subject: [PATCH 2/3] XEP-0405: Manage MIX Roster items --- slixmpp/plugins/xep_0405/mix_pam.py | 26 ++++++++++++++++++++++++++ slixmpp/plugins/xep_0405/stanza.py | 18 ++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/slixmpp/plugins/xep_0405/mix_pam.py b/slixmpp/plugins/xep_0405/mix_pam.py index cff22b51..1158a6f9 100644 --- a/slixmpp/plugins/xep_0405/mix_pam.py +++ b/slixmpp/plugins/xep_0405/mix_pam.py @@ -6,13 +6,16 @@ See the file LICENSE for copying permission. """ from typing import ( + List, Optional, Set, + Tuple, ) from slixmpp import JID, Iq from slixmpp.exceptions import IqError, IqTimeout from slixmpp.plugins import BasePlugin +from slixmpp.stanza.roster import RosterItem from slixmpp.plugins.xep_0405 import stanza from slixmpp.plugins.xep_0369 import stanza as mix_stanza @@ -86,3 +89,26 @@ class XEP_0405(BasePlugin): iq['client_leave']['channel'] = room iq['client_leave'].enable('mix_leave') return await iq.send(**iqkwargs) + + async def get_mix_roster(self, *, + ito: Optional[JID] = None, + ifrom: Optional[JID] = None, + **iqkwargs) -> Tuple[List[RosterItem], List[RosterItem]]: + """ + Get the annotated roster, with MIX channels. + + :return: A tuple of (contacts, mix channels) as RosterItem elements + """ + iq = self.xmpp.make_iq_get(ito=ito, ifrom=ifrom) + iq['roster'].enable('annotate') + result = await iq.send(**iqkwargs) + self.xmpp.event("roster_update", result) + contacts = [] + mix = [] + for item in result['roster']: + channel = item._get_plugin('channel', check=True) + if channel: + mix.append(item) + else: + contacts.append(item) + return (contacts, mix) diff --git a/slixmpp/plugins/xep_0405/stanza.py b/slixmpp/plugins/xep_0405/stanza.py index fe221bd6..58133d98 100644 --- a/slixmpp/plugins/xep_0405/stanza.py +++ b/slixmpp/plugins/xep_0405/stanza.py @@ -8,6 +8,7 @@ from slixmpp import JID from slixmpp.stanza import Iq +from slixmpp.stanza.roster import Roster, RosterItem from slixmpp.xmlstream import ( ElementBase, register_stanza_plugin, @@ -19,6 +20,7 @@ from slixmpp.plugins.xep_0369.stanza import ( ) NS = 'urn:xmpp:mix:pam:2' +NS_ROSTER = 'urn:xmpp:mix:roster:0' class ClientJoin(ElementBase): @@ -35,9 +37,25 @@ class ClientLeave(ElementBase): interfaces = {'channel'} +class Annotate(ElementBase): + namespace = NS_ROSTER + name = 'annotate' + plugin_attrib = 'annotate' + + +class Channel(ElementBase): + namespace = NS_ROSTER + name = 'channel' + plugin_attrib = 'channel' + interfaces = {'participant-id'} + + def register_plugins(): register_stanza_plugin(Iq, ClientJoin) register_stanza_plugin(ClientJoin, Join) register_stanza_plugin(Iq, ClientLeave) register_stanza_plugin(ClientLeave, Leave) + + register_stanza_plugin(Roster, Annotate) + register_stanza_plugin(RosterItem, Channel) From f41fd7cce4c3cc49e6802cd16e38cd6f79f4bab0 Mon Sep 17 00:00:00 2001 From: mathieui Date: Sat, 30 Jan 2021 18:39:28 +0100 Subject: [PATCH 3/3] examples: add an example MIX bot (does the same as the current MUC bot) --- examples/mix.py | 162 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100755 examples/mix.py diff --git a/examples/mix.py b/examples/mix.py new file mode 100755 index 00000000..1c3a34ae --- /dev/null +++ b/examples/mix.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2021 Mathieu Pasquet + This file is part of Slixmpp. + + See the file LICENSE for copying permission. +""" + +import logging +from getpass import getpass +from argparse import ArgumentParser + +import slixmpp + + +class MIXBot(slixmpp.ClientXMPP): + + """ + A simple Slixmpp bot that will greets those + who enter the room, and acknowledge any messages + that mentions the bot's nickname. + """ + + def __init__(self, jid, password, room, nick): + slixmpp.ClientXMPP.__init__(self, jid, password) + + self.room = room + self.rooms = set() + self.nick = nick + + # The session_start event will be triggered when + # the bot establishes its connection with the server + # and the XML streams are ready for use. We want to + # listen for this event so that we we can initialize + # our roster. + self.add_event_handler("session_start", self.start) + + # The mix_message event is triggered whenever a message + # stanza is received from any chat room. + self.add_event_handler("mix_message", self.mix_message) + + # The mix_participant_info_publish event is triggered whenever + # an occupant joins or leaves the channel (not linked to + # actual presence) + self.add_event_handler("mix_participant_info_publish", + self.mix_joined) + + + async def start(self, event): + """ + Process the session_start event. + + Typical actions for the session_start event are + requesting the roster and broadcasting an initial + presence stanza. + """ + # The goal here is to fetch the already joined MIX channels + # which are present in the roster. + _, mix_rooms = await self.plugin['xep_0405'].get_mix_roster() + for room in mix_rooms: + self.rooms.add(room['jid']) + self.send_presence() + if self.room not in self.rooms: + # If we are not joined, we need to. This will carry over + # the next restarts + await self.plugin['xep_0405'].join_channel( + self.room, + self.nick, + ) + + def mix_message(self, msg): + """ + Process incoming message stanzas from any chat room. Be aware + that if you also have any handlers for the 'message' event, + message stanzas may be processed by both handlers, so check + the 'type' attribute when using a 'message' event handler. + + Whenever the bot's nickname is mentioned, respond to + the message. + + IMPORTANT: Always check that a message is not from yourself, + otherwise you will create an infinite loop responding + to your own messages. + + This handler will reply to messages that mention + the bot's nickname. + + Arguments: + msg -- The received message stanza. See the documentation + for stanza objects and the Message stanza to see + how it may be used. + """ + if msg['mix']['nick'] != self.nick and self.nick in msg['body']: + self.send_message(mto=msg['from'].bare, + mbody="I heard that, %s." % msg['mix']['nick'], + mtype='groupchat') + + def mix_joined(self, event): + """ + We receive a publish event whenever someone joins the MIX channel. + It contains the nickname of the new participant, and the JID when + the channel is not a "JID Hidden channel". + """ + participant = event['pubsub_event']['items']['item']['mix_participant'] + if participant['nick'] != self.nick: + self.send_message(mto=event['from'].bare, + mbody="Hello, %s" % participant['nick'], + mtype='groupchat') + + +if __name__ == '__main__': + # Setup the command line arguments. + parser = ArgumentParser() + + # Output verbosity options. + parser.add_argument("-q", "--quiet", help="set logging to ERROR", + action="store_const", dest="loglevel", + const=logging.ERROR, default=logging.INFO) + parser.add_argument("-d", "--debug", help="set logging to DEBUG", + action="store_const", dest="loglevel", + const=logging.DEBUG, default=logging.INFO) + + # JID and password options. + parser.add_argument("-j", "--jid", dest="jid", + help="JID to use") + parser.add_argument("-p", "--password", dest="password", + help="password to use") + parser.add_argument("-r", "--room", dest="room", + help="MIX channel to join") + parser.add_argument("-n", "--nick", dest="nick", + help="MIX nickname") + + args = parser.parse_args() + + # Setup logging. + logging.basicConfig(level=args.loglevel, + format='%(levelname)-8s %(message)s') + + if args.jid is None: + args.jid = input("Username: ") + if args.password is None: + args.password = getpass("Password: ") + if args.room is None: + args.room = input("MIX channel: ") + if args.nick is None: + args.nick = input("MIX nickname: ") + + # Setup the MIXBot and register plugins. Note that while plugins may + # have interdependencies, the order in which you register them does + # not matter. + xmpp = MIXBot(args.jid, args.password, args.room, args.nick) + xmpp.register_plugin('xep_0030') # Service Discovery + xmpp.register_plugin('xep_0199') # XMPP Ping + xmpp.register_plugin('xep_0369') # MIX Core + xmpp.register_plugin('xep_0405') # MIX PAM + + # Connect to the XMPP server and start processing XMPP stanzas. + xmpp.connect() + xmpp.process()