
Specifically, previously, each MUC would be added as a roster item, and then each join presence would be counted as a resource of that item, triggering 1 to 5 events and more backend logic in slixmpp. As a result, joining big rooms is tremendously slow, (JID() calls, event() calls, __getitem__ calls for nothing), and takes RAM (a quick tracemalloc tells me around 1 MiB for 3500 participants, i.e. 2 big IRC rooms). Those resources may not necessarily be cleaned properly, leading to memory leaks on long-term usage. This is a micro-optimization that adds an attribute to roster items so that MUC room events can be ignored safely while not affecting common roster usage.
420 lines
15 KiB
Python
420 lines
15 KiB
Python
"""
|
|
Slixmpp: The Slick XMPP Library
|
|
Copyright (C) 2010 Nathanael C. Fritz
|
|
This file is part of Slixmpp.
|
|
|
|
See the file LICENSE for copying permission.
|
|
"""
|
|
from __future__ import with_statement
|
|
|
|
import logging
|
|
|
|
from slixmpp import Presence
|
|
from slixmpp.plugins import BasePlugin, register_plugin
|
|
from slixmpp.xmlstream import register_stanza_plugin, ElementBase, JID, ET
|
|
from slixmpp.xmlstream.handler.callback import Callback
|
|
from slixmpp.xmlstream.matcher.xpath import MatchXPath
|
|
from slixmpp.xmlstream.matcher.xmlmask import MatchXMLMask
|
|
from slixmpp.exceptions import IqError, IqTimeout
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class MUCPresence(ElementBase):
|
|
name = 'x'
|
|
namespace = 'http://jabber.org/protocol/muc#user'
|
|
plugin_attrib = 'muc'
|
|
interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room'))
|
|
affiliations = set(('', ))
|
|
roles = set(('', ))
|
|
|
|
def getXMLItem(self):
|
|
item = self.xml.find('{http://jabber.org/protocol/muc#user}item')
|
|
if item is None:
|
|
item = ET.Element('{http://jabber.org/protocol/muc#user}item')
|
|
self.xml.append(item)
|
|
return item
|
|
|
|
def getAffiliation(self):
|
|
#TODO if no affilation, set it to the default and return default
|
|
item = self.getXMLItem()
|
|
return item.get('affiliation', '')
|
|
|
|
def setAffiliation(self, value):
|
|
item = self.getXMLItem()
|
|
#TODO check for valid affiliation
|
|
item.attrib['affiliation'] = value
|
|
return self
|
|
|
|
def delAffiliation(self):
|
|
item = self.getXMLItem()
|
|
#TODO set default affiliation
|
|
if 'affiliation' in item.attrib: del item.attrib['affiliation']
|
|
return self
|
|
|
|
def getJid(self):
|
|
item = self.getXMLItem()
|
|
return JID(item.get('jid', ''))
|
|
|
|
def setJid(self, value):
|
|
item = self.getXMLItem()
|
|
if not isinstance(value, str):
|
|
value = str(value)
|
|
item.attrib['jid'] = value
|
|
return self
|
|
|
|
def delJid(self):
|
|
item = self.getXMLItem()
|
|
if 'jid' in item.attrib: del item.attrib['jid']
|
|
return self
|
|
|
|
def getRole(self):
|
|
item = self.getXMLItem()
|
|
#TODO get default role, set default role if none
|
|
return item.get('role', '')
|
|
|
|
def setRole(self, value):
|
|
item = self.getXMLItem()
|
|
#TODO check for valid role
|
|
item.attrib['role'] = value
|
|
return self
|
|
|
|
def delRole(self):
|
|
item = self.getXMLItem()
|
|
#TODO set default role
|
|
if 'role' in item.attrib: del item.attrib['role']
|
|
return self
|
|
|
|
def getNick(self):
|
|
return self.parent()['from'].resource
|
|
|
|
def getRoom(self):
|
|
return self.parent()['from'].bare
|
|
|
|
def setNick(self, value):
|
|
log.warning("Cannot set nick through mucpresence plugin.")
|
|
return self
|
|
|
|
def setRoom(self, value):
|
|
log.warning("Cannot set room through mucpresence plugin.")
|
|
return self
|
|
|
|
def delNick(self):
|
|
log.warning("Cannot delete nick through mucpresence plugin.")
|
|
return self
|
|
|
|
def delRoom(self):
|
|
log.warning("Cannot delete room through mucpresence plugin.")
|
|
return self
|
|
|
|
|
|
class XEP_0045(BasePlugin):
|
|
|
|
"""
|
|
Implements XEP-0045 Multi-User Chat
|
|
"""
|
|
|
|
name = 'xep_0045'
|
|
description = 'XEP-0045: Multi-User Chat'
|
|
dependencies = set(['xep_0030', 'xep_0004'])
|
|
|
|
def plugin_init(self):
|
|
self.rooms = {}
|
|
self.ourNicks = {}
|
|
self.xep = '0045'
|
|
# load MUC support in presence stanzas
|
|
register_stanza_plugin(Presence, MUCPresence)
|
|
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,
|
|
'http://jabber.org/protocol/muc#user',
|
|
'http://jabber.org/protocol/muc#user')), self.handle_groupchat_invite))
|
|
|
|
def plugin_end(self):
|
|
self.xmpp.plugin['xep_0030'].del_feature(feature='http://jabber.org/protocol/muc')
|
|
|
|
def session_bind(self, jid):
|
|
self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/muc')
|
|
|
|
def handle_groupchat_invite(self, inv):
|
|
""" Handle an invite into a muc.
|
|
"""
|
|
logging.debug("MUC invite to %s from %s: %s", inv['to'], inv["from"], inv)
|
|
if inv['from'] not in self.rooms.keys():
|
|
self.xmpp.event("groupchat_invite", inv)
|
|
|
|
def handle_config_change(self, msg):
|
|
"""Handle a MUC configuration change (with status code)."""
|
|
self.xmpp.event('groupchat_config_status', msg)
|
|
self.xmpp.event('muc::%s::config_status' % msg['from'].bare , msg)
|
|
|
|
def handle_groupchat_presence(self, pr):
|
|
""" Handle a presence in a muc.
|
|
"""
|
|
got_offline = False
|
|
got_online = False
|
|
if pr['muc']['room'] not in self.rooms.keys():
|
|
return
|
|
self.xmpp.roster[pr['from']].ignore_updates = True
|
|
entry = pr['muc'].get_stanza_values()
|
|
entry['show'] = pr['show']
|
|
entry['status'] = pr['status']
|
|
entry['alt_nick'] = pr['nick']
|
|
if pr['type'] == 'unavailable':
|
|
if entry['nick'] in self.rooms[entry['room']]:
|
|
del self.rooms[entry['room']][entry['nick']]
|
|
got_offline = True
|
|
else:
|
|
if entry['nick'] not in self.rooms[entry['room']]:
|
|
got_online = True
|
|
self.rooms[entry['room']][entry['nick']] = entry
|
|
log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry)
|
|
self.xmpp.event("groupchat_presence", pr)
|
|
self.xmpp.event("muc::%s::presence" % entry['room'], pr)
|
|
if got_offline:
|
|
self.xmpp.event("muc::%s::got_offline" % entry['room'], pr)
|
|
if got_online:
|
|
self.xmpp.event("muc::%s::got_online" % entry['room'], pr)
|
|
|
|
def handle_groupchat_message(self, msg):
|
|
""" Handle a message event in a muc.
|
|
"""
|
|
self.xmpp.event('groupchat_message', msg)
|
|
self.xmpp.event("muc::%s::message" % msg['from'].bare, msg)
|
|
|
|
def handle_groupchat_error_message(self, msg):
|
|
""" Handle a message error event in a muc.
|
|
"""
|
|
self.xmpp.event('groupchat_message_error', msg)
|
|
self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg)
|
|
|
|
|
|
|
|
def handle_groupchat_subject(self, msg):
|
|
""" Handle a message coming from a muc indicating
|
|
a change of subject (or announcing it when joining the room)
|
|
"""
|
|
self.xmpp.event('groupchat_subject', msg)
|
|
|
|
def jidInRoom(self, room, jid):
|
|
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 getNick(self, room, jid):
|
|
for nick in self.rooms[room]:
|
|
entry = self.rooms[room][nick]
|
|
if entry is not None and entry['jid'].full == jid:
|
|
return nick
|
|
|
|
def configureRoom(self, room, form=None, ifrom=None):
|
|
if form is None:
|
|
form = self.getRoomConfig(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 = form.getXML('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 joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None, pfrom=None):
|
|
""" 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')
|
|
if password:
|
|
passelement = ET.Element('{http://jabber.org/protocol/muc}password')
|
|
passelement.text = password
|
|
x.append(passelement)
|
|
if maxhistory:
|
|
history = ET.Element('{http://jabber.org/protocol/muc}history')
|
|
if maxhistory == "0":
|
|
history.attrib['maxchars'] = maxhistory
|
|
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)
|
|
self.rooms[room] = {}
|
|
self.ourNicks[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')
|
|
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
|
|
|
|
def setAffiliation(self, room, jid=None, nick=None, affiliation='member', ifrom=None):
|
|
""" 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
|
|
|
|
def setRole(self, room, nick, role):
|
|
""" 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
|
|
|
|
def invite(self, room, jid, reason='', mfrom=''):
|
|
""" 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})
|
|
if reason:
|
|
rxml = ET.Element('{http://jabber.org/protocol/muc#user}reason')
|
|
rxml.text = reason
|
|
invite.append(rxml)
|
|
x.append(invite)
|
|
msg.append(x)
|
|
self.xmpp.send(msg)
|
|
|
|
def leaveMUC(self, room, nick, msg='', pfrom=None):
|
|
""" Leave the specified room.
|
|
"""
|
|
if msg:
|
|
self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg, pfrom=pfrom)
|
|
else:
|
|
self.xmpp.send_presence(pshow='unavailable', pto="%s/%s" % (room, nick), pfrom=pfrom)
|
|
del self.rooms[room]
|
|
|
|
def getRoomConfig(self, room, ifrom=''):
|
|
iq = self.xmpp.make_iq_get('http://jabber.org/protocol/muc#owner')
|
|
iq['to'] = room
|
|
iq['from'] = ifrom
|
|
# For now, swallow errors to preserve existing API
|
|
try:
|
|
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')
|
|
if form is None:
|
|
raise ValueError
|
|
return self.xmpp.plugin['xep_0004'].buildForm(form)
|
|
|
|
def cancelConfig(self, room, ifrom=None):
|
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
|
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()
|
|
|
|
def setRoomConfig(self, room, config, ifrom=''):
|
|
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
|
|
x = config.getXML('submit')
|
|
query.append(x)
|
|
iq = self.xmpp.make_iq_set(query)
|
|
iq['to'] = room
|
|
iq['from'] = ifrom
|
|
iq.send()
|
|
|
|
def getJoinedRooms(self):
|
|
return self.rooms.keys()
|
|
|
|
def getOurJidInRoom(self, roomJid):
|
|
""" Return the jid we're using in a room.
|
|
"""
|
|
return "%s/%s" % (roomJid, self.ourNicks[roomJid])
|
|
|
|
def getJidProperty(self, room, nick, jidProperty):
|
|
""" Get the property of a nick in a room, such as its 'jid' or 'affiliation'
|
|
If not found, return None.
|
|
"""
|
|
if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]:
|
|
return self.rooms[room][nick][jidProperty]
|
|
else:
|
|
return None
|
|
|
|
def getRoster(self, room):
|
|
""" Get the list of nicks in a room.
|
|
"""
|
|
if room not in self.rooms.keys():
|
|
return None
|
|
return self.rooms[room].keys()
|
|
|
|
def getUsersByAffiliation(cls, room, affiliation='member', ifrom=None):
|
|
if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'):
|
|
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()
|
|
|
|
|
|
xep_0045 = XEP_0045
|
|
register_plugin(XEP_0045)
|