Merge branch 'xep-45-misc-fixes' into 'master'

Misc fixes for xep-0045

See merge request poezio/slixmpp!62
This commit is contained in:
Link Mauve 2020-11-26 20:48:44 +01:00
commit 9b5ab741c8
2 changed files with 245 additions and 151 deletions

View file

@ -9,8 +9,18 @@
from __future__ import with_statement from __future__ import with_statement
import logging import logging
from typing import (
List,
Tuple,
Optional,
)
from slixmpp import Presence, Message from slixmpp import (
Presence,
Message,
Iq,
JID,
)
from slixmpp.plugins import BasePlugin from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream import register_stanza_plugin, ET from slixmpp.xmlstream import register_stanza_plugin, ET
from slixmpp.xmlstream.handler.callback import Callback from slixmpp.xmlstream.handler.callback import Callback
@ -19,11 +29,23 @@ from slixmpp.xmlstream.matcher.xmlmask import MatchXMLMask
from slixmpp.exceptions import IqError, IqTimeout from slixmpp.exceptions import IqError, IqTimeout
from slixmpp.plugins.xep_0045 import stanza from slixmpp.plugins.xep_0045 import stanza
from slixmpp.plugins.xep_0045.stanza import MUCPresence, MUCMessage from slixmpp.plugins.xep_0045.stanza import (
MUCPresence,
MUCJoin,
MUCMessage,
MUCAdminQuery,
MUCAdminItem,
MUCHistory,
MUCOwnerQuery,
MUCOwnerDestroy,
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
AFFILIATIONS = ('outcast', 'member', 'admin', 'owner', 'none')
ROLES = ('moderator', 'participant', 'visitor', 'none')
class XEP_0045(BasePlugin): class XEP_0045(BasePlugin):
@ -39,19 +61,61 @@ class XEP_0045(BasePlugin):
def plugin_init(self): def plugin_init(self):
self.rooms = {} self.rooms = {}
self.our_nicks = {} self.our_nicks = {}
self.xep = '0045'
# load MUC support in presence stanzas # load MUC support in presence stanzas
register_stanza_plugin(Presence, MUCPresence) register_stanza_plugin(Presence, MUCPresence)
register_stanza_plugin(Presence, MUCJoin)
register_stanza_plugin(MUCJoin, MUCHistory)
register_stanza_plugin(Message, MUCMessage) register_stanza_plugin(Message, MUCMessage)
self.xmpp.register_handler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence)) register_stanza_plugin(Iq, MUCAdminQuery)
self.xmpp.register_handler(Callback('MUCError', MatchXMLMask("<message xmlns='%s' type='error'><error/></message>" % self.xmpp.default_ns), self.handle_groupchat_error_message)) register_stanza_plugin(Iq, MUCOwnerQuery)
self.xmpp.register_handler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message)) register_stanza_plugin(MUCOwnerQuery, MUCOwnerDestroy)
self.xmpp.register_handler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject)) register_stanza_plugin(MUCAdminQuery, MUCAdminItem, iterable=True)
self.xmpp.register_handler(Callback('MUCConfig', MatchXMLMask("<message xmlns='%s' type='groupchat'><x xmlns='http://jabber.org/protocol/muc#user'><status/></x></message>" % self.xmpp.default_ns), self.handle_config_change))
self.xmpp.register_handler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % ( # Register handlers
self.xmpp.default_ns, self.xmpp.register_handler(
stanza.NS_USER, Callback(
stanza.NS_USER)), self.handle_groupchat_invite)) 'MUCPresence',
MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns),
self.handle_groupchat_presence,
))
self.xmpp.register_handler(
Callback(
'MUCError',
MatchXMLMask("<message xmlns='%s' type='error'><error/></message>" % self.xmpp.default_ns),
self.handle_groupchat_error_message
))
self.xmpp.register_handler(
Callback(
'MUCMessage',
MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns),
self.handle_groupchat_message
))
self.xmpp.register_handler(
Callback(
'MUCSubject',
MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns),
self.handle_groupchat_subject
))
self.xmpp.register_handler(
Callback(
'MUCConfig',
MatchXMLMask(
"<message xmlns='%s' type='groupchat'>"
"<x xmlns='http://jabber.org/protocol/muc#user'><status/></x>"
"</message>" % self.xmpp.default_ns
),
self.handle_config_change
))
self.xmpp.register_handler(
Callback(
'MUCInvite',
MatchXPath("{%s}message/{%s}x/{%s}invite" % (
self.xmpp.default_ns,
stanza.NS_USER,
stanza.NS_USER
)),
self.handle_groupchat_invite
))
def plugin_end(self): def plugin_end(self):
self.xmpp.plugin['xep_0030'].del_feature(feature=stanza.NS) self.xmpp.plugin['xep_0030'].del_feature(feature=stanza.NS)
@ -112,7 +176,6 @@ class XEP_0045(BasePlugin):
self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg) self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg)
def handle_groupchat_subject(self, msg: Message) -> None: def handle_groupchat_subject(self, msg: Message) -> None:
""" Handle a message coming from a muc indicating """ Handle a message coming from a muc indicating
a change of subject (or announcing it when joining the room) a change of subject (or announcing it when joining the room)
@ -123,143 +186,96 @@ class XEP_0045(BasePlugin):
return None return None
self.xmpp.event('groupchat_subject', msg) self.xmpp.event('groupchat_subject', msg)
def jid_in_room(self, room, jid): def jid_in_room(self, room: JID, jid: JID) -> bool:
for nick in self.rooms[room]: for nick in self.rooms[room]:
entry = self.rooms[room][nick] entry = self.rooms[room][nick]
if entry is not None and entry['jid'].full == jid: if entry is not None and entry['jid'].full == jid:
return True return True
return False return False
def get_nick(self, room, jid): def get_nick(self, room: JID, jid: JID) -> Optional[str]:
for nick in self.rooms[room]: for nick in self.rooms[room]:
entry = self.rooms[room][nick] entry = self.rooms[room][nick]
if entry is not None and entry['jid'].full == jid: if entry is not None and entry['jid'].full == jid:
return nick return nick
def configure_room(self, room, form=None, ifrom=None): def join_muc(self, room: JID, nick: str, maxhistory="0", password='',
if form is None: pstatus='', pshow='', pfrom=''):
form = self.get_room_config(room, ifrom=ifrom)
iq = self.xmpp.make_iq_set()
iq['to'] = room
if ifrom is not None:
iq['from'] = ifrom
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
form['type'] = 'submit'
query.append(form)
iq.append(query)
# For now, swallow errors to preserve existing API
try:
result = iq.send()
except IqError:
return False
except IqTimeout:
return False
return True
def join_muc(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None, pfrom=None):
""" Join the specified room, requesting 'maxhistory' lines of history. """ Join the specified room, requesting 'maxhistory' lines of history.
""" """
stanza = self.xmpp.make_presence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow, pfrom=pfrom) stanza = self.xmpp.make_presence(
x = ET.Element('{http://jabber.org/protocol/muc}x') pto="%s/%s" % (room, nick), pstatus=pstatus,
pshow=pshow, pfrom=pfrom
)
stanza.enable('muc_join')
if password: if password:
passelement = ET.Element('{http://jabber.org/protocol/muc}password') stanza['muc_join']['password'] = password
passelement.text = password
x.append(passelement)
if maxhistory: if maxhistory:
history = ET.Element('{http://jabber.org/protocol/muc}history') if maxhistory == "0":
if maxhistory == "0": stanza['muc_join']['history']['maxchars'] = '0'
history.attrib['maxchars'] = maxhistory
else: else:
history.attrib['maxstanzas'] = maxhistory stanza['muc_join']['history']['maxstanzas'] = str(maxhistory)
x.append(history) self.xmpp.send(stanza)
stanza.append(x)
if not wait:
self.xmpp.send(stanza)
else:
#wait for our own room presence back
expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)})
self.xmpp.send(stanza, expect)
self.rooms[room] = {} self.rooms[room] = {}
self.our_nicks[room] = nick self.our_nicks[room] = nick
def destroy(self, room, reason='', altroom = '', ifrom=None): async def destroy(self, room: JID, reason='', altroom='', *,
iq = self.xmpp.make_iq_set() ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
if ifrom is not None: iq = self.xmpp.make_iq_set(ifrom=ifrom, ito=room)
iq['from'] = ifrom iq.enable('mucowner_query')
iq['to'] = room iq['mucowner_query'].enable('destroy')
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy')
if altroom: if altroom:
destroy.attrib['jid'] = altroom iq['mucowner_query']['destroy']['jid'] = altroom
xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason') if reason:
xreason.text = reason iq['mucowner_query']['destroy']['reason'] = reason
destroy.append(xreason) await iq.send(**iqkwargs)
query.append(destroy)
iq.append(query)
# For now, swallow errors to preserve existing API
try:
r = iq.send()
except IqError:
return False
except IqTimeout:
return False
return True
def set_affiliation(self, room, jid=None, nick=None, affiliation='member', ifrom=None): async def set_affiliation(self, room: JID, jid: Optional[JID] = None, nick: Optional[str] = None, *, affiliation: str,
ifrom: Optional[JID] = None, **iqkwargs):
""" Change room affiliation.""" """ Change room affiliation."""
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'): if affiliation not in AFFILIATIONS:
raise TypeError raise ValueError('%s is not a valid affiliation' % affiliation)
query = ET.Element('{http://jabber.org/protocol/muc#admin}query') if not any((jid, nick)):
if nick is not None: raise ValueError('One of jid or nick must be set')
item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick}) iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
else: iq.enable('mucadmin_query')
item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':jid}) item = MUCAdminItem()
query.append(item) item['affiliation'] = affiliation
iq = self.xmpp.make_iq_set(query) if nick:
iq['to'] = room item['nick'] = nick
iq['from'] = ifrom if jid:
# For now, swallow errors to preserve existing API item['jid'] = jid
try: iq['mucadmin_query'].append(item)
result = iq.send() await iq.send(**iqkwargs)
except IqError:
return False
except IqTimeout:
return False
return True
def set_role(self, room, nick, role): async def set_role(self, room: JID, nick: str, role: str, *,
ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
""" Change role property of a nick in a room. """ Change role property of a nick in a room.
Typically, roles are temporary (they last only as long as you are in the Typically, roles are temporary (they last only as long as you are in the
room), whereas affiliations are permanent (they last across groupchat room), whereas affiliations are permanent (they last across groupchat
sessions). sessions).
""" """
if role not in ('moderator', 'participant', 'visitor', 'none'): if role not in ROLES:
raise TypeError raise ValueError("Role %s does not exist" % role)
query = ET.Element('{http://jabber.org/protocol/muc#admin}query') iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
item = ET.Element('item', {'role':role, 'nick':nick}) iq.enable('mucadmin_query')
query.append(item) item = MUCAdminItem()
iq = self.xmpp.make_iq_set(query) item['role'] = role
iq['to'] = room item['nick'] = nick
result = iq.send() iq['mucadmin_query'].append(item)
if result is False or result['type'] != 'result': await iq.send(**iqkwargs)
raise ValueError
return True
def invite(self, room, jid, reason='', mfrom=''): def invite(self, room: JID, jid: JID, reason='', *,
mfrom: Optional[JID] = None):
""" Invite a jid to a room.""" """ Invite a jid to a room."""
msg = self.xmpp.make_message(room) msg = self.xmpp.make_message(room, mfrom=mfrom)
msg['from'] = mfrom msg.enable('muc')
x = ET.Element('{http://jabber.org/protocol/muc#user}x') msg['muc']['invite'] = jid
invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
if reason: if reason:
rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason') msg['muc']['invite']['reason'] = reason
rxml.text = reason
invite.append(rxml)
x.append(invite)
msg.append(x)
self.xmpp.send(msg) self.xmpp.send(msg)
def leave_muc(self, room, nick, msg='', pfrom=None): def leave_muc(self, room: JID, nick: str, msg='', pfrom=None):
""" Leave the specified room. """ Leave the specified room.
""" """
if msg: if msg:
@ -268,44 +284,77 @@ class XEP_0045(BasePlugin):
self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom) self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
del self.rooms[room] del self.rooms[room]
def get_room_config(self, room, ifrom=''):
iq = self.xmpp.make_iq_get('http://jabber.org/protocol/muc#owner') async def get_room_config(self, room: JID, ifrom=''):
iq['to'] = room """Get the room config form in 0004 plugin format """
iq['from'] = ifrom iq = self.xmpp.make_iq_get(stanza.NS_OWNER, ito=room, ifrom=ifrom)
# For now, swallow errors to preserve existing API # For now, swallow errors to preserve existing API
try: result = await iq.send()
result = iq.send()
except IqError:
raise ValueError
except IqTimeout:
raise ValueError
form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x') form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
if form is None: if form is None:
raise ValueError raise ValueError("Configuration form not found")
return self.xmpp.plugin['xep_0004'].build_form(form) return self.xmpp.plugin['xep_0004'].build_form(form)
def cancel_config(self, room, ifrom=None): async def cancel_config(self, room: JID, *,
query = ET.Element('{http://jabber.org/protocol/muc#owner}query') ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
"""Cancel a requested config form"""
query = MUCOwnerQuery()
x = ET.Element('{jabber:x:data}x', type='cancel') x = ET.Element('{jabber:x:data}x', type='cancel')
query.append(x) query.append(x)
iq = self.xmpp.make_iq_set(query) iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom)
iq['to'] = room return await iq.send(**iqkwargs)
iq['from'] = ifrom
iq.send()
def set_room_config(self, room, config, ifrom=''): async def set_room_config(self, room: JID, config, *,
query = ET.Element('{http://jabber.org/protocol/muc#owner}query') ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
"""Send a room config form"""
query = MUCOwnerQuery()
config['type'] = 'submit' config['type'] = 'submit'
query.append(config) query.append(config)
iq = self.xmpp.make_iq_set(query) iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom)
iq['to'] = room return await iq.send(**iqkwargs)
iq['from'] = ifrom
iq.send()
def get_joined_rooms(self): async def get_affiliation_list(self, room: JID, affiliation: str, *,
ifrom: Optional[JID] = None, **iqkwargs) -> List[JID]:
""""Get a list of JIDs with the specified affiliation"""
iq = self.xmpp.make_iq_get(stanza.NS_ADMIN, ito=room, ifrom=ifrom)
iq['mucadmin_query']['item']['affiliation'] = affiliation
result = await iq.send(**iqkwargs)
return [item['jid'] for item in result['mucadmin_query']]
async def get_roles_list(self, room: JID, role: str, *,
ifrom: Optional[JID] = None, **iqkwargs) -> List[str]:
""""Get a list of JIDs with the specified role"""
iq = self.xmpp.make_iq_get(stanza.NS_ADMIN, ito=room, ifrom=ifrom)
iq['mucadmin_query']['item']['role'] = role
result = await iq.send(**iqkwargs)
return [item['nick'] for item in result['mucadmin_query']]
async def send_affiliation_list(self, room: JID, affiliations: List[Tuple[JID, str]], *,
ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
"""Send an affiliation delta list"""
iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
for jid, affiliation in affiliations:
item = MUCAdminItem()
item['jid'] = jid
item['affiliation'] = affiliation
iq['mucadmin_query'].append(item)
return await iq.send(**iqkwargs)
async def send_role_list(self, room: JID, roles: List[Tuple[str, str]], *,
ifrom: Optional[JID], **iqkwargs) -> Iq:
"""Send a role delta list"""
iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
for nick, affiliation in roles:
item = MUCAdminItem()
item['nick'] = nick
item['affiliation'] = affiliation
iq['mucadmin_query'].append(item)
return await iq.send(**iqkwargs)
def get_joined_rooms(self) -> List[JID]:
return self.rooms.keys() return self.rooms.keys()
def get_our_jid_in_room(self, room_jid): def get_our_jid_in_room(self, room_jid: JID) -> str:
""" Return the jid we're using in a room. """ Return the jid we're using in a room.
""" """
return "%s/%s" % (room_jid, self.our_nicks[room_jid]) return "%s/%s" % (room_jid, self.our_nicks[room_jid])
@ -319,19 +368,15 @@ class XEP_0045(BasePlugin):
else: else:
return None return None
def get_roster(self, room): def get_roster(self, room: JID) -> List[str]:
""" Get the list of nicks in a room. """ Get the list of nicks in a room.
""" """
if room not in self.rooms.keys(): if room not in self.rooms.keys():
return None return None
return self.rooms[room].keys() return self.rooms[room].keys()
def get_users_by_affiliation(cls, room, affiliation='member', ifrom=None): def get_users_by_affiliation(self, room: JID, affiliation='member', *, ifrom: Optional[JID] = None):
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'): # Preserve old API
if affiliation not in AFFILIATIONS:
raise TypeError raise TypeError
query = ET.Element('{http://jabber.org/protocol/muc#admin}query') return self.get_affiliation_list(room, affiliation, ifrom=ifrom)
item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation': affiliation})
query.append(item)
iq = cls.xmpp.Iq(sto=room, sfrom=ifrom, stype='get')
iq.append(query)
return iq.send()

View file

@ -147,3 +147,52 @@ class MUCMessage(MUCBase):
</x> </x>
</message> </message>
''' '''
class MUCJoin(ElementBase):
name = 'x'
namespace = NS
plugin_attrib = 'muc_join'
interfaces = {'password'}
sub_interfaces = {'password'}
class MUCInvite(ElementBase):
name = 'invite'
plugin_attrib = 'invite'
namespace = NS_USER
interfaces = {'to', 'reason'}
sub_interfaces = {'reason'}
class MUCHistory(ElementBase):
name = 'history'
plugin_attrib = 'history'
namespace = NS
interfaces = {'maxchars', 'maxstanzas', 'since', 'seconds'}
class MUCOwnerQuery(ElementBase):
name = 'query'
plugin_attrib = 'mucowner_query'
namespace = NS_OWNER
class MUCOwnerDestroy(ElementBase):
name = 'destroy'
plugin_attrib = 'destroy'
interfaces = {'reason', 'jid'}
sub_interfaces = {'reason'}
class MUCAdminQuery(ElementBase):
name = 'query'
plugin_attrib = 'mucadmin_query'
namespace = NS_ADMIN
class MUCAdminItem(ElementBase):
namespace = NS_ADMIN
name = 'item'
plugin_attrib = 'item'
interfaces = {'role', 'affiliation', 'nick', 'jid'}