diff sat/core/sat_main.py @ 2562:26edcf3a30eb

core, setup: huge cleaning: - moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention - move twisted directory to root - removed all hacks from setup.py, and added missing dependencies, it is now clean - use https URL for website in setup.py - removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed - renamed sat.sh to sat and fixed its installation - added python_requires to specify Python version needed - replaced glib2reactor which use deprecated code by gtk3reactor sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author Goffi <goffi@goffi.org>
date Mon, 02 Apr 2018 19:44:50 +0200
parents src/core/sat_main.py@27539029a662
children 973d4551ffae
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat/core/sat_main.py	Mon Apr 02 19:44:50 2018 +0200
@@ -0,0 +1,1083 @@
+#!/usr/bin/env python2
+# -*- coding: utf-8 -*-
+
+# SAT: a jabber client
+# Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org)
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import sat
+from sat.core.i18n import _, languageSwitch
+from twisted.application import service
+from twisted.internet import defer
+from twisted.words.protocols.jabber import jid
+from twisted.internet import reactor
+from wokkel.xmppim import RosterItem
+from sat.core import xmpp
+from sat.core import exceptions
+from sat.core.log import getLogger
+log = getLogger(__name__)
+from sat.core.constants import Const as C
+from sat.memory import memory
+from sat.memory import cache
+from sat.tools import trigger
+from sat.tools import utils
+from sat.tools.common import dynamic_import
+from sat.tools.common import regex
+from sat.stdui import ui_contact_list, ui_profile_manager
+import sat.plugins
+from glob import glob
+import sys
+import os.path
+import uuid
+
+try:
+    from collections import OrderedDict # only available from python 2.7
+except ImportError:
+    from ordereddict import OrderedDict
+
+
+class SAT(service.Service):
+
+    def __init__(self):
+        self._cb_map = {}  # map from callback_id to callbacks
+        self._menus = OrderedDict()  # dynamic menus. key: callback_id, value: menu data (dictionnary)
+        self._menus_paths = {}  # path to id. key: (menu_type, lower case tuple of path), value: menu id
+        self.initialised = defer.Deferred()
+        self.profiles = {}
+        self.plugins = {}
+        self.ns_map = {u'x-data': u'jabber:x:data'}  # map for short name to whole namespace,
+                                                      # extended by plugins with registerNamespace
+        self.memory = memory.Memory(self)
+        self.trigger = trigger.TriggerManager()  # trigger are used to change SàT behaviour
+
+        bridge_name = self.memory.getConfig('', 'bridge', 'dbus')
+
+        bridge_module = dynamic_import.bridge(bridge_name)
+        if bridge_module is None:
+            log.error(u"Can't find bridge module of name {}".format(bridge_name))
+            sys.exit(1)
+        log.info(u"using {} bridge".format(bridge_name))
+        try:
+            self.bridge = bridge_module.Bridge()
+        except exceptions.BridgeInitError:
+            log.error(u"Bridge can't be initialised, can't start SàT core")
+            sys.exit(1)
+        self.bridge.register_method("getReady", lambda: self.initialised)
+        self.bridge.register_method("getVersion", lambda: self.full_version)
+        self.bridge.register_method("getFeatures", self.getFeatures)
+        self.bridge.register_method("profileNameGet", self.memory.getProfileName)
+        self.bridge.register_method("profilesListGet", self.memory.getProfilesList)
+        self.bridge.register_method("getEntityData", lambda jid_, keys, profile: self.memory.getEntityData(jid.JID(jid_), keys, profile))
+        self.bridge.register_method("getEntitiesData", self.memory._getEntitiesData)
+        self.bridge.register_method("profileCreate", self.memory.createProfile)
+        self.bridge.register_method("asyncDeleteProfile", self.memory.asyncDeleteProfile)
+        self.bridge.register_method("profileStartSession", self.memory.startSession)
+        self.bridge.register_method("profileIsSessionStarted", self.memory._isSessionStarted)
+        self.bridge.register_method("profileSetDefault", self.memory.profileSetDefault)
+        self.bridge.register_method("connect", self._connect)
+        self.bridge.register_method("disconnect", self.disconnect)
+        self.bridge.register_method("getContacts", self.getContacts)
+        self.bridge.register_method("getContactsFromGroup", self.getContactsFromGroup)
+        self.bridge.register_method("getMainResource", self.memory._getMainResource)
+        self.bridge.register_method("getPresenceStatuses", self.memory._getPresenceStatuses)
+        self.bridge.register_method("getWaitingSub", self.memory.getWaitingSub)
+        self.bridge.register_method("messageSend", self._messageSend)
+        self.bridge.register_method("getConfig", self._getConfig)
+        self.bridge.register_method("setParam", self.setParam)
+        self.bridge.register_method("getParamA", self.memory.getStringParamA)
+        self.bridge.register_method("asyncGetParamA", self.memory.asyncGetStringParamA)
+        self.bridge.register_method("asyncGetParamsValuesFromCategory", self.memory.asyncGetParamsValuesFromCategory)
+        self.bridge.register_method("getParamsUI", self.memory.getParamsUI)
+        self.bridge.register_method("getParamsCategories", self.memory.getParamsCategories)
+        self.bridge.register_method("paramsRegisterApp", self.memory.paramsRegisterApp)
+        self.bridge.register_method("historyGet", self.memory._historyGet)
+        self.bridge.register_method("setPresence", self._setPresence)
+        self.bridge.register_method("subscription", self.subscription)
+        self.bridge.register_method("addContact", self._addContact)
+        self.bridge.register_method("updateContact", self._updateContact)
+        self.bridge.register_method("delContact", self._delContact)
+        self.bridge.register_method("isConnected", self.isConnected)
+        self.bridge.register_method("launchAction", self.launchCallback)
+        self.bridge.register_method("actionsGet", self.actionsGet)
+        self.bridge.register_method("progressGet", self._progressGet)
+        self.bridge.register_method("progressGetAll", self._progressGetAll)
+        self.bridge.register_method("menusGet", self.getMenus)
+        self.bridge.register_method("menuHelpGet", self.getMenuHelp)
+        self.bridge.register_method("menuLaunch", self._launchMenu)
+        self.bridge.register_method("discoInfos", self.memory.disco._discoInfos)
+        self.bridge.register_method("discoItems", self.memory.disco._discoItems)
+        self.bridge.register_method("discoFindByFeatures", self._findByFeatures)
+        self.bridge.register_method("saveParamsTemplate", self.memory.save_xml)
+        self.bridge.register_method("loadParamsTemplate", self.memory.load_xml)
+        self.bridge.register_method("sessionInfosGet", self.getSessionInfos)
+        self.bridge.register_method("namespacesGet", self.getNamespaces)
+
+        self.memory.initialized.addCallback(self._postMemoryInit)
+
+    @property
+    def version(self):
+        """Return the short version of SàT"""
+        return C.APP_VERSION
+
+    @property
+    def full_version(self):
+        """Return the full version of SàT (with release name and extra data when in development mode)"""
+        version = self.version
+        if version[-1] == 'D':
+            # we are in debug version, we add extra data
+            try:
+                return self._version_cache
+            except AttributeError:
+                self._version_cache = u"{} « {} » ({})".format(version, C.APP_RELEASE_NAME, utils.getRepositoryData(sat))
+                return self._version_cache
+        else:
+            return version
+
+    @property
+    def bridge_name(self):
+        return os.path.splitext(os.path.basename(self.bridge.__file__))[0]
+
+    def _postMemoryInit(self, ignore):
+        """Method called after memory initialization is done"""
+        self.common_cache = cache.Cache(self, None)
+        log.info(_("Memory initialised"))
+        try:
+            self._import_plugins()
+            ui_contact_list.ContactList(self)
+            ui_profile_manager.ProfileManager(self)
+        except Exception as e:
+            log.error(_(u"Could not initialize backend: {reason}").format(
+                reason = str(e).decode('utf-8', 'ignore')))
+            sys.exit(1)
+        self.initialised.callback(None)
+        log.info(_(u"Backend is ready"))
+
+    def _unimport_plugin(self, plugin_path):
+        """remove a plugin from sys.modules if it is there"""
+        try:
+            del sys.modules[plugin_path]
+        except KeyError:
+            pass
+
+    def _import_plugins(self):
+        """Import all plugins found in plugins directory"""
+        # FIXME: module imported but cancelled should be deleted
+        # TODO: make this more generic and reusable in tools.common
+        # FIXME: should use imp
+        # TODO: do not import all plugins if no needed: component plugins are not needed if we
+        #       just use a client, and plugin blacklisting should be possible in sat.conf
+        plugins_path = os.path.dirname(sat.plugins.__file__)
+        plugin_glob = "plugin*." + C.PLUGIN_EXT
+        plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename, glob(os.path.join(plugins_path, plugin_glob)))]
+        plugins_to_import = {}  # plugins we still have to import
+        for plug in plug_lst:
+            plugin_path = 'sat.plugins.' + plug
+            try:
+                __import__(plugin_path)
+            except exceptions.MissingModule as e:
+                self._unimport_plugin(plugin_path)
+                log.warning(u"Can't import plugin [{path}] because of an unavailale third party module:\n{msg}".format(
+                    path=plugin_path, msg=e))
+                continue
+            except exceptions.CancelError as e:
+                log.info(u"Plugin [{path}] cancelled its own import: {msg}".format(path=plugin_path, msg=e))
+                self._unimport_plugin(plugin_path)
+                continue
+            except Exception as e:
+                import traceback
+                log.error(_(u"Can't import plugin [{path}]:\n{error}").format(path=plugin_path, error=traceback.format_exc()))
+                self._unimport_plugin(plugin_path)
+                continue
+            mod = sys.modules[plugin_path]
+            plugin_info = mod.PLUGIN_INFO
+            import_name = plugin_info['import_name']
+
+            plugin_modes = plugin_info[u'modes'] = set(plugin_info.setdefault(u"modes", C.PLUG_MODE_DEFAULT))
+
+            # if the plugin is an entry point, it must work in component mode
+            if plugin_info[u'type'] == C.PLUG_TYPE_ENTRY_POINT:
+                # if plugin is an entrypoint, we cache it
+                if C.PLUG_MODE_COMPONENT not in plugin_modes:
+                    log.error(_(u"{type} type must be used with {mode} mode, ignoring plugin").format(
+                        type = C.PLUG_TYPE_ENTRY_POINT, mode = C.PLUG_MODE_COMPONENT))
+                    self._unimport_plugin(plugin_path)
+                    continue
+
+            if import_name in plugins_to_import:
+                log.error(_(u"Name conflict for import name [{import_name}], can't import plugin [{name}]").format(**plugin_info))
+                continue
+            plugins_to_import[import_name] = (plugin_path, mod, plugin_info)
+        while True:
+            try:
+                self._import_plugins_from_dict(plugins_to_import)
+            except ImportError:
+                pass
+            if not plugins_to_import:
+                break
+
+    def _import_plugins_from_dict(self, plugins_to_import, import_name=None, optional=False):
+        """Recursively import and their dependencies in the right order
+
+        @param plugins_to_import(dict): key=import_name and values=(plugin_path, module, plugin_info)
+        @param import_name(unicode, None): name of the plugin to import as found in PLUGIN_INFO['import_name']
+        @param optional(bool): if False and plugin is not found, an ImportError exception is raised
+        """
+        if import_name in self.plugins:
+            log.debug(u'Plugin {} already imported, passing'.format(import_name))
+            return
+        if not import_name:
+            import_name, (plugin_path, mod, plugin_info) = plugins_to_import.popitem()
+        else:
+            if not import_name in plugins_to_import:
+                if optional:
+                    log.warning(_(u"Recommended plugin not found: {}").format(import_name))
+                    return
+                msg = u"Dependency not found: {}".format(import_name)
+                log.error(msg)
+                raise ImportError(msg)
+            plugin_path, mod, plugin_info = plugins_to_import.pop(import_name)
+        dependencies = plugin_info.setdefault("dependencies", [])
+        recommendations = plugin_info.setdefault("recommendations", [])
+        for to_import in dependencies + recommendations:
+            if to_import not in self.plugins:
+                log.debug(u'Recursively import dependency of [%s]: [%s]' % (import_name, to_import))
+                try:
+                    self._import_plugins_from_dict(plugins_to_import, to_import, to_import not in dependencies)
+                except ImportError as e:
+                    log.warning(_(u"Can't import plugin {name}: {error}").format(name=plugin_info['name'], error=e))
+                    if optional:
+                        return
+                    raise e
+        log.info("importing plugin: {}".format(plugin_info['name']))
+        # we instanciate the plugin here
+        try:
+            self.plugins[import_name] = getattr(mod, plugin_info['main'])(self)
+        except Exception as e:
+            log.warning(u'Error while loading plugin "{name}", ignoring it: {error}'
+                .format(name=plugin_info['name'], error=e))
+            if optional:
+                return
+            raise ImportError(u"Error during initiation")
+        if C.bool(plugin_info.get(C.PI_HANDLER, C.BOOL_FALSE)):
+            self.plugins[import_name].is_handler = True
+        else:
+            self.plugins[import_name].is_handler = False
+        # we keep metadata as a Class attribute
+        self.plugins[import_name]._info = plugin_info
+        #TODO: test xmppclient presence and register handler parent
+
+    def pluginsUnload(self):
+        """Call unload method on every loaded plugin, if exists
+
+        @return (D): A deferred which return None when all method have been called
+        """
+        # TODO: in the futur, it should be possible to hot unload a plugin
+        #       pluging depending on the unloaded one should be unloaded too
+        #       for now, just a basic call on plugin.unload is done
+        defers_list = []
+        for plugin in self.plugins.itervalues():
+            try:
+                unload = plugin.unload
+            except AttributeError:
+                continue
+            else:
+                defers_list.append(defer.maybeDeferred(unload))
+        return defers_list
+
+    def _connect(self, profile_key, password='', options=None):
+        profile = self.memory.getProfileName(profile_key)
+        return self.connect(profile, password, options)
+
+    def connect(self, profile, password='', options=None, max_retries=C.XMPP_MAX_RETRIES):
+        """Connect a profile (i.e. connect client.component to XMPP server)
+
+        Retrieve the individual parameters, authenticate the profile
+        and initiate the connection to the associated XMPP server.
+        @param profile: %(doc_profile)s
+        @param password (string): the SàT profile password
+        @param options (dict): connection options. Key can be:
+            -
+        @param max_retries (int): max number of connection retries
+        @return (D(bool)):
+            - True if the XMPP connection was already established
+            - False if the XMPP connection has been initiated (it may still fail)
+        @raise exceptions.PasswordError: Profile password is wrong
+        """
+        if options is None:
+            options={}
+        def connectProfile(dummy=None):
+            if self.isConnected(profile):
+                log.info(_("already connected !"))
+                return True
+
+            if self.memory.isComponent(profile):
+                d = xmpp.SatXMPPComponent.startConnection(self, profile, max_retries)
+            else:
+                d = xmpp.SatXMPPClient.startConnection(self, profile, max_retries)
+            return d.addCallback(lambda dummy: False)
+
+        d = self.memory.startSession(password, profile)
+        d.addCallback(connectProfile)
+        return d
+
+    def disconnect(self, profile_key):
+        """disconnect from jabber server"""
+        # FIXME: client should not be deleted if only disconnected
+        #        it shoud be deleted only when session is finished
+        if not self.isConnected(profile_key):
+            # isConnected is checked here and not on client
+            # because client is deleted when session is ended
+            log.info(_(u"not connected !"))
+            return defer.succeed(None)
+        client = self.getClient(profile_key)
+        return client.entityDisconnect()
+
+    def getFeatures(self, profile_key=C.PROF_KEY_NONE):
+        """Get available features
+
+        Return list of activated plugins and plugin specific data
+        @param profile_key: %(doc_profile_key)s
+            C.PROF_KEY_NONE can be used to have general plugins data (i.e. not profile dependent)
+        @return (dict)[Deferred]: features data where:
+            - key is plugin import name, present only for activated plugins
+            - value is a an other dict, when meaning is specific to each plugin.
+                this dict is return by plugin's getFeature method.
+                If this method doesn't exists, an empty dict is returned.
+        """
+        try:
+            # FIXME: there is no method yet to check profile session
+            #        as soon as one is implemented, it should be used here
+            self.getClient(profile_key)
+        except KeyError:
+            log.warning("Requesting features for a profile outside a session")
+            profile_key = C.PROF_KEY_NONE
+        except exceptions.ProfileNotSetError:
+            pass
+
+        features = []
+        for import_name, plugin in self.plugins.iteritems():
+            try:
+                features_d = defer.maybeDeferred(plugin.getFeatures, profile_key)
+            except AttributeError:
+                features_d = defer.succeed({})
+            features.append(features_d)
+
+        d_list = defer.DeferredList(features)
+        def buildFeatures(result, import_names):
+            assert len(result) == len(import_names)
+            ret = {}
+            for name, (success, data) in zip (import_names, result):
+                if success:
+                    ret[name] = data
+                else:
+                    log.warning(u"Error while getting features for {name}: {failure}".format(
+                        name=name, failure=data))
+                    ret[name] = {}
+            return ret
+
+        d_list.addCallback(buildFeatures, self.plugins.keys())
+        return d_list
+
+    def getContacts(self, profile_key):
+        client = self.getClient(profile_key)
+        def got_roster(dummy):
+            ret = []
+            for item in client.roster.getItems():  # we get all items for client's roster
+                # and convert them to expected format
+                attr = client.roster.getAttributes(item)
+                ret.append([item.jid.userhost(), attr, item.groups])
+            return ret
+
+        return client.roster.got_roster.addCallback(got_roster)
+
+    def getContactsFromGroup(self, group, profile_key):
+        client = self.getClient(profile_key)
+        return [jid_.full() for jid_ in client.roster.getJidsFromGroup(group)]
+
+    def purgeEntity(self, profile):
+        """Remove reference to a profile client/component and purge cache
+
+        the garbage collector can then free the memory
+        """
+        try:
+            del self.profiles[profile]
+        except KeyError:
+            log.error(_("Trying to remove reference to a client not referenced"))
+        else:
+            self.memory.purgeProfileSession(profile)
+
+    def startService(self):
+        log.info(u"Salut à toi ô mon frère !")
+
+    def stopService(self):
+        log.info(u"Salut aussi à Rantanplan")
+        return self.pluginsUnload()
+
+    def run(self):
+        log.debug(_("running app"))
+        reactor.run()
+
+    def stop(self):
+        log.debug(_("stopping app"))
+        reactor.stop()
+
+    ## Misc methods ##
+
+    def getJidNStream(self, profile_key):
+        """Convenient method to get jid and stream from profile key
+        @return: tuple (jid, xmlstream) from profile, can be None"""
+        # TODO: deprecate this method (getClient is enough)
+        profile = self.memory.getProfileName(profile_key)
+        if not profile or not self.profiles[profile].isConnected():
+            return (None, None)
+        return (self.profiles[profile].jid, self.profiles[profile].xmlstream)
+
+    def getClient(self, profile_key):
+        """Convenient method to get client from profile key
+
+        @return: client or None if it doesn't exist
+        @raise exceptions.ProfileKeyUnknown: the profile or profile key doesn't exist
+        @raise exceptions.NotFound: client is not available
+            This happen if profile has not been used yet
+        """
+        profile = self.memory.getProfileName(profile_key)
+        if not profile:
+            raise exceptions.ProfileKeyUnknown
+        try:
+            return self.profiles[profile]
+        except KeyError:
+            raise exceptions.NotFound(profile_key)
+
+    def getClients(self, profile_key):
+        """Convenient method to get list of clients from profile key (manage list through profile_key like C.PROF_KEY_ALL)
+
+        @param profile_key: %(doc_profile_key)s
+        @return: list of clients
+        """
+        if not profile_key:
+            raise exceptions.DataError(_(u'profile_key must not be empty'))
+        try:
+            profile = self.memory.getProfileName(profile_key, True)
+        except exceptions.ProfileUnknownError:
+            return []
+        if profile == C.PROF_KEY_ALL:
+            return self.profiles.values()
+        elif profile[0] == '@':  # only profile keys can start with "@"
+            raise exceptions.ProfileKeyUnknown
+        return [self.profiles[profile]]
+
+    def _getConfig(self, section, name):
+        """Get the main configuration option
+
+        @param section: section of the config file (None or '' for DEFAULT)
+        @param name: name of the option
+        @return: unicode representation of the option
+        """
+        return unicode(self.memory.getConfig(section, name, ''))
+
+    def logErrback(self, failure_):
+        """generic errback logging
+
+        can be used as last errback to show unexpected error
+        """
+        log.error(_(u"Unexpected error: {}".format(failure_)))
+        return failure_
+
+    # namespaces
+
+    def registerNamespace(self, short_name, namespace):
+        """associate a namespace to a short name"""
+        if short_name in self.ns_map:
+            raise exceptions.ConflictError(u'this short name is already used')
+        self.ns_map[short_name] = namespace
+
+    def getNamespaces(self):
+        return self.ns_map
+
+    def getSessionInfos(self, profile_key):
+        """compile interesting data on current profile session"""
+        client = self.getClient(profile_key)
+        data = {
+            "jid": client.jid.full(),
+            "started": unicode(int(client.started)),
+            }
+        return defer.succeed(data)
+
+    # local dirs
+
+    def getLocalPath(self, client, dir_name, *extra_path, **kwargs):
+        """retrieve path for local data
+
+        if path doesn't exist, it will be created
+        @param client(SatXMPPClient, None): client instance
+            used when profile is set, can be None if profile is False
+        @param dir_name(unicode): name of the main path directory
+        @param component(bool): if True, path will be prefixed with C.COMPONENTS_DIR
+        @param profile(bool): if True, path will be suffixed by profile name
+        @param *extra_path: extra path element(s) to use
+        @return (unicode): path
+        """
+        # FIXME: component and profile are parsed with **kwargs because of python 2 limitations
+        #        once moved to python 3, this can be fixed
+        component = kwargs.pop('component', False)
+        profile = kwargs.pop('profile', True)
+        assert not kwargs
+
+        path_elts = [self.memory.getConfig('', 'local_dir')]
+        if component:
+            path_elts.append(C.COMPONENTS_DIR)
+        path_elts.append(regex.pathEscape(dir_name))
+        if extra_path:
+            path_elts.extend([regex.pathEscape(p) for p in extra_path])
+        if profile:
+            regex.pathEscape(client.profile)
+        path = os.path.join(*path_elts)
+        if not os.path.exists(path):
+            os.makedirs(path)
+        return path
+
+    ## Client management ##
+
+    def setParam(self, name, value, category, security_limit, profile_key):
+        """set wanted paramater and notice observers"""
+        self.memory.setParam(name, value, category, security_limit, profile_key)
+
+    def isConnected(self, profile_key):
+        """Return connection status of profile
+        @param profile_key: key_word or profile name to determine profile name
+        @return: True if connected
+        """
+        profile = self.memory.getProfileName(profile_key)
+        if not profile:
+            log.error(_('asking connection status for a non-existant profile'))
+            raise exceptions.ProfileUnknownError(profile_key)
+        if profile not in self.profiles:
+            return False
+        return self.profiles[profile].isConnected()
+
+    ## XMPP methods ##
+
+    def _messageSend(self, to_jid_s, message, subject=None, mess_type='auto', extra=None, profile_key=C.PROF_KEY_NONE):
+        client = self.getClient(profile_key)
+        to_jid = jid.JID(to_jid_s)
+        #XXX: we need to use the dictionary comprehension because D-Bus return its own types, and pickle can't manage them. TODO: Need to find a better way
+        return client.sendMessage(to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()})
+
+    def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE):
+        return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key)
+
+    def setPresence(self, to_jid=None, show="", statuses=None, profile_key=C.PROF_KEY_NONE):
+        """Send our presence information"""
+        if statuses is None:
+            statuses = {}
+        profile = self.memory.getProfileName(profile_key)
+        assert profile
+        priority = int(self.memory.getParamA("Priority", "Connection", profile_key=profile))
+        self.profiles[profile].presence.available(to_jid, show, statuses, priority)
+        #XXX: FIXME: temporary fix to work around openfire 3.7.0 bug (presence is not broadcasted to generating resource)
+        if '' in statuses:
+            statuses[C.PRESENCE_STATUSES_DEFAULT] = statuses.pop('')
+        self.bridge.presenceUpdate(self.profiles[profile].jid.full(), show,
+                                   int(priority), statuses, profile)
+
+    def subscription(self, subs_type, raw_jid, profile_key):
+        """Called to manage subscription
+        @param subs_type: subsciption type (cf RFC 3921)
+        @param raw_jid: unicode entity's jid
+        @param profile_key: profile"""
+        profile = self.memory.getProfileName(profile_key)
+        assert profile
+        to_jid = jid.JID(raw_jid)
+        log.debug(_(u'subsciption request [%(subs_type)s] for %(jid)s') % {'subs_type': subs_type, 'jid': to_jid.full()})
+        if subs_type == "subscribe":
+            self.profiles[profile].presence.subscribe(to_jid)
+        elif subs_type == "subscribed":
+            self.profiles[profile].presence.subscribed(to_jid)
+        elif subs_type == "unsubscribe":
+            self.profiles[profile].presence.unsubscribe(to_jid)
+        elif subs_type == "unsubscribed":
+            self.profiles[profile].presence.unsubscribed(to_jid)
+
+    def _addContact(self, to_jid_s, profile_key):
+        return self.addContact(jid.JID(to_jid_s), profile_key)
+
+    def addContact(self, to_jid, profile_key):
+        """Add a contact in roster list"""
+        profile = self.memory.getProfileName(profile_key)
+        assert profile
+        # presence is sufficient, as a roster push will be sent according to RFC 6121 §3.1.2
+        self.profiles[profile].presence.subscribe(to_jid)
+
+    def _updateContact(self, to_jid_s, name, groups, profile_key):
+        return self.updateContact(jid.JID(to_jid_s), name, groups, profile_key)
+
+    def updateContact(self, to_jid, name, groups, profile_key):
+        """update a contact in roster list"""
+        profile = self.memory.getProfileName(profile_key)
+        assert profile
+        groups = set(groups)
+        roster_item = RosterItem(to_jid)
+        roster_item.name = name or None
+        roster_item.groups = set(groups)
+        return self.profiles[profile].roster.setItem(roster_item)
+
+    def _delContact(self, to_jid_s, profile_key):
+        return self.delContact(jid.JID(to_jid_s), profile_key)
+
+    def delContact(self, to_jid, profile_key):
+        """Remove contact from roster list"""
+        profile = self.memory.getProfileName(profile_key)
+        assert profile
+        self.profiles[profile].presence.unsubscribe(to_jid)  # is not asynchronous
+        return self.profiles[profile].roster.removeItem(to_jid)
+
+    ## Discovery ##
+    # discovery methods are shortcuts to self.memory.disco
+    # the main difference with client.disco is that self.memory.disco manage cache
+
+    def hasFeature(self, *args, **kwargs):
+        return self.memory.disco.hasFeature(*args, **kwargs)
+
+    def checkFeature(self, *args, **kwargs):
+        return self.memory.disco.checkFeature(*args, **kwargs)
+
+    def checkFeatures(self, *args, **kwargs):
+        return self.memory.disco.checkFeatures(*args, **kwargs)
+
+    def getDiscoInfos(self, *args, **kwargs):
+        return self.memory.disco.getInfos(*args, **kwargs)
+
+    def getDiscoItems(self, *args, **kwargs):
+        return self.memory.disco.getItems(*args, **kwargs)
+
+    def findServiceEntity(self, *args, **kwargs):
+        return self.memory.disco.findServiceEntity(*args, **kwargs)
+
+    def findServiceEntities(self, *args, **kwargs):
+        return self.memory.disco.findServiceEntities(*args, **kwargs)
+
+    def findFeaturesSet(self, *args, **kwargs):
+        return self.memory.disco.findFeaturesSet(*args, **kwargs)
+
+    def _findByFeatures(self, namespaces, identities, bare_jids, service, roster, own_jid, profile_key):
+        client = self.getClient(profile_key)
+        return self.findByFeatures(client, namespaces, identities, bare_jids, service, roster, own_jid)
+
+    @defer.inlineCallbacks
+    def findByFeatures(self, client, namespaces, identities=None, bare_jids=False, service=True, roster=True, own_jid=True):
+        """retrieve all services or contacts managing a set a features
+
+        @param namespaces(list[unicode]): features which must be handled
+        @param identities(list[tuple[unicode,unicode]], None): if not None or empty, only keep those identities
+            tuple must by (category, type)
+        @param bare_jids(bool): retrieve only bare_jids if True
+            if False, retrieve full jid of connected devices
+        @param service(bool): if True return service from our roster
+        @param roster(bool): if True, return entities in roster
+            full jid of all matching resources available will be returned
+        @param own_jid(bool): if True, return profile's jid resources
+        @return (tuple(dict[jid.JID(), tuple[unicode, unicode, unicode]]*3)): found entities in a tuple with:
+            - service entities
+            - own entities
+            - roster entities
+        """
+        if not identities:
+            identities = None
+        if not namespaces and not identities:
+            raise exceptions.DataError("at least one namespace or one identity must be set")
+        found_service = {}
+        found_own = {}
+        found_roster = {}
+        if service:
+            services_jids = yield self.findFeaturesSet(client, namespaces)
+            for service_jid in services_jids:
+                infos = yield self.getDiscoInfos(client, service_jid)
+                if identities is not None and not set(infos.identities.keys()).issuperset(identities):
+                    continue
+                found_identities = [(cat, type_, name or u'') for (cat, type_), name in infos.identities.iteritems()]
+                found_service[service_jid.full()] = found_identities
+
+        jids = []
+        if roster:
+            jids.extend(client.roster.getJids())
+        if own_jid:
+            jids.append(client.jid.userhostJID())
+
+        for found, jids in ((found_own, [client.jid.userhostJID()]),
+                            (found_roster, client.roster.getJids())):
+            for jid_ in jids:
+                if jid_.resource:
+                    if bare_jids:
+                        continue
+                    resources = [jid_.resource]
+                else:
+                    if bare_jids:
+                        resources = [None]
+                    else:
+                        try:
+                            resources = self.memory.getAllResources(client, jid_)
+                        except exceptions.UnknownEntityError:
+                            continue
+                for resource in resources:
+                    full_jid = jid.JID(tuple=(jid_.user, jid_.host, resource))
+                    infos = yield self.getDiscoInfos(client, full_jid)
+                    if infos.features.issuperset(namespaces):
+                        if identities is not None and not set(infos.identities.keys()).issuperset(identities):
+                            continue
+                        found_identities = [(cat, type_, name or u'') for (cat, type_), name in infos.identities.iteritems()]
+                        found[full_jid.full()] = found_identities
+
+        defer.returnValue((found_service, found_own, found_roster))
+
+    ## Generic HMI ##
+
+    def _killAction(self, keep_id, client):
+        log.debug(u"Killing action {} for timeout".format(keep_id))
+        client.actions[keep_id]
+
+    def actionNew(self, action_data, security_limit=C.NO_SECURITY_LIMIT, keep_id=None, profile=C.PROF_KEY_NONE):
+        """Shortcut to bridge.actionNew which generate and id and keep for retrieval
+
+        @param action_data(dict): action data (see bridge documentation)
+        @param security_limit: %(doc_security_limit)s
+        @param keep_id(None, unicode): if not None, used to keep action for differed retrieval
+            must be set to the callback_id
+            action will be deleted after 30 min.
+        @param profile: %(doc_profile)s
+        """
+        id_ = unicode(uuid.uuid4())
+        if keep_id is not None:
+            client = self.getClient(profile)
+            action_timer = reactor.callLater(60*30, self._killAction, keep_id, client)
+            client.actions[keep_id] = (action_data, id_, security_limit, action_timer)
+
+        self.bridge.actionNew(action_data, id_, security_limit, profile)
+
+    def actionsGet(self, profile):
+        """Return current non answered actions
+
+        @param profile: %(doc_profile)s
+        """
+        client = self.getClient(profile)
+        return [action_tuple[:-1] for action_tuple in client.actions.itervalues()]
+
+    def registerProgressCb(self, progress_id, callback, metadata=None, profile=C.PROF_KEY_NONE):
+        """Register a callback called when progress is requested for id"""
+        if metadata is None:
+            metadata = {}
+        client = self.getClient(profile)
+        if progress_id in client._progress_cb:
+            raise exceptions.ConflictError(u"Progress ID is not unique !")
+        client._progress_cb[progress_id] = (callback, metadata)
+
+    def removeProgressCb(self, progress_id, profile):
+        """Remove a progress callback"""
+        client = self.getClient(profile)
+        try:
+            del client._progress_cb[progress_id]
+        except KeyError:
+            log.error(_(u"Trying to remove an unknow progress callback"))
+
+    def _progressGet(self, progress_id, profile):
+        data = self.progressGet(progress_id, profile)
+        return {k: unicode(v) for k,v in data.iteritems()}
+
+    def progressGet(self, progress_id, profile):
+        """Return a dict with progress information
+
+        @param progress_id(unicode): unique id of the progressing element
+        @param profile: %(doc_profile)s
+        @return (dict): data with the following keys:
+            'position' (int): current possition
+            'size' (int): end_position
+            if id doesn't exists (may be a finished progression), and empty dict is returned
+        """
+        client = self.getClient(profile)
+        try:
+            data = client._progress_cb[progress_id][0](progress_id, profile)
+        except KeyError:
+            data = {}
+        return data
+
+    def _progressGetAll(self, profile_key):
+        progress_all = self.progressGetAll(profile_key)
+        for profile, progress_dict in progress_all.iteritems():
+            for progress_id, data in progress_dict.iteritems():
+                for key, value in data.iteritems():
+                    data[key] = unicode(value)
+        return progress_all
+
+    def progressGetAllMetadata(self, profile_key):
+        """Return all progress metadata at once
+
+        @param profile_key: %(doc_profile)s
+            if C.PROF_KEY_ALL is used, all progress metadata from all profiles are returned
+        @return (dict[dict[dict]]): a dict which map profile to progress_dict
+            progress_dict map progress_id to progress_data
+            progress_metadata is the same dict as sent by [progressStarted]
+        """
+        clients = self.getClients(profile_key)
+        progress_all = {}
+        for client in clients:
+            profile = client.profile
+            progress_dict = {}
+            progress_all[profile] = progress_dict
+            for progress_id, (dummy, progress_metadata) in client._progress_cb.iteritems():
+                progress_dict[progress_id] = progress_metadata
+        return progress_all
+
+    def progressGetAll(self, profile_key):
+        """Return all progress status at once
+
+        @param profile_key: %(doc_profile)s
+            if C.PROF_KEY_ALL is used, all progress status from all profiles are returned
+        @return (dict[dict[dict]]): a dict which map profile to progress_dict
+            progress_dict map progress_id to progress_data
+            progress_data is the same dict as returned by [progressGet]
+        """
+        clients = self.getClients(profile_key)
+        progress_all = {}
+        for client in clients:
+            profile = client.profile
+            progress_dict = {}
+            progress_all[profile] = progress_dict
+            for progress_id, (progress_cb, dummy) in client._progress_cb.iteritems():
+                progress_dict[progress_id] = progress_cb(progress_id, profile)
+        return progress_all
+
+    def registerCallback(self, callback, *args, **kwargs):
+        """Register a callback.
+
+        @param callback(callable): method to call
+        @param kwargs: can contain:
+            with_data(bool): True if the callback use the optional data dict
+            force_id(unicode): id to avoid generated id. Can lead to name conflict, avoid if possible
+            one_shot(bool): True to delete callback once it have been called
+        @return: id of the registered callback
+        """
+        callback_id = kwargs.pop('force_id', None)
+        if callback_id is None:
+            callback_id = str(uuid.uuid4())
+        else:
+            if callback_id in self._cb_map:
+                raise exceptions.ConflictError(_(u"id already registered"))
+        self._cb_map[callback_id] = (callback, args, kwargs)
+
+        if "one_shot" in kwargs: # One Shot callback are removed after 30 min
+            def purgeCallback():
+                try:
+                    self.removeCallback(callback_id)
+                except KeyError:
+                    pass
+            reactor.callLater(1800, purgeCallback)
+
+        return callback_id
+
+    def removeCallback(self, callback_id):
+        """ Remove a previously registered callback
+        @param callback_id: id returned by [registerCallback] """
+        log.debug("Removing callback [%s]" % callback_id)
+        del self._cb_map[callback_id]
+
+    def launchCallback(self, callback_id, data=None, profile_key=C.PROF_KEY_NONE):
+        """Launch a specific callback
+
+        @param callback_id: id of the action (callback) to launch
+        @param data: optional data
+        @profile_key: %(doc_profile_key)s
+        @return: a deferred which fire a dict where key can be:
+            - xmlui: a XMLUI need to be displayed
+            - validated: if present, can be used to launch a callback, it can have the values
+                - C.BOOL_TRUE
+                - C.BOOL_FALSE
+        """
+        # FIXME: security limit need to be checked here
+        try:
+            client = self.getClient(profile_key)
+        except exceptions.NotFound:
+            # client is not available yet
+            profile = self.memory.getProfileName(profile_key)
+            if not profile:
+                raise exceptions.ProfileUnknownError(_(u'trying to launch action with a non-existant profile'))
+        else:
+            profile = client.profile
+            # we check if the action is kept, and remove it
+            try:
+                action_tuple = client.actions[callback_id]
+            except KeyError:
+                pass
+            else:
+                action_tuple[-1].cancel() # the last item is the action timer
+                del client.actions[callback_id]
+
+        try:
+            callback, args, kwargs = self._cb_map[callback_id]
+        except KeyError:
+            raise exceptions.DataError(u"Unknown callback id {}".format(callback_id))
+
+        if kwargs.get("with_data", False):
+            if data is None:
+                raise exceptions.DataError("Required data for this callback is missing")
+            args,kwargs=list(args)[:],kwargs.copy() # we don't want to modify the original (kw)args
+            args.insert(0, data)
+            kwargs["profile"] = profile
+            del kwargs["with_data"]
+
+        if kwargs.pop('one_shot', False):
+            self.removeCallback(callback_id)
+
+        return defer.maybeDeferred(callback, *args, **kwargs)
+
+    #Menus management
+
+    def _getMenuCanonicalPath(self, path):
+        """give canonical form of path
+
+        canonical form is a tuple of the path were every element is stripped and lowercase
+        @param path(iterable[unicode]): untranslated path to menu
+        @return (tuple[unicode]): canonical form of path
+        """
+        return tuple((p.lower().strip() for p in path))
+
+    def importMenu(self, path, callback, security_limit=C.NO_SECURITY_LIMIT, help_string="", type_=C.MENU_GLOBAL):
+        """register a new menu for frontends
+
+        @param path(iterable[unicode]): path to go to the menu (category/subcategory/.../item) (e.g.: ("File", "Open"))
+            /!\ use D_() instead of _() for translations (e.g. (D_("File"), D_("Open")))
+            untranslated/lower case path can be used to identity a menu, for this reason it must be unique independently of case.
+        @param callback(callable): method to be called when menuitem is selected, callable or a callback id (string) as returned by [registerCallback]
+        @param security_limit(int): %(doc_security_limit)s
+            /!\ security_limit MUST be added to data in launchCallback if used #TODO
+        @param help_string(unicode): string used to indicate what the menu do (can be show as a tooltip).
+            /!\ use D_() instead of _() for translations
+        @param type(unicode): one of:
+            - C.MENU_GLOBAL: classical menu, can be shown in a menubar on top (e.g. something like File/Open)
+            - C.MENU_ROOM: like a global menu, but only shown in multi-user chat
+                menu_data must contain a "room_jid" data
+            - C.MENU_SINGLE: like a global menu, but only shown in one2one chat
+                menu_data must contain a "jid" data
+            - C.MENU_JID_CONTEXT: contextual menu, used with any jid (e.g.: ad hoc commands, jid is already filled)
+                menu_data must contain a "jid" data
+            - C.MENU_ROSTER_JID_CONTEXT: like JID_CONTEXT, but restricted to jids in roster.
+                menu_data must contain a "room_jid" data
+            - C.MENU_ROSTER_GROUP_CONTEXT: contextual menu, used with group (e.g.: publish microblog, group is already filled)
+                menu_data must contain a "group" data
+        @return (unicode): menu_id (same as callback_id)
+        """
+
+        if callable(callback):
+            callback_id = self.registerCallback(callback, with_data=True)
+        elif isinstance(callback, basestring):
+            # The callback is already registered
+            callback_id = callback
+            try:
+                callback, args, kwargs = self._cb_map[callback_id]
+            except KeyError:
+                raise exceptions.DataError("Unknown callback id")
+            kwargs["with_data"] = True # we have to be sure that we use extra data
+        else:
+            raise exceptions.DataError("Unknown callback type")
+
+        for menu_data in self._menus.itervalues():
+            if menu_data['path'] == path and menu_data['type'] == type_:
+                raise exceptions.ConflictError(_("A menu with the same path and type already exists"))
+
+        path_canonical = self._getMenuCanonicalPath(path)
+        menu_key = (type_, path_canonical)
+
+        if menu_key in self._menus_paths:
+            raise exceptions.ConflictError(u"this menu path is already used: {path} ({menu_key})".format(
+                path=path_canonical, menu_key=menu_key))
+
+        menu_data = {'path': tuple(path),
+                     'path_canonical': path_canonical,
+                     'security_limit': security_limit,
+                     'help_string': help_string,
+                     'type': type_
+                    }
+
+        self._menus[callback_id] = menu_data
+        self._menus_paths[menu_key] = callback_id
+
+        return callback_id
+
+    def getMenus(self, language='', security_limit=C.NO_SECURITY_LIMIT):
+        """Return all menus registered
+
+        @param language: language used for translation, or empty string for default
+        @param security_limit: %(doc_security_limit)s
+        @return: array of tuple with:
+            - menu id (same as callback_id)
+            - menu type
+            - raw menu path (array of strings)
+            - translated menu path
+            - extra (dict(unicode, unicode)): extra data where key can be:
+                - icon: name of the icon to use (TODO)
+                - help_url: link to a page with more complete documentation (TODO)
+        """
+        ret = []
+        for menu_id, menu_data in self._menus.iteritems():
+            type_ = menu_data['type']
+            path = menu_data['path']
+            menu_security_limit = menu_data['security_limit']
+            if security_limit!=C.NO_SECURITY_LIMIT and (menu_security_limit==C.NO_SECURITY_LIMIT or menu_security_limit>security_limit):
+                continue
+            languageSwitch(language)
+            path_i18n = [_(elt) for elt in path]
+            languageSwitch()
+            extra = {} # TODO: manage extra data like icon
+            ret.append((menu_id, type_, path, path_i18n, extra))
+
+        return ret
+
+    def _launchMenu(self, menu_type, path, data=None, security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE):
+        client = self.getClient(profile_key)
+        return self.launchMenu(client, menu_type, path, data, security_limit)
+
+    def launchMenu(self, client, menu_type, path, data=None, security_limit=C.NO_SECURITY_LIMIT):
+        """launch action a menu action
+
+        @param menu_type(unicode): type of menu to launch
+        @param path(iterable[unicode]): canonical path of the menu
+        @params data(dict): menu data
+        @raise NotFound: this path is not known
+        """
+        # FIXME: manage security_limit here
+        #        defaut security limit should be high instead of C.NO_SECURITY_LIMIT
+        canonical_path = self._getMenuCanonicalPath(path)
+        menu_key = (menu_type, canonical_path)
+        try:
+            callback_id = self._menus_paths[menu_key]
+        except KeyError:
+            raise exceptions.NotFound(u"Can't find menu {path} ({menu_type})".format(
+                path=canonical_path, menu_type=menu_type))
+        return self.launchCallback(callback_id, data, client.profile)
+
+    def getMenuHelp(self, menu_id, language=''):
+        """return the help string of the menu
+
+        @param menu_id: id of the menu (same as callback_id)
+        @param language: language used for translation, or empty string for default
+        @param return: translated help
+
+        """
+        try:
+            menu_data = self._menus[menu_id]
+        except KeyError:
+            raise exceptions.DataError("Trying to access an unknown menu")
+        languageSwitch(language)
+        help_string = _(menu_data['help_string'])
+        languageSwitch()
+        return help_string