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)]