Mercurial > libervia-backend
diff src/plugins/plugin_xep_0045.py @ 1963:a2bc5089c2eb
backend, frontends: message refactoring (huge commit):
/!\ several features are temporarily disabled, like notifications in frontends
next step in refactoring, with the following changes:
- jp: updated jp message to follow changes in backend/bridge
- jp: added --lang, --subject, --subject_lang, and --type options to jp message + fixed unicode handling for jid
- quick_frontend (QuickApp, QuickChat):
- follow backend changes
- refactored chat, message are now handled in OrderedDict and uid are kept so they can be updated
- Message and Occupant classes handle metadata, so frontend just have to display them
- Primitivus (Chat):
- follow backend/QuickFrontend changes
- info & standard messages are handled in the same MessageWidget class
- improved/simplified handling of messages, removed update() method
- user joined/left messages are merged when next to each other
- a separator is shown when message is received while widget is out of focus, so user can quickly see the new messages
- affiliation/role are shown (in a basic way for now) in occupants panel
- removed "/me" messages handling, as it will be done by a backend plugin
- message language is displayed when available (only one language per message for now)
- fixed :history and :search commands
- core (constants): new constants for messages type, XML namespace, entity type
- core: *Message methods renamed to follow new code sytle (e.g. sendMessageToBridge => messageSendToBridge)
- core (messages handling): fixed handling of language
- core (messages handling): mes_data['from'] and ['to'] are now jid.JID
- core (core.xmpp): reorganised message methods, added getNick() method to client.roster
- plugin text commands: fixed plugin and adapted to new messages behaviour. client is now used in arguments instead of profile
- plugins: added information for cancellation reason in CancelError calls
- plugin XEP-0045: various improvments, but this plugin still need work:
- trigger is used to avoid message already handled by the plugin to be handled a second time
- changed the way to handle history, the last message from DB is checked and we request only messages since this one, in seconds (thanks Poezio folks :))
- subject reception is waited before sending the roomJoined signal, this way we are sure that everything including history is ready
- cmd_* method now follow the new convention with client instead of profile
- roomUserJoined and roomUserLeft messages are removed, the events are now handled with info message with a "ROOM_USER_JOINED" info subtype
- probably other forgotten stuffs :p
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 20 Jun 2016 18:41:53 +0200 |
parents | 2daf7b4c6756 |
children | 200cd707a46d |
line wrap: on
line diff
--- a/src/plugins/plugin_xep_0045.py Sun Jun 19 22:22:13 2016 +0200 +++ b/src/plugins/plugin_xep_0045.py Mon Jun 20 18:41:53 2016 +0200 @@ -23,10 +23,13 @@ log = getLogger(__name__) from twisted.internet import defer from twisted.words.protocols.jabber import jid +from dateutil.tz import tzutc from sat.core import exceptions from sat.memory import memory +import calendar +import time import uuid import copy @@ -50,6 +53,10 @@ NS_MUC = 'http://jabber.org/protocol/muc' AFFILIATIONS = ('owner', 'admin', 'member', 'none', 'outcast') +ROOM_USER_JOINED = 'ROOM_USER_JOINED' +ROOM_USER_LEFT = 'ROOM_USER_LEFT' +OCCUPANT_KEYS = ('nick', 'entity', 'affiliation', 'role') +ENTITY_TYPE_MUC = "MUC" CONFIG_SECTION = u'plugin muc' @@ -73,20 +80,18 @@ def __init__(self, host): log.info(_("Plugin XEP_0045 initialization")) self.host = host - self.clients = {} + self.clients = {} # FIXME: should be moved to profile's client 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("mucJoin", ".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("getRoomsJoined", ".plugin", in_sign='s', out_sign='a(sa{sa{ss}}ss)', 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("roomJoined", ".plugin", signature='sa{sa{ss}}sss') # args: room_jid, occupants, user_nick, subject, 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) @@ -98,6 +103,7 @@ log.info(_("Text commands not available")) host.trigger.add("presence_available", self.presenceTrigger) + host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=1000000) def profileConnected(self, profile): def assign_service(service): @@ -105,6 +111,23 @@ client.muc_service = service return self.getMUCService(profile=profile).addCallback(assign_service) + def MessageReceivedTrigger(self, client, message_elt, post_treat): + if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT: + if message_elt.subject or message_elt.delay: + return False + from_jid = jid.JID(message_elt['from']) + room_jid = from_jid.userhostJID() + if room_jid in self.clients[client.profile].joined_rooms: + room = self.clients[client.profile].joined_rooms[room_jid] + if not room._room_ok: + log.warning(u"Received non delayed message in a room before its initialisation: {}".format(message_elt.toXml())) + room._cache.append(message_elt) + return False + else: + log.warning(u"Received groupchat message for a room which has not been joined, ignoring it: {}".format(message_elt.toXml())) + return False + return True + def checkClient(self, profile): """Check if the profile is connected and has used the MUC feature. @@ -132,29 +155,23 @@ raise UnknownRoom("This room has not been joined") return profile - def __room_joined(self, room, profile): + def _joinCb(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() + d = self.clients[profile].configure(room.roomJID, {}) + d.addErrback(lambda dummy: log.error(_(u'Error while configuring the room'))) return room - def __err_joining_room(self, failure, room_jid, nick, history_options, password, profile): + def _joinEb(self, failure, room_jid, nick, 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]) + return self.clients[profile].join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, callbackKeywords={'profile': profile}, errbackArgs=[room_jid, nick, password, profile]) mess = D_("Error while joining the room %s" % room_jid.userhost()) try: mess += " with condition '%s'" % failure.value.condition @@ -164,6 +181,11 @@ self.host.bridge.newAlert(mess, D_("Group chat error"), "ERROR", profile) raise failure + @staticmethod + def _getOccupants(room): + """Get occupants of a room in a form suitable for bridge""" + return {u.nick: {k:unicode(getattr(u,k) or '') for k in OCCUPANT_KEYS} for u in room.roster.values()} + def isRoom(self, entity_bare, profile_key): """Tell if a bare entity is a MUC room. @@ -181,7 +203,8 @@ 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)) + if room._room_ok: + result.append((room.roomJID.userhost(), self._getOccupants(room), room.nick, room.subject)) return result def getRoomNick(self, room_jid, profile_key=C.PROF_KEY_NONE): @@ -301,6 +324,7 @@ def getRoomsSubjects(self, profile_key=C.PROF_KEY_NONE): """Return received subjects of rooms""" + # FIXME: to be removed profile = self.host.memory.getProfileName(profile_key) if not self.checkClient(profile): return [] @@ -355,45 +379,31 @@ """ 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 join(self, client, room_jid, nick, options): 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()}) + if room_jid in self.clients[client.profile].joined_rooms: + log.warning(_(u'%(profile)s is already in room %(room_jid)s') % {'profile': client.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}) + log.info(_(u"[%(profile)s] is joining room %(room)s with nick %(nick)s") % {'profile': client.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) + return self.clients[client.profile].join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, callbackKeywords={'profile': client.profile}, errbackArgs=[room_jid, nick, password, client.profile]) 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 + """join method used by bridge + @return: unicode (the room bare) """ + client = self.host.getClient(profile_key) 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 + muc_service = self.host.getClient(client.profile).muc_service try: room_jid = jid.JID(room_jid_s) except (RuntimeError, jid.InvalidFormat, AttributeError): @@ -401,9 +411,9 @@ 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) + room_jid = self.getUniqueName(profile_key=client.profile) # TODO: error management + signal in bridge - d = self.join(room_jid, nick, options, profile) + d = self.join(client, room_jid, nick, options) return d.addCallback(lambda room: room.roomJID.userhost()) def nick(self, room_jid, nick, profile_key): @@ -477,7 +487,7 @@ # Text commands # - def cmd_nick(self, mess_data, profile): + def cmd_nick(self, client, mess_data): """change nickname @command (group): new_nick @@ -486,11 +496,11 @@ nick = mess_data["unparsed"].strip() if nick: room = mess_data["to"] - self.nick(room, nick, profile) + self.nick(room, nick, client.profile) return False - def cmd_join(self, mess_data, profile): + def cmd_join(self, client, mess_data): """join a new room @command (all): JID @@ -498,13 +508,13 @@ """ 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) + nick = (self.getRoomNick(room_jid, client.profile) or + self.host.getClient(client.profile).jid.user) + self.join(client, room_jid, nick, {}) return False - def cmd_leave(self, mess_data, profile): + def cmd_leave(self, client, mess_data): """quit a room @command (group): [ROOM_JID] @@ -515,19 +525,19 @@ else: room = mess_data["to"] - self.leave(room, profile) + self.leave(room, client.profile) return False - def cmd_part(self, mess_data, profile): + def cmd_part(self, client, mess_data): """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) + return self.cmd_leave(client, mess_data) - def cmd_kick(self, mess_data, profile): + def cmd_kick(self, client, mess_data): """kick a room member @command (group): ROOM_NICK @@ -536,24 +546,24 @@ options = mess_data["unparsed"].strip().split() try: nick = options[0] - assert(self.isNickInRoom(mess_data["to"], nick, profile)) + assert(self.isNickInRoom(mess_data["to"], nick, client.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) + self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data) return False - d = self.kick(nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, profile) + d = self.kick(nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, client.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) + self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback_msg, mess_data) return True d.addCallback(cb) return d - def cmd_ban(self, mess_data, profile): + def cmd_ban(self, client, mess_data): """ban an entity from the room @command (group): (JID) [reason] @@ -568,21 +578,21 @@ 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) + self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data) return False - d = self.ban(entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, profile) + d = self.ban(entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, client.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) + self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback_msg, mess_data) return True d.addCallback(cb) return d - def cmd_affiliate(self, mess_data, profile): + def cmd_affiliate(self, client, mess_data): """affiliate an entity to the room @command (group): (JID) [owner|admin|member|none|outcast] @@ -601,25 +611,25 @@ 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) + self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data) 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) + self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data) return False - d = self.affiliate(entity_jid, mess_data["to"], {'affiliation': affiliation}, profile) + d = self.affiliate(entity_jid, mess_data["to"], {'affiliation': affiliation}, client.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) + self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback_msg, mess_data) return True d.addCallback(cb) return d - def cmd_title(self, mess_data, profile): + def cmd_title(self, client, mess_data): """change room's subject @command (group): title @@ -629,28 +639,28 @@ if subject: room = mess_data["to"] - self.subject(room, subject, profile) + self.subject(room, subject, client.profile) return False - def cmd_topic(self, mess_data, profile): + def cmd_topic(self, client, mess_data): """just a synonym of /title @command (group): title - title: new room subject """ - return self.cmd_title(mess_data, profile) + return self.cmd_title(client, mess_data) - def _whois(self, whois_msg, mess_data, target_jid, profile): + def _whois(self, client, whois_msg, mess_data, target_jid): """ Add MUC user information to whois """ if mess_data['type'] != "groupchat": return - if target_jid.userhostJID() not in self.clients[profile].joined_rooms: + if target_jid.userhostJID() not in self.clients[client.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) + user = self.clients[client.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) @@ -681,16 +691,40 @@ 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 + 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 _addRoom(self, room): + super(SatMUCClient, self)._addRoom(room) + room._roster_ok = False + room._room_ok = None # False when roster, history and subject are available + # True when new messages are saved to database + room._history_d = defer.Deferred() # use to send bridge signal once backlog are written in history + room._history_d.callback(None) + room._cache = [] + + def _gotLastDbHistory(self, mess_data_list, room_jid, nick, password): + if mess_data_list: + timestamp = mess_data_list[0][1] + # we use seconds since last message to get backlog without duplicates + # and we remove 1 second to avoid getting the last message again + seconds = int(time.time() - timestamp) - 1 + else: + seconds = None + d = super(SatMUCClient, self).join(room_jid, nick, muc.HistoryOptions(seconds=seconds), password) + return d + + def join(self, room_jid, nick, password=None): + d = self.host.memory.historyGet(self.parent.jid.userhostJID(), room_jid, 1, True, profile=self.parent.profile) + d.addCallback(self._gotLastDbHistory, room_jid, nick, password) + return d + + ## presence/roster ## def availableReceived(self, presence): """ @@ -698,7 +732,6 @@ """ # XXX: we override MUCClient.availableReceived to fix bugs # (affiliation and role are not set) - # FIXME: propose a patch upstream room, user = self._getRoomUser(presence) @@ -720,9 +753,9 @@ 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. @@ -737,22 +770,49 @@ room.removeUser(user) if muc.STATUS_CODE.NEW_NICK in presence.mucStatuses: - self.__changing_nicks.add(presence.nick) + self._changing_nicks.add(presence.nick) self.userChangedNick(room, user, presence.nick) else: - self.__changing_nicks.discard(presence.nick) + 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) + if user.nick == room.nick: + # we have received our own nick, this mean that the full room roster was received + room._roster_ok = True + log.debug(u"room {room} joined with nick {nick}".format(room=room.occupantJID.userhost(), nick=user.nick)) + # We set type so we don't have use a deferred with disco to check entity type + self.host.memory.updateEntityData(room.roomJID, C.ENTITY_TYPE, ENTITY_TYPE_MUC, profile_key=self.parent.profile) + + elif room._roster_ok: + try: + self._changing_nicks.remove(user.nick) + except KeyError: + # this is a new user + log.debug(_(u"user {nick} has joined room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost())) + if not self.host.trigger.point("MUC user joined", room, user, self.parent.profile): + return + + extra = {'info_type': ROOM_USER_JOINED, + 'user_affiliation': user.affiliation, + 'user_role': user.role, + 'user_nick': user.nick + } + if user.entity is not None: + extra['user_entity'] = user.entity.full() + mess_data = { # dict is similar to the one used in client.onMessage + "from": room.roomJID, + "to": self.parent.jid, + "uid": unicode(uuid.uuid4()), + "message": {'': D_(u"=> {} has joined the room").format(user.nick)}, + "subject": {}, + "type": C.MESS_TYPE_INFO, + "extra": extra, + "timestamp": time.time(), + } + self.host.messageAddToHistory(mess_data, self.parent) + self.host.messageSendToBridge(mess_data, self.parent) + def userLeftRoom(self, room, user): if not self.host.trigger.point("MUC user left", room, user, self.parent.profile): @@ -760,14 +820,31 @@ 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}) + log.info(_(u"Room ({room}) left ({profile})").format( + 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) + log.debug(_(u"user {nick} left room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost())) + extra = {'info_type': ROOM_USER_LEFT, + 'user_affiliation': user.affiliation, + 'user_role': user.role, + 'user_nick': user.nick + } + if user.entity is not None: + extra['user_entity'] = user.entity.full() + mess_data = { # dict is similar to the one used in client.onMessage + "from": room.roomJID, + "to": self.parent.jid, + "uid": unicode(uuid.uuid4()), + "message": {'': D_(u"<= {} has left the room").format(user.nick)}, + "subject": {}, + "type": C.MESS_TYPE_INFO, + "extra": extra, + "timestamp": time.time(), + } + self.host.messageAddToHistory(mess_data, self.parent) + self.host.messageSendToBridge(mess_data, self.parent) def userChangedNick(self, room, user, new_nick): self.host.bridge.roomUserChangedNick(room.roomJID.userhost(), user.nick, new_nick, self.parent.profile) @@ -775,19 +852,111 @@ 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) + ## messages ## + def receivedGroupChat(self, room, user, body): log.debug(u'receivedGroupChat: room=%s user=%s body=%s' % (room.roomJID.full(), user, body)) + def _addToHistory(self, dummy, user, message): + # we check if message is not in history + # and raise ConflictError else + stamp = message.delay.stamp.astimezone(tzutc()).timetuple() + timestamp = float(calendar.timegm(stamp)) + data = { # dict is similar to the one used in client.onMessage + "from": message.sender, + "to": message.recipient, + "uid": unicode(uuid.uuid4()), + "type": C.MESS_TYPE_GROUPCHAT, + "extra": {}, + "timestamp": timestamp, + "received_timestamp": unicode(time.time()), + } + # FIXME: message and subject don't handle xml:lang + data['message'] = {'': message.body} if message.body is not None else {} + data['subject'] = {'': message.subject} if message.subject is not None else {} + + if data['message'] or data['subject']: + return self.host.memory.addToHistory(self.parent, data) + else: + return defer.succeed(None) + + def _addToHistoryEb(self, failure): + failure.trap(exceptions.CancelError) + 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 + """Called when history (backlog) message are received + + we check if message is not already in our history + and add it if needed + @param room(muc.Room): room instance + @param user(muc.User, None): the user that sent the message + None if the message come from the room + @param message(muc.GroupChat): the parsed message + """ + room._history_d.addCallback(self._addToHistory, user, message) + room._history_d.addErrback(self._addToHistoryEb) + + ## subject ## + + def groupChatReceived(self, message): + """ + A group chat message has been received from a MUC room. + + There are a few event methods that may get called here. + L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}. + """ + # We override this method to fix subject handling + # FIXME: remove this merge fixed upstream + room, user = self._getRoomUser(message) + + if room is None: + return + + if message.subject is not None: + self.receivedSubject(room, user, message.subject) + elif message.delay is None: + self.receivedGroupChat(room, user, message) + else: + self.receivedHistory(room, user, message) + + def subject(self, room, subject): + return muc.MUCClientProtocol.subject(self, room, subject) + + def _historyCb(self, dummy, room): + self.host.bridge.roomJoined( + room.roomJID.userhost(), + XEP_0045._getOccupants(room), + room.nick, + room.subject, + self.parent.profile) + del room._history_d + cache = room._cache + del room._cache + room._room_ok = True + for elem in cache: + self.parent.xmlstream.dispatch(elem) + + + def _historyEb(self, failure_, room): + log.error(u"Error while managing history: {}".format(failure_)) + self._historyCb(None, room) 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}) + # when subject is received, we know that we have whole roster and history + # cf. http://xmpp.org/extensions/xep-0045.html#enter-subject + room.subject = subject # FIXME: subject doesn't handle xml:lang self.rec_subjects[room.roomJID.userhost()] = (room.roomJID.userhost(), subject) - self.host.bridge.roomNewSubject(room.roomJID.userhost(), subject, self.parent.profile) + if room._room_ok is None: + # this is the first subject we receive + # that mean that we have received everything we need + room._room_ok = False + room._history_d.addCallbacks(self._historyCb, self._historyEb, [room], errbackArgs=[room]) + else: + # the subject has been changed + log.debug(_(u"New subject for room ({room_id}): {subject}").format(room_id = room.roomJID.full(), subject = subject)) + self.host.bridge.roomNewSubject(room.roomJID.userhost(), subject, self.parent.profile) + + ## disco ## def getDiscoInfo(self, requestor, target, nodeIdentifier=''): return [disco.DiscoFeature(NS_MUC)]