1151 lines
51 KiB
Python
1151 lines
51 KiB
Python
#!/usr/bin/env python2
|
||
# -*- coding: utf-8 -*-
|
||
|
||
# SAT: a jabber client
|
||
# Copyright (C) 2009-2016 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.words.xish import domish
|
||
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.memory import Memory
|
||
from sat.tools import trigger
|
||
from sat.tools import utils
|
||
from sat.tools.common import dynamic_import
|
||
from sat.stdui import ui_contact_list, ui_profile_manager
|
||
import sat.plugins
|
||
from glob import glob
|
||
from uuid import uuid4
|
||
import sys
|
||
import os.path
|
||
import uuid
|
||
import time
|
||
|
||
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.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("getProfileName", self.memory.getProfileName)
|
||
self.bridge.register_method("getProfilesList", 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("asyncCreateProfile", self.memory.asyncCreateProfile)
|
||
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("asyncConnect", self._asyncConnect)
|
||
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("saveParamsTemplate", self.memory.save_xml)
|
||
self.bridge.register_method("loadParamsTemplate", self.memory.load_xml)
|
||
self.bridge.register_method("sessionInfosGet", self.getSessionInfos)
|
||
|
||
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 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, 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"""
|
||
log.info(_("Memory initialised"))
|
||
self._import_plugins()
|
||
ui_contact_list.ContactList(self)
|
||
ui_profile_manager.ProfileManager(self)
|
||
self.initialised.callback(None)
|
||
log.info(_(u"Backend is ready"))
|
||
|
||
def _import_plugins(self):
|
||
"""Import all plugins found in plugins directory"""
|
||
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:
|
||
try:
|
||
del sys.modules[plugin_path]
|
||
except KeyError:
|
||
pass
|
||
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))
|
||
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()))
|
||
continue
|
||
mod = sys.modules[plugin_path]
|
||
plugin_info = mod.PLUGIN_INFO
|
||
import_name = plugin_info['import_name']
|
||
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 'handler' in plugin_info and plugin_info['handler'] == 'yes':
|
||
self.plugins[import_name].is_handler = True
|
||
else:
|
||
self.plugins[import_name].is_handler = False
|
||
#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 _asyncConnect(self, profile_key, password=''):
|
||
profile = self.memory.getProfileName(profile_key)
|
||
return self.asyncConnect(profile, password)
|
||
|
||
def asyncConnect(self, profile, password='', max_retries=C.XMPP_MAX_RETRIES):
|
||
"""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 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
|
||
"""
|
||
def connectXMPPClient(dummy=None):
|
||
if self.isConnected(profile):
|
||
log.info(_("already connected !"))
|
||
return True
|
||
d = self._connectXMPPClient(profile, max_retries)
|
||
return d.addCallback(lambda dummy: False)
|
||
|
||
d = self.memory.startSession(password, profile)
|
||
d.addCallback(connectXMPPClient)
|
||
return d
|
||
|
||
@defer.inlineCallbacks
|
||
def _connectXMPPClient(self, profile, max_retries):
|
||
"""This part is called from asyncConnect when we have loaded individual parameters from memory"""
|
||
try:
|
||
port = int(self.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 self.memory.asyncGetParamA("Password", "Connection", profile_key=profile)
|
||
current = self.profiles[profile] = xmpp.SatXMPPClient(self, profile,
|
||
jid.JID(self.memory.getParamA("JabberID", "Connection", profile_key=profile)),
|
||
password, self.memory.getParamA(C.FORCE_SERVER_PARAM, "Connection", profile_key=profile),
|
||
port, max_retries)
|
||
|
||
current.messageProt = xmpp.SatMessageProtocol(self)
|
||
current.messageProt.setHandlerParent(current)
|
||
|
||
current.roster = xmpp.SatRosterProtocol(self)
|
||
current.roster.setHandlerParent(current)
|
||
|
||
current.presence = xmpp.SatPresenceProtocol(self)
|
||
current.presence.setHandlerParent(current)
|
||
|
||
current.fallBack = xmpp.SatFallbackHandler(self)
|
||
current.fallBack.setHandlerParent(current)
|
||
|
||
current.versionHandler = xmpp.SatVersionHandler(C.APP_NAME_FULL,
|
||
self.full_version)
|
||
current.versionHandler.setHandlerParent(current)
|
||
|
||
current.identityHandler = xmpp.SatIdentityHandler()
|
||
current.identityHandler.setHandlerParent(current)
|
||
|
||
log.debug(_("setting plugins parents"))
|
||
|
||
plugin_conn_cb = []
|
||
for plugin in self.plugins.iteritems():
|
||
if plugin[1].is_handler:
|
||
plugin[1].getHandler(profile).setHandlerParent(current)
|
||
connected_cb = getattr(plugin[1], "profileConnected", None) # profile connected is called after client is ready and roster is got
|
||
if connected_cb:
|
||
plugin_conn_cb.append((plugin[0], connected_cb))
|
||
try:
|
||
yield plugin[1].profileConnecting(profile) # profile connecting is called before actually starting client
|
||
except AttributeError:
|
||
pass
|
||
|
||
current.startService()
|
||
|
||
yield current.getConnectionDeferred()
|
||
yield current.roster.got_roster # we want to be sure that we got the roster
|
||
|
||
# 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, profile))
|
||
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], '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 disconnect(self, profile_key):
|
||
"""disconnect from jabber server"""
|
||
if not self.isConnected(profile_key):
|
||
log.info(_("not connected !"))
|
||
return
|
||
profile = self.memory.getProfileName(profile_key)
|
||
log.info(_("Disconnecting..."))
|
||
self.profiles[profile].stopService()
|
||
for plugin in self.plugins.iteritems():
|
||
disconnected_cb = getattr(plugin[1], "profileDisconnected", None)
|
||
if disconnected_cb:
|
||
disconnected_cb(profile)
|
||
|
||
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 purgeClient(self, profile):
|
||
"""Remove reference to a profile client 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"))
|
||
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 use yet
|
||
"""
|
||
profile = self.memory.getProfileName(profile_key)
|
||
if not profile:
|
||
raise exceptions.ProfileKeyUnknown
|
||
try:
|
||
return self.profiles[profile]
|
||
except KeyError:
|
||
raise exceptions.NotFound
|
||
|
||
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
|
||
"""
|
||
try:
|
||
profile = self.memory.getProfileName(profile_key, True)
|
||
except exceptions.ProfileUnknownError:
|
||
return []
|
||
if profile == C.PROF_KEY_ALL:
|
||
return self.profiles.values()
|
||
elif profile.count('@') > 1:
|
||
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, ''))
|
||
|
||
## 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 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 _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 self.messageSend(client, to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()})
|
||
|
||
def messageSend(self, client, 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, messageSend trigger will no be used
|
||
useful when a message need to be sent without any modification
|
||
"""
|
||
profile = client.profile
|
||
if subject is None:
|
||
subject = {}
|
||
if extra is None:
|
||
extra = {}
|
||
data = { # dict is similar to the one used in client.onMessage
|
||
"from": client.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.memory.getEntityData(data["to"], ['type'], 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.trigger.point("messageSend", client, 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.messageSendToStream, client)
|
||
if send_only:
|
||
log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter"))
|
||
else:
|
||
post_xml_treatments.addCallback(self.messageAddToHistory, client)
|
||
post_xml_treatments.addCallback(self.messageSendToBridge, client)
|
||
post_xml_treatments.addErrback(self._cancelErrorTrap)
|
||
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 messageSendToStream(self, data, client):
|
||
"""Actualy send the message to the server
|
||
|
||
@param data: message data dictionnary
|
||
@param client: profile's client
|
||
"""
|
||
client.send(data['xml'])
|
||
return data
|
||
|
||
def messageAddToHistory(self, data, client):
|
||
"""Store message into database (for local history)
|
||
|
||
@param data: message data dictionnary
|
||
@param client: profile's client
|
||
"""
|
||
if data["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['message'] or data['subject']: # we need a message to store
|
||
self.memory.addToHistory(client, data)
|
||
else:
|
||
log.warning(u"No message found") # empty body should be managed by plugins before this point
|
||
return data
|
||
|
||
def messageSendToBridge(self, data, client):
|
||
"""Send message to bridge, so frontends can display it
|
||
|
||
@param data: message data dictionnary
|
||
@param client: profile's client
|
||
"""
|
||
if data["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['message'] or data['subject']: # we need a message to send something
|
||
# We send back the message, so all frontends are aware of it
|
||
self.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile)
|
||
else:
|
||
log.warning(_("No message found"))
|
||
return data
|
||
|
||
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 findServiceEntities(self, *args, **kwargs):
|
||
return self.memory.disco.findServiceEntities(*args, **kwargs)
|
||
|
||
def findFeaturesSet(self, *args, **kwargs):
|
||
return self.memory.disco.findFeaturesSet(*args, **kwargs)
|
||
|
||
|
||
## 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(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
|
||
|
||
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)
|