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
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.xmlstream import register_stanza_plugin, ET
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.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__)
AFFILIATIONS = ('outcast', 'member', 'admin', 'owner', 'none')
ROLES = ('moderator', 'participant', 'visitor', 'none')
class XEP_0045(BasePlugin):
@ -39,19 +61,61 @@ class XEP_0045(BasePlugin):
def plugin_init(self):
self.rooms = {}
self.our_nicks = {}
self.xep = '0045'
# load MUC support in presence stanzas
register_stanza_plugin(Presence, MUCPresence)
register_stanza_plugin(Presence, MUCJoin)
register_stanza_plugin(MUCJoin, MUCHistory)
register_stanza_plugin(Message, MUCMessage)
self.xmpp.register_handler(Callback('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))
register_stanza_plugin(Iq, MUCAdminQuery)
register_stanza_plugin(Iq, MUCOwnerQuery)
register_stanza_plugin(MUCOwnerQuery, MUCOwnerDestroy)
register_stanza_plugin(MUCAdminQuery, MUCAdminItem, iterable=True)
# Register handlers
self.xmpp.register_handler(
Callback(
'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):
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)
def handle_groupchat_subject(self, msg: Message) -> None:
""" Handle a message coming from a muc indicating
a change of subject (or announcing it when joining the room)
@ -123,143 +186,96 @@ class XEP_0045(BasePlugin):
return None
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]:
entry = self.rooms[room][nick]
if entry is not None and entry['jid'].full == jid:
return True
return False
def get_nick(self, room, jid):
def get_nick(self, room: JID, jid: JID) -> Optional[str]:
for nick in self.rooms[room]:
entry = self.rooms[room][nick]
if entry is not None and entry['jid'].full == jid:
return nick
def configure_room(self, room, form=None, ifrom=None):
if form is None:
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):
def join_muc(self, room: JID, nick: str, maxhistory="0", password='',
pstatus='', pshow='', pfrom=''):
""" 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)
x = ET.Element('{http://jabber.org/protocol/muc}x')
stanza = self.xmpp.make_presence(
pto="%s/%s" % (room, nick), pstatus=pstatus,
pshow=pshow, pfrom=pfrom
)
stanza.enable('muc_join')
if password:
passelement = ET.Element('{http://jabber.org/protocol/muc}password')
passelement.text = password
x.append(passelement)
stanza['muc_join']['password'] = password
if maxhistory:
history = ET.Element('{http://jabber.org/protocol/muc}history')
if maxhistory == "0":
history.attrib['maxchars'] = maxhistory
if maxhistory == "0":
stanza['muc_join']['history']['maxchars'] = '0'
else:
history.attrib['maxstanzas'] = maxhistory
x.append(history)
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)
stanza['muc_join']['history']['maxstanzas'] = str(maxhistory)
self.xmpp.send(stanza)
self.rooms[room] = {}
self.our_nicks[room] = nick
def destroy(self, room, reason='', altroom = '', ifrom=None):
iq = self.xmpp.make_iq_set()
if ifrom is not None:
iq['from'] = ifrom
iq['to'] = room
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
destroy = ET.Element('{http://jabber.org/protocol/muc#owner}destroy')
async def destroy(self, room: JID, reason='', altroom='', *,
ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
iq = self.xmpp.make_iq_set(ifrom=ifrom, ito=room)
iq.enable('mucowner_query')
iq['mucowner_query'].enable('destroy')
if altroom:
destroy.attrib['jid'] = altroom
xreason = ET.Element('{http://jabber.org/protocol/muc#owner}reason')
xreason.text = reason
destroy.append(xreason)
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
iq['mucowner_query']['destroy']['jid'] = altroom
if reason:
iq['mucowner_query']['destroy']['reason'] = reason
await iq.send(**iqkwargs)
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."""
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
raise TypeError
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
if nick is not None:
item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'nick':nick})
else:
item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation':affiliation, 'jid':jid})
query.append(item)
iq = self.xmpp.make_iq_set(query)
iq['to'] = room
iq['from'] = ifrom
# For now, swallow errors to preserve existing API
try:
result = iq.send()
except IqError:
return False
except IqTimeout:
return False
return True
if affiliation not in AFFILIATIONS:
raise ValueError('%s is not a valid affiliation' % affiliation)
if not any((jid, nick)):
raise ValueError('One of jid or nick must be set')
iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
iq.enable('mucadmin_query')
item = MUCAdminItem()
item['affiliation'] = affiliation
if nick:
item['nick'] = nick
if jid:
item['jid'] = jid
iq['mucadmin_query'].append(item)
await iq.send(**iqkwargs)
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.
Typically, roles are temporary (they last only as long as you are in the
room), whereas affiliations are permanent (they last across groupchat
sessions).
"""
if role not in ('moderator', 'participant', 'visitor', 'none'):
raise TypeError
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
item = ET.Element('item', {'role':role, 'nick':nick})
query.append(item)
iq = self.xmpp.make_iq_set(query)
iq['to'] = room
result = iq.send()
if result is False or result['type'] != 'result':
raise ValueError
return True
if role not in ROLES:
raise ValueError("Role %s does not exist" % role)
iq = self.xmpp.make_iq_set(ito=room, ifrom=ifrom)
iq.enable('mucadmin_query')
item = MUCAdminItem()
item['role'] = role
item['nick'] = nick
iq['mucadmin_query'].append(item)
await iq.send(**iqkwargs)
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."""
msg = self.xmpp.make_message(room)
msg['from'] = mfrom
x = ET.Element('{http://jabber.org/protocol/muc#user}x')
invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid})
msg = self.xmpp.make_message(room, mfrom=mfrom)
msg.enable('muc')
msg['muc']['invite'] = jid
if reason:
rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason')
rxml.text = reason
invite.append(rxml)
x.append(invite)
msg.append(x)
msg['muc']['invite']['reason'] = reason
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.
"""
if msg:
@ -268,44 +284,77 @@ class XEP_0045(BasePlugin):
self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
del self.rooms[room]
def get_room_config(self, room, ifrom=''):
iq = self.xmpp.make_iq_get('http://jabber.org/protocol/muc#owner')
iq['to'] = room
iq['from'] = ifrom
async def get_room_config(self, room: JID, ifrom=''):
"""Get the room config form in 0004 plugin format """
iq = self.xmpp.make_iq_get(stanza.NS_OWNER, ito=room, ifrom=ifrom)
# For now, swallow errors to preserve existing API
try:
result = iq.send()
except IqError:
raise ValueError
except IqTimeout:
raise ValueError
result = await iq.send()
form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x')
if form is None:
raise ValueError
raise ValueError("Configuration form not found")
return self.xmpp.plugin['xep_0004'].build_form(form)
def cancel_config(self, room, ifrom=None):
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
async def cancel_config(self, room: JID, *,
ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
"""Cancel a requested config form"""
query = MUCOwnerQuery()
x = ET.Element('{jabber:x:data}x', type='cancel')
query.append(x)
iq = self.xmpp.make_iq_set(query)
iq['to'] = room
iq['from'] = ifrom
iq.send()
iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom)
return await iq.send(**iqkwargs)
def set_room_config(self, room, config, ifrom=''):
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
async def set_room_config(self, room: JID, config, *,
ifrom: Optional[JID] = None, **iqkwargs) -> Iq:
"""Send a room config form"""
query = MUCOwnerQuery()
config['type'] = 'submit'
query.append(config)
iq = self.xmpp.make_iq_set(query)
iq['to'] = room
iq['from'] = ifrom
iq.send()
iq = self.xmpp.make_iq_set(query, ito=room, ifrom=ifrom)
return await iq.send(**iqkwargs)
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()
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 "%s/%s" % (room_jid, self.our_nicks[room_jid])
@ -319,19 +368,15 @@ class XEP_0045(BasePlugin):
else:
return None
def get_roster(self, room):
def get_roster(self, room: JID) -> List[str]:
""" Get the list of nicks in a room.
"""
if room not in self.rooms.keys():
return None
return self.rooms[room].keys()
def get_users_by_affiliation(cls, room, affiliation='member', ifrom=None):
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
def get_users_by_affiliation(self, room: JID, affiliation='member', *, ifrom: Optional[JID] = None):
# Preserve old API
if affiliation not in AFFILIATIONS:
raise TypeError
query = ET.Element('{http://jabber.org/protocol/muc#admin}query')
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()
return self.get_affiliation_list(room, affiliation, ifrom=ifrom)

View file

@ -147,3 +147,52 @@ class MUCMessage(MUCBase):
</x>
</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'}