Mercurial > libervia-backend
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 []