diff src/core/xmpp.py @ 2144:1d3f73e065e1

core, jp: component handling + client handling refactoring: - SàT can now handle components - plugin have now a "modes" key in PLUGIN_INFO where they declare if they can be used with clients and or components. They default to be client only. - components are really similar to clients, but with some changes in behaviour: * component has "entry point", which is a special plugin with a componentStart method, which is called just after component is connected * trigger end with a different suffixes (e.g. profileConnected vs profileConnectedComponent), so a plugin which manage both clients and components can have different workflow * for clients, only triggers of plugins handling client mode are launched * for components, only triggers of plugins needed in dependencies are launched. They all must handle component mode. * component have a sendHistory attribute (False by default) which can be set to True to allow saving sent messages into history * for convenience, "client" is still used in method even if it can now be a component * a new "component" boolean attribute tells if we have a component or a client * components have to add themselve Message protocol * roster and presence protocols are not added for components * component default port is 5347 (which is Prosody's default port) - asyncCreateProfile has been renamed for profileCreate, both to follow new naming convention and to prepare the transition to fully asynchronous bridge - createProfile has a new "component" attribute. When used to create a component, it must be set to a component entry point - jp: added --component argument to profile/create - disconnect bridge method is now asynchronous, this way frontends can know when disconnection is finished - new PI_* constants for PLUGIN_INFO values (not used everywhere yet) - client/component connection workflow has been moved to their classes instead of being a host methods - host.messageSend is now client.sendMessage, and former client.sendMessage is now client.sendMessageData. - identities are now handled in client.identities list, so it can be updated dynamically by plugins (in the future, frontends should be able to update them too through bridge) - profileConnecting* profileConnected* profileDisconnected* and getHandler now all use client instead of profile
author Goffi <goffi@goffi.org>
date Sun, 12 Feb 2017 17:55:43 +0100
parents f8401024ab28
children 545a1261ac3b
line wrap: on
line diff
--- a/src/core/xmpp.py	Tue Feb 07 00:15:03 2017 +0100
+++ b/src/core/xmpp.py	Sun Feb 12 17:55:43 2017 +0100
@@ -28,6 +28,7 @@
 from twisted.words.xish import domish
 from twisted.python import failure
 from wokkel import client as wokkel_client, disco, xmppim, generic, iwokkel
+from wokkel import component
 from wokkel import delay
 from sat.core.log import getLogger
 log = getLogger(__name__)
@@ -39,40 +40,229 @@
 import sys
 
 
-class SatXMPPClient(wokkel_client.XMPPClient):
-    implements(iwokkel.IDisco)
+class SatXMPPEntity(object):
+    """Common code for Client and Component"""
 
-    def __init__(self, host_app, profile, user_jid, password, host=None, port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES):
-        # XXX: DNS SRV records are checked when the host is not specified.
-        # If no SRV record is found, the host is directly extracted from the JID.
-        self.started = time.time()
-        if sys.platform == "android":
-            # FIXME: temporary hack as SRV is not working on android
-            # TODO: remove this hack and fix SRV
-            log.info(u"FIXME: Android hack, ignoring SRV")
-            host = user_jid.host
-            hosts_map = host_app.memory.getConfig("android", "hosts_dict", {})
-            if host in hosts_map:
-                log.info(u"using {host_to_use} for host {host_ori} as requested in config".format(
-                    host_ori = host,
-                    host_to_use = hosts_map[host]))
-                host = hosts_map[host]
-        wokkel_client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT)
+    def __init__(self, host_app, profile, max_retries):
+
         self.factory.clientConnectionLost = self.connectionLost
         self.factory.maxRetries = max_retries
-        self.__connected = False
+        # when self._connected is None, we are not connected
+        # else, it's a deferred which fire on disconnection
+        self._connected = None
         self.profile = profile
         self.host_app = host_app
         self.cache = cache.Cache(host_app, profile)
-        self._mess_id_uid = {} # map from message id to uid use in history. Key: (full_jid,message_id) Value: uid
+        self._mess_id_uid = {} # map from message id to uid used in history. Key: (full_jid,message_id) Value: uid
         self.conn_deferred = defer.Deferred()
-        self._progress_cb = {}  # callback called when a progress is requested (key = progress id)
-        self.actions = {} # used to keep track of actions for retrieval (key = action_id)
+
+    ## initialisation ##
+
+    @defer.inlineCallbacks
+    def _callConnectionTriggers(self):
+        """Call conneting trigger prepare connected trigger
+
+        @param plugins(iterable): plugins to use
+        @return (list[object, callable]): plugin to trigger tuples with:
+            - plugin instance
+            - profileConnected* triggers (to call after connection)
+        """
+        plugin_conn_cb = []
+        for plugin in self._getPluginsList():
+            # we check if plugin handle client mode
+            if plugin.is_handler:
+                plugin.getHandler(self).setHandlerParent(self)
+
+            # profileConnecting/profileConnected methods handling
+
+            # profile connecting is called right now (before actually starting client)
+            connecting_cb = getattr(plugin, "profileConnecting" + self.trigger_suffix, None)
+            if connecting_cb is not None:
+                yield connecting_cb(self)
+
+            # profile connected is called after client is ready and roster is got
+            connected_cb = getattr(plugin, "profileConnected" + self.trigger_suffix, None)
+            if connected_cb is not None:
+                plugin_conn_cb.append((plugin, connected_cb))
+
+        defer.returnValue(plugin_conn_cb)
+
+    def _getPluginsList(self):
+        """Return list of plugin to use
+
+        need to be implemented by subclasses
+        this list is used to call profileConnect* triggers
+        @return(iterable[object]): plugins to use
+        """
+        raise NotImplementedError
+
+    def _createSubProtocols(self):
+        return
+
+    def entityConnected(self):
+        """Called once connection is done
+
+        may return a Deferred, to perform initialisation tasks
+        """
+        return
+
+    @classmethod
+    @defer.inlineCallbacks
+    def startConnection(cls, host, profile, max_retries):
+        """instantiate the entity and start the connection"""
+        # FIXME: reconnection doesn't seems to be handled correclty (client is deleted then recreated from scrash
+        #        most of methods called here should be called once on first connection (e.g. adding subprotocols)
+        #        but client should not be deleted except if session is finished (independently of connection/deconnection
+        #
+        try:
+            port = int(host.memory.getParamA(C.FORCE_PORT_PARAM, "Connection", profile_key=profile))
+        except ValueError:
+            log.debug(_("Can't parse port value, using default value"))
+            port = None  # will use default value 5222 or be retrieved from a DNS SRV record
+
+        password = yield host.memory.asyncGetParamA("Password", "Connection", profile_key=profile)
+        entity = host.profiles[profile] = cls(host, profile,
+            jid.JID(host.memory.getParamA("JabberID", "Connection", profile_key=profile)),
+            password, host.memory.getParamA(C.FORCE_SERVER_PARAM, "Connection", profile_key=profile) or None,
+            port, max_retries)
+
+        entity._createSubProtocols()
+
+        entity.fallBack = SatFallbackHandler(host)
+        entity.fallBack.setHandlerParent(entity)
+
+        entity.versionHandler = SatVersionHandler(C.APP_NAME_FULL,
+                                                  host.full_version)
+        entity.versionHandler.setHandlerParent(entity)
+
+        entity.identityHandler = SatIdentityHandler()
+        entity.identityHandler.setHandlerParent(entity)
+
+        log.debug(_("setting plugins parents"))
+
+        plugin_conn_cb = yield entity._callConnectionTriggers()
+
+        entity.startService()
+
+        yield entity.getConnectionDeferred()
+
+        yield defer.maybeDeferred(entity.entityConnected)
+
+        # Call profileConnected callback for all plugins, and print error message if any of them fails
+        conn_cb_list = []
+        for dummy, callback in plugin_conn_cb:
+            conn_cb_list.append(defer.maybeDeferred(callback, entity))
+        list_d = defer.DeferredList(conn_cb_list)
+
+        def logPluginResults(results):
+            all_succeed = all([success for success, result in results])
+            if not all_succeed:
+                log.error(_(u"Plugins initialisation error"))
+                for idx, (success, result) in enumerate(results):
+                    if not success:
+                        log.error(u"error (plugin %(name)s): %(failure)s" %
+                                  {'name': plugin_conn_cb[idx][0]._info['import_name'], 'failure': result})
+
+        yield list_d.addCallback(logPluginResults) # FIXME: we should have a timeout here, and a way to know if a plugin freeze
+        # TODO: mesure launch time of each plugin
 
     def getConnectionDeferred(self):
         """Return a deferred which fire when the client is connected"""
         return self.conn_deferred
 
+    def _disconnectionCb(self, dummy):
+        self._connected = None
+
+    def _disconnectionEb(self, failure_):
+        log.error(_(u"Error while disconnecting: {}".format(failure_)))
+
+    def _authd(self, xmlstream):
+        if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
+            return
+        super(SatXMPPEntity, self)._authd(xmlstream)
+
+        # the following Deferred is used to know when we are connected
+        # so we need to be set it to None when connection is lost
+        self._connected = defer.Deferred()
+        self._connected.addCallback(self._cleanConnection)
+        self._connected.addCallback(self._disconnectionCb)
+        self._connected.addErrback(self._disconnectionEb)
+
+        log.info(_("********** [%s] CONNECTED **********") % self.profile)
+        self.streamInitialized()
+        self.host_app.bridge.connected(self.profile, unicode(self.jid))  # we send the signal to the clients
+
+    def _finish_connection(self, dummy):
+        self.conn_deferred.callback(None)
+
+    def streamInitialized(self):
+        """Called after _authd"""
+        log.debug(_(u"XML stream is initialized"))
+        self.keep_alife = task.LoopingCall(self.xmlstream.send, " ")  # Needed to avoid disconnection (specially with openfire)
+        self.keep_alife.start(C.XMPP_KEEP_ALIFE)
+
+        self.disco = SatDiscoProtocol(self)
+        self.disco.setHandlerParent(self)
+        self.discoHandler = disco.DiscoHandler()
+        self.discoHandler.setHandlerParent(self)
+        disco_d = defer.succeed(None)
+
+        if not self.host_app.trigger.point("Disco handled", disco_d, self.profile):
+            return
+
+        disco_d.addCallback(self._finish_connection)
+
+    def initializationFailed(self, reason):
+        log.error(_(u"ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" % {'profile': self.profile, 'reason': reason}))
+        self.conn_deferred.errback(reason.value)
+        try:
+            super(SatXMPPEntity, self).initializationFailed(reason)
+        except:
+            # we already chained an errback, no need to raise an exception
+            pass
+
+    ## connection ##
+
+    def connectionLost(self, connector, reason):
+        try:
+            self.keep_alife.stop()
+        except AttributeError:
+            log.debug(_("No keep_alife"))
+        if self._connected is not None:
+            self.host_app.bridge.disconnected(self.profile)  # we send the signal to the clients
+            self._connected.callback(None)
+            self.host_app.purgeEntity(self.profile)  # and we remove references to this client
+            log.info(_("********** [%s] DISCONNECTED **********") % self.profile)
+        if not self.conn_deferred.called:
+            # FIXME: real error is not gotten here (e.g. if jid is not know by Prosody,
+            #        we should have the real error)
+            self.conn_deferred.errback(error.StreamError(u"Server unexpectedly closed the connection"))
+
+    @defer.inlineCallbacks
+    def _cleanConnection(self, dummy):
+        """method called on disconnection
+
+        used to call profileDisconnected* triggers
+        """
+        trigger_name = "profileDisconnected" + self.trigger_suffix
+        for plugin in self._getPluginsList():
+            disconnected_cb = getattr(plugin, trigger_name, None)
+            if disconnected_cb is not None:
+                yield disconnected_cb(self)
+
+    def isConnected(self):
+        return self._connected is not None
+
+    def entityDisconnect(self):
+        log.info(_(u"Disconnecting..."))
+        self.stopService()
+        if self._connected is not None:
+            return self._connected
+        else:
+            return defer.succeed(None)
+
+    ## sending ##
+
     def IQ(self, type_=u'set', timeout=None):
         """shortcut to create an IQ element managing deferred
 
@@ -94,6 +284,223 @@
         iq_error_elt = error.StanzaError(condition).toResponse(iq_elt)
         self.xmlstream.send(iq_error_elt)
 
+    def generateMessageXML(self, data):
+        """Generate <message/> stanza from message data
+
+        @param data(dict): message data
+            domish element will be put in data['xml']
+            following keys are needed:
+                - from
+                - to
+                - uid: can be set to '' if uid attribute is not wanted
+                - message
+                - type
+                - subject
+                - extra
+        @return (dict) message data
+        """
+        data['xml'] = message_elt = domish.Element((None, 'message'))
+        message_elt["to"] = data["to"].full()
+        message_elt["from"] = data['from'].full()
+        message_elt["type"] = data["type"]
+        if data['uid']: # key must be present but can be set to ''
+                        # by a plugin to avoid id on purpose
+            message_elt['id'] = data['uid']
+        for lang, subject in data["subject"].iteritems():
+            subject_elt = message_elt.addElement("subject", content=subject)
+            if lang:
+                subject_elt[(C.NS_XML, 'lang')] = lang
+        for lang, message in data["message"].iteritems():
+            body_elt = message_elt.addElement("body", content=message)
+            if lang:
+                body_elt[(C.NS_XML, 'lang')] = lang
+        try:
+            thread = data['extra']['thread']
+        except KeyError:
+            if 'thread_parent' in data['extra']:
+                raise exceptions.InternalError(u"thread_parent found while there is not associated thread")
+        else:
+            thread_elt = message_elt.addElement("thread", content=thread)
+            try:
+                thread_elt["parent"] = data["extra"]["thread_parent"]
+            except KeyError:
+                pass
+        return data
+
+    def addPostXmlCallbacks(self, post_xml_treatments):
+        """Used to add class level callbacks at the end of the workflow
+
+        @param post_xml_treatments(D): the same Deferred as in sendMessage trigger
+        """
+        raise NotImplementedError
+
+    def sendMessage(self, to_jid, message, subject=None, mess_type='auto', extra=None, uid=None, no_trigger=False):
+        """Send a message to an entity
+
+        @param to_jid(jid.JID): destinee of the message
+        @param message(dict): message body, key is the language (use '' when unknown)
+        @param subject(dict): message subject, key is the language (use '' when unknown)
+        @param mess_type(str): one of standard message type (cf RFC 6121 §5.2.2) or:
+            - auto: for automatic type detection
+            - info: for information ("info_type" can be specified in extra)
+        @param extra(dict, None): extra data. Key can be:
+            - info_type: information type, can be
+                TODO
+        @param uid(unicode, None): unique id:
+            should be unique at least in this XMPP session
+            if None, an uuid will be generated
+        @param no_trigger (bool): if True, sendMessage[suffix] trigger will no be used
+            useful when a message need to be sent without any modification
+        """
+        if subject is None:
+            subject = {}
+        if extra is None:
+            extra = {}
+        data = {  # dict is similar to the one used in client.onMessage
+            "from": self.jid,
+            "to": to_jid,
+            "uid": uid or unicode(uuid.uuid4()),
+            "message": message,
+            "subject": subject,
+            "type": mess_type,
+            "extra": extra,
+            "timestamp": time.time(),
+        }
+        pre_xml_treatments = defer.Deferred() # XXX: plugin can add their pre XML treatments to this deferred
+        post_xml_treatments = defer.Deferred() # XXX: plugin can add their post XML treatments to this deferred
+
+        if data["type"] == "auto":
+            # we try to guess the type
+            if data["subject"]:
+                data["type"] = 'normal'
+            elif not data["to"].resource:  # if to JID has a resource, the type is not 'groupchat'
+                # we may have a groupchat message, we check if the we know this jid
+                try:
+                    entity_type = self.host_app.memory.getEntityData(data["to"], ['type'], self.profile)["type"]
+                    #FIXME: should entity_type manage resources ?
+                except (exceptions.UnknownEntityError, KeyError):
+                    entity_type = "contact"
+
+                if entity_type == "chatroom":
+                    data["type"] = 'groupchat'
+                else:
+                    data["type"] = 'chat'
+            else:
+                data["type"] == 'chat'
+            data["type"] == "chat" if data["subject"] else "normal"
+
+        # FIXME: send_only is used by libervia's OTR plugin to avoid
+        #        the triggers from frontend, and no_trigger do the same
+        #        thing internally, this could be unified
+        send_only = data['extra'].get('send_only', False)
+
+        if not no_trigger and not send_only:
+            if not self.host_app.trigger.point("sendMessage" + self.trigger_suffix, self, data, pre_xml_treatments, post_xml_treatments):
+                return defer.succeed(None)
+
+        log.debug(_(u"Sending message (type {type}, to {to})").format(type=data["type"], to=to_jid.full()))
+
+        pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data))
+        pre_xml_treatments.chainDeferred(post_xml_treatments)
+        post_xml_treatments.addCallback(self.sendMessageData)
+        if send_only:
+            log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter"))
+        else:
+            self.addPostXmlCallbacks(post_xml_treatments)
+            post_xml_treatments.addErrback(self._cancelErrorTrap)
+            post_xml_treatments.addErrback(self.host_app.logErrback)
+        pre_xml_treatments.callback(data)
+        return pre_xml_treatments
+
+    def _cancelErrorTrap(self, failure):
+        """A message sending can be cancelled by a plugin treatment"""
+        failure.trap(exceptions.CancelError)
+
+    def messageAddToHistory(self, data):
+        """Store message into database (for local history)
+
+        @param data: message data dictionnary
+        @param client: profile's client
+        """
+        if data[u"type"] != C.MESS_TYPE_GROUPCHAT:
+            # we don't add groupchat message to history, as we get them back
+            # and they will be added then
+            if data[u'message'] or data[u'subject']: # we need a message to store
+                self.host_app.memory.addToHistory(self, data)
+            else:
+               log.warning(u"No message found") # empty body should be managed by plugins before this point
+        return data
+
+    def messageSendToBridge(self, data):
+        """Send message to bridge, so frontends can display it
+
+        @param data: message data dictionnary
+        @param client: profile's client
+        """
+        if data[u"type"] != C.MESS_TYPE_GROUPCHAT:
+            # we don't send groupchat message to bridge, as we get them back
+            # and they will be added the
+            if data[u'message'] or data[u'subject']: # we need a message to send something
+                # We send back the message, so all frontends are aware of it
+                self.host_app.bridge.messageNew(data[u'uid'], data[u'timestamp'], data[u'from'].full(), data[u'to'].full(), data[u'message'], data[u'subject'], data[u'type'], data[u'extra'], profile=self.profile)
+            else:
+               log.warning(_(u"No message found"))
+        return data
+
+
+class SatXMPPClient(SatXMPPEntity, wokkel_client.XMPPClient):
+    implements(iwokkel.IDisco)
+    trigger_suffix = ""
+    component = False
+
+    def __init__(self, host_app, profile, user_jid, password, host=None, port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES):
+        # XXX: DNS SRV records are checked when the host is not specified.
+        # If no SRV record is found, the host is directly extracted from the JID.
+        self.started = time.time()
+
+        # Currently, we use "client/pc/Salut à Toi", but as
+        # SàT is multi-frontends and can be used on mobile devices, as a bot, with a web frontend,
+        # etc., we should implement a way to dynamically update identities through the bridge
+        self.identities = [disco.DiscoIdentity(u"client", u"pc", C.APP_NAME)]
+        if sys.platform == "android":
+            # FIXME: temporary hack as SRV is not working on android
+            # TODO: remove this hack and fix SRV
+            log.info(u"FIXME: Android hack, ignoring SRV")
+            host = user_jid.host
+            hosts_map = host_app.memory.getConfig("android", "hosts_dict", {})
+            if host in hosts_map:
+                log.info(u"using {host_to_use} for host {host_ori} as requested in config".format(
+                    host_ori = host,
+                    host_to_use = hosts_map[host]))
+                host = hosts_map[host]
+        wokkel_client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT)
+        SatXMPPEntity.__init__(self, host_app, profile, max_retries)
+        self._progress_cb = {}  # callback called when a progress is requested (key = progress id)
+        self.actions = {} # used to keep track of actions for retrieval (key = action_id)
+
+    def _getPluginsList(self):
+        for p in self.host_app.plugins.itervalues():
+            if C.PLUG_MODE_CLIENT in p._info[u'modes']:
+                yield p
+
+    def _createSubProtocols(self):
+        self.messageProt = SatMessageProtocol(self.host_app)
+        self.messageProt.setHandlerParent(self)
+
+        self.roster = SatRosterProtocol(self.host_app)
+        self.roster.setHandlerParent(self)
+
+        self.presence = SatPresenceProtocol(self.host_app)
+        self.presence.setHandlerParent(self)
+
+    def entityConnected(self):
+        # we want to be sure that we got the roster
+        return self.roster.got_roster
+
+    def addPostXmlCallbacks(self, post_xml_treatments):
+        post_xml_treatments.addCallback(self.messageAddToHistory)
+        post_xml_treatments.addCallback(self.messageSendToBridge)
+
     def send(self, obj):
         # original send method accept string
         # but we restrict to domish.Element to make trigger treatments easier
@@ -109,7 +516,7 @@
         #     return
         super(SatXMPPClient, self).send(obj)
 
-    def sendMessage(self, mess_data):
+    def sendMessageData(self, mess_data):
         """Convenient method to send message data to stream
 
         This method will send mess_data[u'xml'] to stream, but a trigger is there
@@ -121,7 +528,7 @@
         # XXX: This is the last trigger before u"send" (last but one globally) for sending message.
         #      This is intented for e2e encryption which doesn't do full stanza encryption (e.g. OTR)
         #      This trigger point can't cancel the method
-        self.host_app.trigger.point("sendMessageFinish", self, mess_data)
+        self.host_app.trigger.point("sendMessageData", self, mess_data)
         self.send(mess_data[u'xml'])
         return mess_data
 
@@ -130,7 +537,7 @@
 
         This message will be an info message, not recorded in history.
         It can be used to give feedback of a command
-        @param to_jid(jid.Jid): destinee jid
+        @param to_jid(jid.JID): destinee jid
         @param message(unicode): message to send to frontends
         """
         self.host_app.bridge.messageNew(uid=unicode(uuid.uuid4()),
@@ -143,59 +550,108 @@
                                         extra={},
                                         profile=self.profile)
 
-    def _authd(self, xmlstream):
-        if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
-            return
-        wokkel_client.XMPPClient._authd(self, xmlstream)
-        self.__connected = True
-        log.info(_("********** [%s] CONNECTED **********") % self.profile)
-        self.streamInitialized()
-        self.host_app.bridge.connected(self.profile, unicode(self.jid))  # we send the signal to the clients
+    def _finish_connection(self, dummy):
+        self.roster.requestRoster()
+        self.presence.available()
+        super(SatXMPPClient, self)._finish_connection(dummy)
+
+
+class SatXMPPComponent(SatXMPPEntity, component.Component):
+    """XMPP component
+
+    This component are similar but not identical to clients.
+    An entry point plugin is launched after component is connected.
+    Component need to instantiate MessageProtocol itself
+    """
+    implements(iwokkel.IDisco)
+    trigger_suffix = "Component"  # used for to distinguish some trigger points set in SatXMPPEntity
+    component = True
+    sendHistory = False  # XXX: set to True from entry plugin to keep messages in history for received messages
+
+    def __init__(self, host_app, profile, component_jid, password, host=None, port=None, max_retries=C.XMPP_MAX_RETRIES):
+        self.started = time.time()
+        if port is None:
+            port = C.XMPP_COMPONENT_PORT
 
-    def streamInitialized(self):
-        """Called after _authd"""
-        log.debug(_("XML stream is initialized"))
-        self.keep_alife = task.LoopingCall(self.xmlstream.send, " ")  # Needed to avoid disconnection (specially with openfire)
-        self.keep_alife.start(C.XMPP_KEEP_ALIFE)
+        ## entry point ##
+        entry_point = host_app.memory.getEntryPoint(profile)
+        try:
+            self.entry_plugin = host_app.plugins[entry_point]
+        except KeyError:
+            raise exceptions.NotFound(_(u"The requested entry point ({entry_point}) is not available").format(
+                entry_point = entry_point))
 
-        self.disco = SatDiscoProtocol(self)
-        self.disco.setHandlerParent(self)
-        self.discoHandler = disco.DiscoHandler()
-        self.discoHandler.setHandlerParent(self)
-        disco_d = defer.succeed(None)
+        self.identities = [disco.DiscoIdentity(u"component", u"generic", C.APP_NAME)]
+        # jid is set automatically on bind by Twisted for Client, but not for Component
+        self.jid = component_jid
+        if host is None:
+            try:
+                host = component_jid.host.split(u'.', 1)[1]
+            except IndexError:
+                raise ValueError(u"Can't guess host from jid, please specify a host")
+        # XXX: component.Component expect unicode jid, while Client expect jid.JID.
+        #      this is not consistent, so we use jid.JID for SatXMPP*
+        component.Component.__init__(self, host, port, component_jid.full(), password)
+        SatXMPPEntity.__init__(self, host_app, profile, max_retries)
 
-        if not self.host_app.trigger.point("Disco handled", disco_d, self.profile):
-            return
+    def _buildDependencies(self, current, plugins, required=True):
+        """build recursively dependencies needed for a plugin
 
-        def finish_connection(dummy):
-            self.roster.requestRoster()
-            self.presence.available()
-            self.conn_deferred.callback(None)
+        this method build list of plugin needed for a component and raises
+        errors if they are not available or not allowed for components
+        @param current(object): parent plugin to check
+            use entry_point for first call
+        @param plugins(list): list of validated plugins, will be filled by the method
+            give an empty list for first call
+        @param required(bool): True if plugin is mandatory
+            for recursive calls only, should not be modified by inital caller
+        @raise InternalError: one of the plugin is not handling components
+        @raise KeyError: one plugin should be present in self.host_app.plugins but it is not
+        """
+        if C.PLUG_MODE_COMPONENT not in current._info[u'modes']:
+            if not required:
+                return
+            else:
+                log.error(_(u"Plugin {current_name} if needed for {entry_name}, but it doesn't handle component mode").format(
+                    current_name = current._info[u'import_name'],
+                    entry_name = self.entry_plugin._info[u'import_name']
+                    ))
+                raise exceptions.InternalError(_(u"invalid plugin mode"))
 
-        disco_d.addCallback(finish_connection)
+        for import_name in current._info.get(C.PI_DEPENDENCIES, []):
+            # plugins are already loaded as dependencies
+            # so we know they are in self.host_app.plugins
+            dep = self.host_app.plugins[import_name]
+            self._checkDependencies(dep, plugins)
 
-    def initializationFailed(self, reason):
-        log.error(_(u"ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" % {'profile': self.profile, 'reason': reason}))
-        self.conn_deferred.errback(reason.value)
-        try:
-            wokkel_client.XMPPClient.initializationFailed(self, reason)
-        except:
-            # we already chained an errback, no need to raise an exception
-            pass
+        for import_name in current._info.get(C.PI_RECOMMENDATIONS, []):
+            # here plugins are only recommendations,
+            # so they may not exist in self.host_app.plugins
+            try:
+                dep = self.host_app.plugins[import_name]
+            except KeyError:
+                continue
+            self._buildDependencies(dep, plugins, required = False)
+
+        if current not in plugins:
+            # current can be required for several plugins and so
+            # it can already be present in the list
+            plugins.append(current)
 
-    def isConnected(self):
-        return self.__connected
+    def _getPluginsList(self):
+        # XXX: for component we don't launch all plugins triggers
+        #      but only the ones from which there is a dependency
+        plugins = []
+        self._buildDependencies(self.entry_plugin, plugins)
+        return plugins
 
-    def connectionLost(self, connector, unused_reason):
-        try:
-            self.keep_alife.stop()
-        except AttributeError:
-            log.debug(_("No keep_alife"))
-        if self.__connected:
-            log.info(_("********** [%s] DISCONNECTED **********") % self.profile)
-            self.host_app.bridge.disconnected(self.profile)  # we send the signal to the clients
-            self.host_app.purgeClient(self.profile)  # and we remove references to this client
-        self.__connected = False
+    def entityConnected(self):
+        # we can now launch entry point
+        return self.entry_plugin.componentStart(self)
+
+    def addPostXmlCallbacks(self, post_xml_treatments):
+        if self.sendHistory:
+            post_xml_treatments.addCallback(self.messageAddToHistory)
 
 
 class SatMessageProtocol(xmppim.MessageProtocol):
@@ -432,8 +888,6 @@
 
     def isPresenceAuthorised(self, entity_jid):
         """Return True if entity is authorised to see our presence"""
-        import pudb
-        pudb.set_trace()
         try:
             item = self._jids[entity_jid.userhostJID()]
         except KeyError:
@@ -687,15 +1141,16 @@
         # it reject our features (resulting in e.g. no notification on PEP)
         return generic.VersionHandler.getDiscoInfo(self, requestor, target, None)
 
+
 class SatIdentityHandler(XMPPHandler):
-    """ Manage disco Identity of SàT. Currently, we use "client/pc/Salut à Toi", but as
-    SàT is multi-frontends and can be used on mobile devices, as a bot, with a web frontend,
-    etc, we should implement a way to dynamically update identities through the bridge """
+    """ Manage disco Identity of SàT.
+
+    """
     #TODO: dynamic identity update (see docstring). Note that a XMPP entity can have several identities
     implements(iwokkel.IDisco)
 
     def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
-        return [disco.DiscoIdentity(u"client", u"pc", C.APP_NAME)]
+        return self.parent.identities
 
     def getDiscoItems(self, requestor, target, nodeIdentifier=''):
         return []