Mercurial > libervia-backend
view src/plugins/plugin_xep_0045.py @ 1946:827bb478d8af
primitivus: paste is now detected, and put in edit bar, so use can check and validate it
author | Goffi <goffi@goffi.org> |
---|---|
date | Thu, 21 Apr 2016 20:20:40 +0200 |
parents | 2daf7b4c6756 |
children | a2bc5089c2eb |
line wrap: on
line source
#!/usr/bin/env python2 # -*- coding: utf-8 -*- # SAT plugin for managing xep-0045 # Copyright (C) 2009-2016 Jérôme Poisson (goffi@goffi.org) # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. from sat.core.i18n import _, D_ from sat.core.constants import Const as C from sat.core.log import getLogger log = getLogger(__name__) from twisted.internet import defer from twisted.words.protocols.jabber import jid from sat.core import exceptions from sat.memory import memory import uuid import copy from wokkel import muc, disco, iwokkel from sat.tools import xml_tools from zope.interface import implements PLUGIN_INFO = { "name": "XEP 0045 Plugin", "import_name": "XEP-0045", "type": "XEP", "protocols": ["XEP-0045"], "dependencies": [], "recommendations": [C.TEXT_CMDS], "main": "XEP_0045", "handler": "yes", "description": _("""Implementation of Multi-User Chat""") } NS_MUC = 'http://jabber.org/protocol/muc' AFFILIATIONS = ('owner', 'admin', 'member', 'none', 'outcast') CONFIG_SECTION = u'plugin muc' default_conf = {"default_muc": u'sat@chat.jabberfr.org'} class UnknownRoom(Exception): pass class AlreadyJoinedRoom(Exception): pass class NotReadyYet(Exception): pass class XEP_0045(object): # TODO: this plugin is messy, need a big cleanup/refactoring def __init__(self, host): log.info(_("Plugin XEP_0045 initialization")) self.host = host self.clients = {} self._sessions = memory.Sessions() host.bridge.addMethod("joinMUC", ".plugin", in_sign='ssa{ss}s', out_sign='s', method=self._join, async=True) host.bridge.addMethod("mucNick", ".plugin", in_sign='sss', out_sign='', method=self.mucNick) host.bridge.addMethod("mucLeave", ".plugin", in_sign='ss', out_sign='', method=self.mucLeave, async=True) host.bridge.addMethod("getRoomsJoined", ".plugin", in_sign='s', out_sign='a(sass)', method=self.getRoomsJoined) host.bridge.addMethod("getRoomsSubjects", ".plugin", in_sign='s', out_sign='a(ss)', method=self.getRoomsSubjects) host.bridge.addMethod("getUniqueRoomName", ".plugin", in_sign='ss', out_sign='s', method=self._getUniqueName) host.bridge.addMethod("configureRoom", ".plugin", in_sign='ss', out_sign='s', method=self._configureRoom, async=True) host.bridge.addMethod("getDefaultMUC", ".plugin", in_sign='', out_sign='s', method=self.getDefaultMUC) host.bridge.addSignal("roomJoined", ".plugin", signature='sasss') # args: room_jid, room_nicks, user_nick, profile host.bridge.addSignal("roomLeft", ".plugin", signature='ss') # args: room_jid, profile host.bridge.addSignal("roomUserJoined", ".plugin", signature='ssa{ss}s') # args: room_jid, user_nick, user_data, profile host.bridge.addSignal("roomUserLeft", ".plugin", signature='ssa{ss}s') # args: room_jid, user_nick, user_data, profile host.bridge.addSignal("roomUserChangedNick", ".plugin", signature='ssss') # args: room_jid, old_nick, new_nick, profile host.bridge.addSignal("roomNewSubject", ".plugin", signature='sss') # args: room_jid, subject, profile self.__submit_conf_id = host.registerCallback(self._submitConfiguration, with_data=True) host.importMenu((D_("MUC"), D_("configure")), self._configureRoomMenu, security_limit=0, help_string=D_("Configure Multi-User Chat room"), type_=C.MENU_ROOM) try: self.host.plugins[C.TEXT_CMDS].registerTextCommands(self) self.host.plugins[C.TEXT_CMDS].addWhoIsCb(self._whois, 100) except KeyError: log.info(_("Text commands not available")) host.trigger.add("presence_available", self.presenceTrigger) def profileConnected(self, profile): def assign_service(service): client = self.host.getClient(profile) client.muc_service = service return self.getMUCService(profile=profile).addCallback(assign_service) def checkClient(self, profile): """Check if the profile is connected and has used the MUC feature. If profile was using MUC feature but is now disconnected, remove it from the client list. @param profile: profile to check @return: True if the profile is connected and has used the MUC feature, else False""" if not profile or profile not in self.clients or not self.host.isConnected(profile): log.error(_(u'Unknown or disconnected profile (%s)') % profile) if profile in self.clients: del self.clients[profile] return False return True def getProfileAssertInRoom(self, room_jid, profile_key): """Retrieve the profile name, assert that it's connected and participating in the given room. @param room_jid (JID): room JID @param profile_key (str): %(doc_profile_key) @return: the profile name """ profile = self.host.memory.getProfileName(profile_key) if not self.checkClient(profile): raise exceptions.ProfileUnknownError("Unknown or disconnected profile") if room_jid not in self.clients[profile].joined_rooms: raise UnknownRoom("This room has not been joined") return profile def __room_joined(self, room, profile): """Called when the user is in the requested room""" def _sendBridgeSignal(ignore=None): self.host.bridge.roomJoined(room.roomJID.userhost(), [user.nick for user in room.roster.values()], room.nick, profile) self.clients[profile].joined_rooms[room.roomJID] = room if room.locked: # FIXME: the current behaviour is to create an instant room # and send the signal only when the room is unlocked # a proper configuration management should be done print "room locked !" self.clients[profile].configure(room.roomJID, {}).addCallbacks(_sendBridgeSignal, lambda x: log.error(_(u'Error while configuring the room'))) else: _sendBridgeSignal() return room def __err_joining_room(self, failure, room_jid, nick, history_options, password, profile): """Called when something is going wrong when joining the room""" if hasattr(failure.value, "condition") and failure.value.condition == 'conflict': # we have a nickname conflict, we try again with "_" suffixed to current nickname nick += '_' return self.clients[profile].join(room_jid, nick, history_options, password).addCallbacks(self.__room_joined, self.__err_joining_room, callbackKeywords={'profile': profile}, errbackArgs=[room_jid, nick, history_options, password, profile]) mess = D_("Error while joining the room %s" % room_jid.userhost()) try: mess += " with condition '%s'" % failure.value.condition except AttributeError: pass log.error(mess) self.host.bridge.newAlert(mess, D_("Group chat error"), "ERROR", profile) raise failure def isRoom(self, entity_bare, profile_key): """Tell if a bare entity is a MUC room. @param entity_bare (jid.JID): bare entity @param profile_key (unicode): %(doc_profile_key)s @return bool """ profile = self.host.memory.getProfileName(profile_key) return entity_bare in self.clients[profile].joined_rooms def getRoomsJoined(self, profile_key=C.PROF_KEY_NONE): """Return room where user is""" profile = self.host.memory.getProfileName(profile_key) result = [] if not self.checkClient(profile): return result for room in self.clients[profile].joined_rooms.values(): result.append((room.roomJID.userhost(), [user.nick for user in room.roster.values()], room.nick)) return result def getRoomNick(self, room_jid, profile_key=C.PROF_KEY_NONE): """return nick used in room by user @param room_jid (jid.JID): JID of the room @profile_key: profile @return: nick or empty string in case of error""" profile = self.host.memory.getProfileName(profile_key) if not self.checkClient(profile) or room_jid not in self.clients[profile].joined_rooms: return '' return self.clients[profile].joined_rooms[room_jid].nick def getRoomNickOfUser(self, room, user_jid, secure=True): """Returns the nick of the given user in the room. @param room (wokkel.muc.Room): the room @param user (jid.JID): bare JID of the user @param secure (bool): set to True for a secure check @return: unicode or None if the user didn't join the room. """ for user in room.roster.values(): if user.entity is not None: if user.entity.userhostJID() == user_jid.userhostJID(): return user.nick elif not secure: # FIXME: this is NOT ENOUGH to check an identity!! # See in which conditions user.entity could be None. if user.nick == user_jid.user: return user.nick return None def getRoomNicksOfUsers(self, room, users=[], secure=True): """Returns the nicks of the given users in the room. @param room (wokkel.muc.Room): the room @param users (list[jid.JID]): list of users @param secure (True): set to True for a secure check @return: a couple (x, y) with: - x (list[unicode]): nicks of the users who are in the room - y (list[jid.JID]): JID of the missing users. """ nicks = [] missing = [] for user in users: nick = self.getRoomNickOfUser(room, user, secure) if nick is None: missing.append(user) else: nicks.append(nick) return nicks, missing def _configureRoom(self, room_jid_s, profile_key=C.PROF_KEY_NONE): d = self.configureRoom(jid.JID(room_jid_s), profile_key) d.addCallback(lambda xmlui: xmlui.toXml()) return d def _configureRoomMenu(self, menu_data, profile): """Return room configuration form @param menu_data: %(menu_data)s @param profile: %(doc_profile)s """ try: room_jid = jid.JID(menu_data['room_jid']) except KeyError: log.error(_("room_jid key is not present !")) return defer.fail(exceptions.DataError) def xmluiReceived(xmlui): return {"xmlui": xmlui.toXml()} return self.configureRoom(room_jid, profile).addCallback(xmluiReceived) def configureRoom(self, room_jid, profile_key=C.PROF_KEY_NONE): """return the room configuration form @param room: jid of the room to configure @param profile_key: %(doc_profile_key)s @return: configuration form as XMLUI """ profile = self.getProfileAssertInRoom(room_jid, profile_key) def config2XMLUI(result): if not result: return "" session_id, session_data = self._sessions.newSession(profile=profile) session_data["room_jid"] = room_jid xmlui = xml_tools.dataForm2XMLUI(result, submit_id=self.__submit_conf_id) xmlui.session_id = session_id return xmlui d = self.clients[profile].getConfiguration(room_jid) d.addCallback(config2XMLUI) return d def _submitConfiguration(self, raw_data, profile): try: session_data = self._sessions.profileGet(raw_data["session_id"], profile) except KeyError: log.warning(D_("Session ID doesn't exist, session has probably expired.")) _dialog = xml_tools.XMLUI('popup', title=D_('Room configuration failed')) _dialog.addText(D_("Session ID doesn't exist, session has probably expired.")) return defer.succeed({'xmlui': _dialog.toXml()}) data = xml_tools.XMLUIResult2DataFormResult(raw_data) d = self.clients[profile].configure(session_data['room_jid'], data) _dialog = xml_tools.XMLUI('popup', title=D_('Room configuration succeed')) _dialog.addText(D_("The new settings have been saved.")) d.addCallback(lambda ignore: {'xmlui': _dialog.toXml()}) del self._sessions[raw_data["session_id"]] return d def isNickInRoom(self, room_jid, nick, profile): """Tell if a nick is currently present in a room""" profile = self.getProfileAssertInRoom(room_jid, profile) return self.clients[profile].joined_rooms[room_jid].inRoster(muc.User(nick)) def getRoomsSubjects(self, profile_key=C.PROF_KEY_NONE): """Return received subjects of rooms""" profile = self.host.memory.getProfileName(profile_key) if not self.checkClient(profile): return [] return self.clients[profile].rec_subjects.values() @defer.inlineCallbacks def getMUCService(self, jid_=None, profile=C.PROF_KEY_NONE): """Return first found MUC service of an entity @param jid_: entity which may have a MUC service, or None for our own server @param profile: %(doc_profile)s """ muc_service = None services = yield self.host.findServiceEntities("conference", "text", jid_, profile=profile) for service in services: if ".irc." not in service.userhost(): # FIXME: # This ugly hack is here to avoid an issue with openfire: the IRC gateway # use "conference/text" identity (instead of "conference/irc") muc_service = service break defer.returnValue(muc_service) def _getUniqueName(self, muc_service="", profile_key=C.PROF_KEY_NONE): return self.getUniqueName(muc_service or None, profile_key).full() def getUniqueName(self, muc_service=None, profile_key=C.PROF_KEY_NONE): """Return unique name for a room, avoiding collision @param muc_service (jid.JID) : leave empty string to use the default service @return: jid.JID (unique room bare JID) """ # TODO: we should use #RFC-0045 10.1.4 when available here client = self.host.getClient(profile_key) room_name = uuid.uuid1() if muc_service is None: try: muc_service = client.muc_service except AttributeError: raise NotReadyYet("Main server MUC service has not been checked yet") if muc_service is None: log.warning(_("No MUC service found on main server")) raise exceptions.FeatureNotFound muc_service = muc_service.userhost() return jid.JID("%s@%s" % (room_name, muc_service)) def getDefaultMUC(self): """Return the default MUC. @return: unicode """ return self.host.memory.getConfig(CONFIG_SECTION, 'default_muc', default_conf['default_muc']) def join(self, room_jid, nick, options, profile_key=C.PROF_KEY_NONE): def _errDeferred(exc_obj=Exception, txt='Error while joining room'): d = defer.Deferred() d.errback(exc_obj(txt)) return d profile = self.host.memory.getProfileName(profile_key) if not self.checkClient(profile): return _errDeferred() if room_jid in self.clients[profile].joined_rooms: log.warning(_(u'%(profile)s is already in room %(room_jid)s') % {'profile': profile, 'room_jid': room_jid.userhost()}) return _errDeferred(AlreadyJoinedRoom, D_(u"The room has already been joined")) log.info(_(u"[%(profile)s] is joining room %(room)s with nick %(nick)s") % {'profile': profile, 'room': room_jid.userhost(), 'nick': nick}) if "history" in options: history_limit = int(options["history"]) else: history_limit = int(self.host.memory.getParamA(C.HISTORY_LIMIT, 'General', profile_key=profile)) # http://xmpp.org/extensions/xep-0045.html#enter-managehistory history_options = muc.HistoryOptions(maxStanzas=history_limit) password = options["password"] if "password" in options else None return self.clients[profile].join(room_jid, nick, history_options, password).addCallbacks(self.__room_joined, self.__err_joining_room, callbackKeywords={'profile': profile}, errbackArgs=[room_jid, nick, history_options, password, profile]) # FIXME: how to set the cancel method on the Deferred created by wokkel? # This happens when the room is not reachable, e.g. no internet connection: # > /usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py(480)_startRunCallbacks() # -> raise AlreadyCalledError(extra) def _join(self, room_jid_s, nick, options=None, profile_key=C.PROF_KEY_NONE): """join method used by bridge: use the join method, but doesn't return any deferred @return: unicode (the room bare) """ if options is None: options = {} profile = self.host.memory.getProfileName(profile_key) if not self.checkClient(profile): return if room_jid_s: muc_service = self.host.getClient(profile).muc_service try: room_jid = jid.JID(room_jid_s) except (RuntimeError, jid.InvalidFormat, AttributeError): return defer.fail(jid.InvalidFormat(_(u"Invalid room identifier: '%s'. Please give a room short or full identifier like 'room' or 'room@%s'.") % (room_jid_s, unicode(muc_service)))) if not room_jid.user: room_jid.user, room_jid.host = room_jid.host, muc_service else: room_jid = self.getUniqueName(profile_key=profile_key) # TODO: error management + signal in bridge d = self.join(room_jid, nick, options, profile) return d.addCallback(lambda room: room.roomJID.userhost()) def nick(self, room_jid, nick, profile_key): profile = self.getProfileAssertInRoom(room_jid, profile_key) return self.clients[profile].nick(room_jid, nick) def leave(self, room_jid, profile_key): profile = self.getProfileAssertInRoom(room_jid, profile_key) return self.clients[profile].leave(room_jid) def subject(self, room_jid, subject, profile_key): profile = self.getProfileAssertInRoom(room_jid, profile_key) return self.clients[profile].subject(room_jid, subject) def mucNick(self, room_jid_s, nick, profile_key=C.PROF_KEY_NONE): """Change nickname in a room""" return self.nick(jid.JID(room_jid_s), nick, profile_key) def mucLeave(self, room_jid_s, profile_key=C.PROF_KEY_NONE): """Leave a room""" return self.leave(jid.JID(room_jid_s), profile_key) def getHandler(self, profile): self.clients[profile] = SatMUCClient(self) return self.clients[profile] def profileDisconnected(self, profile): try: del self.clients[profile] except KeyError: pass def kick(self, nick, room_jid, options={}, profile_key=C.PROF_KEY_NONE): """ Kick a participant from the room @param nick (str): nick of the user to kick @param room_jid_s (JID): jid of the room @param options (dict): attribute with extra info (reason, password) as in #XEP-0045 @param profile_key (str): %(doc_profile_key)s """ profile = self.getProfileAssertInRoom(room_jid, profile_key) return self.clients[profile].kick(room_jid, nick, reason=options.get('reason', None)) def ban(self, entity_jid, room_jid, options={}, profile_key=C.PROF_KEY_NONE): """ Ban an entity from the room @param entity_jid (JID): bare jid of the entity to be banned @param room_jid_s (JID): jid of the room @param options: attribute with extra info (reason, password) as in #XEP-0045 @param profile_key (str): %(doc_profile_key)s """ assert(not entity_jid.resource) assert(not room_jid.resource) profile = self.getProfileAssertInRoom(room_jid, profile_key) return self.clients[profile].ban(room_jid, entity_jid, reason=options.get('reason', None)) def affiliate(self, entity_jid, room_jid, options=None, profile_key=C.PROF_KEY_NONE): """ Change the affiliation of an entity @param entity_jid (JID): bare jid of the entity @param room_jid_s (JID): jid of the room @param options: attribute with extra info (reason, nick) as in #XEP-0045 @param profile_key (str): %(doc_profile_key)s """ assert(not entity_jid.resource) assert(not room_jid.resource) assert('affiliation' in options) profile = self.getProfileAssertInRoom(room_jid, profile_key) # TODO: handles reason and nick return self.clients[profile].modifyAffiliationList(room_jid, [entity_jid], options['affiliation']) # Text commands # def cmd_nick(self, mess_data, profile): """change nickname @command (group): new_nick - new_nick: new nick to use """ nick = mess_data["unparsed"].strip() if nick: room = mess_data["to"] self.nick(room, nick, profile) return False def cmd_join(self, mess_data, profile): """join a new room @command (all): JID - JID: room to join (on the same service if full jid is not specified) """ if mess_data["unparsed"].strip(): room_jid = self.host.plugins[C.TEXT_CMDS].getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host) nick = (self.getRoomNick(room_jid, profile) or self.host.getClient(profile).jid.user) self.join(room_jid, nick, {}, profile) return False def cmd_leave(self, mess_data, profile): """quit a room @command (group): [ROOM_JID] - ROOM_JID: jid of the room to live (current room if not specified) """ if mess_data["unparsed"].strip(): room = self.host.plugins[C.TEXT_CMDS].getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host) else: room = mess_data["to"] self.leave(room, profile) return False def cmd_part(self, mess_data, profile): """just a synonym of /leave @command (group): [ROOM_JID] - ROOM_JID: jid of the room to live (current room if not specified) """ return self.cmd_leave(mess_data, profile) def cmd_kick(self, mess_data, profile): """kick a room member @command (group): ROOM_NICK - ROOM_NICK: the nick of the person to kick """ options = mess_data["unparsed"].strip().split() try: nick = options[0] assert(self.isNickInRoom(mess_data["to"], nick, profile)) except (IndexError, AssertionError): feedback = _(u"You must provide a member's nick to kick.") self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile) return False d = self.kick(nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, profile) def cb(dummy): feedback_msg = _(u'You have kicked {}').format(nick) if len(options) > 1: feedback_msg += _(u' for the following reason: {}').format(options[1]) self.host.plugins[C.TEXT_CMDS].feedBack(feedback_msg, mess_data, profile) return True d.addCallback(cb) return d def cmd_ban(self, mess_data, profile): """ban an entity from the room @command (group): (JID) [reason] - JID: the JID of the entity to ban - reason: the reason why this entity is being banned """ options = mess_data["unparsed"].strip().split() try: jid_s = options[0] entity_jid = jid.JID(jid_s).userhostJID() assert(entity_jid.user) assert(entity_jid.host) except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError): feedback = _(u"You must provide a valid JID to ban, like in '/ban contact@example.net'") self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile) return False d = self.ban(entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, profile) def cb(dummy): feedback_msg = _(u'You have banned {}').format(entity_jid) if len(options) > 1: feedback_msg += _(u' for the following reason: {}').format(options[1]) self.host.plugins[C.TEXT_CMDS].feedBack(feedback_msg, mess_data, profile) return True d.addCallback(cb) return d def cmd_affiliate(self, mess_data, profile): """affiliate an entity to the room @command (group): (JID) [owner|admin|member|none|outcast] - JID: the JID of the entity to affiliate - owner: grant owner privileges - admin: grant admin privileges - member: grant member privileges - none: reset entity privileges - outcast: ban entity """ options = mess_data["unparsed"].strip().split() try: jid_s = options[0] entity_jid = jid.JID(jid_s).userhostJID() assert(entity_jid.user) assert(entity_jid.host) except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError): feedback = _(u"You must provide a valid JID to affiliate, like in '/affiliate contact@example.net member'") self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile) return False affiliation = options[1] if len(options) > 1 else 'none' if affiliation not in AFFILIATIONS: feedback = _(u"You must provide a valid affiliation: %s") % ' '.join(AFFILIATIONS) self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile) return False d = self.affiliate(entity_jid, mess_data["to"], {'affiliation': affiliation}, profile) def cb(dummy): feedback_msg = _(u'New affiliation for %(entity)s: %(affiliation)s').format(entity=entity_jid, affiliation=affiliation) self.host.plugins[C.TEXT_CMDS].feedBack(feedback_msg, mess_data, profile) return True d.addCallback(cb) return d def cmd_title(self, mess_data, profile): """change room's subject @command (group): title - title: new room subject """ subject = mess_data["unparsed"].strip() if subject: room = mess_data["to"] self.subject(room, subject, profile) return False def cmd_topic(self, mess_data, profile): """just a synonym of /title @command (group): title - title: new room subject """ return self.cmd_title(mess_data, profile) def _whois(self, whois_msg, mess_data, target_jid, profile): """ Add MUC user information to whois """ if mess_data['type'] != "groupchat": return if target_jid.userhostJID() not in self.clients[profile].joined_rooms: log.warning(_("This room has not been joined")) return if not target_jid.resource: return user = self.clients[profile].joined_rooms[target_jid.userhostJID()].getUser(target_jid.resource) whois_msg.append(_("Nickname: %s") % user.nick) if user.entity: whois_msg.append(_("Entity: %s") % user.entity) if user.affiliation != 'none': whois_msg.append(_("Affiliation: %s") % user.affiliation) if user.role != 'none': whois_msg.append(_("Role: %s") % user.role) if user.status: whois_msg.append(_("Status: %s") % user.status) if user.show: whois_msg.append(_("Show: %s") % user.show) def presenceTrigger(self, presence_elt, client): # XXX: shouldn't it be done by the server ?!! muc_client = self.clients[client.profile] for room_jid, room in muc_client.joined_rooms.iteritems(): elt = copy.deepcopy(presence_elt) elt['to'] = room_jid.userhost() + '/' + room.nick client.presence.send(elt) return True class SatMUCClient (muc.MUCClient): implements(iwokkel.IDisco) def __init__(self, plugin_parent): self.plugin_parent = plugin_parent self.host = plugin_parent.host muc.MUCClient.__init__(self) self.rec_subjects = {} self.__changing_nicks = set() # used to keep trace of who is changing nick, # and to discard userJoinedRoom signal in this case print "init SatMUCClient OK" @property def joined_rooms(self): return self._rooms def subject(self, room, subject): return muc.MUCClientProtocol.subject(self, room, subject) def availableReceived(self, presence): """ Available presence was received. """ # XXX: we override MUCClient.availableReceived to fix bugs # (affiliation and role are not set) # FIXME: propose a patch upstream room, user = self._getRoomUser(presence) if room is None: return if user is None: nick = presence.sender.resource user = muc.User(nick, presence.entity) # Update user data user.role = presence.role user.affiliation = presence.affiliation user.status = presence.status user.show = presence.show if room.inRoster(user): self.userUpdatedStatus(room, user, presence.show, presence.status) else: room.addUser(user) self.userJoinedRoom(room, user) def unavailableReceived(self, presence): # XXX: we override this method to manage nickname change # TODO: feed this back to Wokkel """ Unavailable presence was received. If this was received from a MUC room occupant JID, that occupant has left the room. """ room, user = self._getRoomUser(presence) if room is None or user is None: return room.removeUser(user) if muc.STATUS_CODE.NEW_NICK in presence.mucStatuses: self.__changing_nicks.add(presence.nick) self.userChangedNick(room, user, presence.nick) else: self.__changing_nicks.discard(presence.nick) self.userLeftRoom(room, user) def userJoinedRoom(self, room, user): self.host.memory.updateEntityData(room.roomJID, "type", "chatroom", profile_key=self.parent.profile) if user.nick in self.__changing_nicks: self.__changing_nicks.remove(user.nick) else: log.debug(_(u"user %(nick)s has joined room (%(room_id)s)") % {'nick': user.nick, 'room_id': room.occupantJID.userhost()}) if not self.host.trigger.point("MUC user joined", room, user, self.parent.profile): return user_data = {'entity': user.entity.full() if user.entity else '', 'affiliation': user.affiliation, 'role': user.role} self.host.bridge.roomUserJoined(room.roomJID.userhost(), user.nick, user_data, self.parent.profile) def userLeftRoom(self, room, user): if not self.host.trigger.point("MUC user left", room, user, self.parent.profile): return if user.nick == room.nick: # we left the room room_jid_s = room.roomJID.userhost() log.info(_(u"Room [%(room)s] left (%(profile)s))") % {"room": room_jid_s, "profile": self.parent.profile}) self.host.memory.delEntityCache(room.roomJID, profile_key=self.parent.profile) self.host.bridge.roomLeft(room.roomJID.userhost(), self.parent.profile) else: log.debug(_(u"user %(nick)s left room (%(room_id)s)") % {'nick': user.nick, 'room_id': room.occupantJID.userhost()}) user_data = {'entity': user.entity.full() if user.entity else '', 'affiliation': user.affiliation, 'role': user.role} self.host.bridge.roomUserLeft(room.roomJID.userhost(), user.nick, user_data, self.parent.profile) def userChangedNick(self, room, user, new_nick): self.host.bridge.roomUserChangedNick(room.roomJID.userhost(), user.nick, new_nick, self.parent.profile) def userUpdatedStatus(self, room, user, show, status): self.host.bridge.presenceUpdate(room.roomJID.userhost() + '/' + user.nick, show or '', 0, {C.PRESENCE_STATUSES_DEFAULT: status or ''}, self.parent.profile) def receivedGroupChat(self, room, user, body): log.debug(u'receivedGroupChat: room=%s user=%s body=%s' % (room.roomJID.full(), user, body)) def receivedHistory(self, room, user, message): # http://xmpp.org/extensions/xep-0045.html#enter-history # log.debug(u'receivedHistory: room=%s user=%s body=%s' % (room.roomJID.full(), user, message)) pass def receivedSubject(self, room, user, subject): # http://xmpp.org/extensions/xep-0045.html#enter-subject log.debug(_(u"New subject for room (%(room_id)s): %(subject)s") % {'room_id': room.roomJID.full(), 'subject': subject}) self.rec_subjects[room.roomJID.userhost()] = (room.roomJID.userhost(), subject) self.host.bridge.roomNewSubject(room.roomJID.userhost(), subject, self.parent.profile) def getDiscoInfo(self, requestor, target, nodeIdentifier=''): return [disco.DiscoFeature(NS_MUC)] def getDiscoItems(self, requestor, target, nodeIdentifier=''): # TODO: manage room queries ? Bad for privacy, must be disabled by default # see XEP-0045 § 6.7 return []