Mercurial > libervia-backend
comparison 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 |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT: a jabber client | |
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 import sat | |
21 from sat.core.i18n import _, languageSwitch | |
22 from twisted.application import service | |
23 from twisted.internet import defer | |
24 from twisted.words.protocols.jabber import jid | |
25 from twisted.internet import reactor | |
26 from wokkel.xmppim import RosterItem | |
27 from sat.core import xmpp | |
28 from sat.core import exceptions | |
29 from sat.core.log import getLogger | |
30 log = getLogger(__name__) | |
31 from sat.core.constants import Const as C | |
32 from sat.memory import memory | |
33 from sat.memory import cache | |
34 from sat.tools import trigger | |
35 from sat.tools import utils | |
36 from sat.tools.common import dynamic_import | |
37 from sat.tools.common import regex | |
38 from sat.stdui import ui_contact_list, ui_profile_manager | |
39 import sat.plugins | |
40 from glob import glob | |
41 import sys | |
42 import os.path | |
43 import uuid | |
44 | |
45 try: | |
46 from collections import OrderedDict # only available from python 2.7 | |
47 except ImportError: | |
48 from ordereddict import OrderedDict | |
49 | |
50 | |
51 class SAT(service.Service): | |
52 | |
53 def __init__(self): | |
54 self._cb_map = {} # map from callback_id to callbacks | |
55 self._menus = OrderedDict() # dynamic menus. key: callback_id, value: menu data (dictionnary) | |
56 self._menus_paths = {} # path to id. key: (menu_type, lower case tuple of path), value: menu id | |
57 self.initialised = defer.Deferred() | |
58 self.profiles = {} | |
59 self.plugins = {} | |
60 self.ns_map = {u'x-data': u'jabber:x:data'} # map for short name to whole namespace, | |
61 # extended by plugins with registerNamespace | |
62 self.memory = memory.Memory(self) | |
63 self.trigger = trigger.TriggerManager() # trigger are used to change SàT behaviour | |
64 | |
65 bridge_name = self.memory.getConfig('', 'bridge', 'dbus') | |
66 | |
67 bridge_module = dynamic_import.bridge(bridge_name) | |
68 if bridge_module is None: | |
69 log.error(u"Can't find bridge module of name {}".format(bridge_name)) | |
70 sys.exit(1) | |
71 log.info(u"using {} bridge".format(bridge_name)) | |
72 try: | |
73 self.bridge = bridge_module.Bridge() | |
74 except exceptions.BridgeInitError: | |
75 log.error(u"Bridge can't be initialised, can't start SàT core") | |
76 sys.exit(1) | |
77 self.bridge.register_method("getReady", lambda: self.initialised) | |
78 self.bridge.register_method("getVersion", lambda: self.full_version) | |
79 self.bridge.register_method("getFeatures", self.getFeatures) | |
80 self.bridge.register_method("profileNameGet", self.memory.getProfileName) | |
81 self.bridge.register_method("profilesListGet", self.memory.getProfilesList) | |
82 self.bridge.register_method("getEntityData", lambda jid_, keys, profile: self.memory.getEntityData(jid.JID(jid_), keys, profile)) | |
83 self.bridge.register_method("getEntitiesData", self.memory._getEntitiesData) | |
84 self.bridge.register_method("profileCreate", self.memory.createProfile) | |
85 self.bridge.register_method("asyncDeleteProfile", self.memory.asyncDeleteProfile) | |
86 self.bridge.register_method("profileStartSession", self.memory.startSession) | |
87 self.bridge.register_method("profileIsSessionStarted", self.memory._isSessionStarted) | |
88 self.bridge.register_method("profileSetDefault", self.memory.profileSetDefault) | |
89 self.bridge.register_method("connect", self._connect) | |
90 self.bridge.register_method("disconnect", self.disconnect) | |
91 self.bridge.register_method("getContacts", self.getContacts) | |
92 self.bridge.register_method("getContactsFromGroup", self.getContactsFromGroup) | |
93 self.bridge.register_method("getMainResource", self.memory._getMainResource) | |
94 self.bridge.register_method("getPresenceStatuses", self.memory._getPresenceStatuses) | |
95 self.bridge.register_method("getWaitingSub", self.memory.getWaitingSub) | |
96 self.bridge.register_method("messageSend", self._messageSend) | |
97 self.bridge.register_method("getConfig", self._getConfig) | |
98 self.bridge.register_method("setParam", self.setParam) | |
99 self.bridge.register_method("getParamA", self.memory.getStringParamA) | |
100 self.bridge.register_method("asyncGetParamA", self.memory.asyncGetStringParamA) | |
101 self.bridge.register_method("asyncGetParamsValuesFromCategory", self.memory.asyncGetParamsValuesFromCategory) | |
102 self.bridge.register_method("getParamsUI", self.memory.getParamsUI) | |
103 self.bridge.register_method("getParamsCategories", self.memory.getParamsCategories) | |
104 self.bridge.register_method("paramsRegisterApp", self.memory.paramsRegisterApp) | |
105 self.bridge.register_method("historyGet", self.memory._historyGet) | |
106 self.bridge.register_method("setPresence", self._setPresence) | |
107 self.bridge.register_method("subscription", self.subscription) | |
108 self.bridge.register_method("addContact", self._addContact) | |
109 self.bridge.register_method("updateContact", self._updateContact) | |
110 self.bridge.register_method("delContact", self._delContact) | |
111 self.bridge.register_method("isConnected", self.isConnected) | |
112 self.bridge.register_method("launchAction", self.launchCallback) | |
113 self.bridge.register_method("actionsGet", self.actionsGet) | |
114 self.bridge.register_method("progressGet", self._progressGet) | |
115 self.bridge.register_method("progressGetAll", self._progressGetAll) | |
116 self.bridge.register_method("menusGet", self.getMenus) | |
117 self.bridge.register_method("menuHelpGet", self.getMenuHelp) | |
118 self.bridge.register_method("menuLaunch", self._launchMenu) | |
119 self.bridge.register_method("discoInfos", self.memory.disco._discoInfos) | |
120 self.bridge.register_method("discoItems", self.memory.disco._discoItems) | |
121 self.bridge.register_method("discoFindByFeatures", self._findByFeatures) | |
122 self.bridge.register_method("saveParamsTemplate", self.memory.save_xml) | |
123 self.bridge.register_method("loadParamsTemplate", self.memory.load_xml) | |
124 self.bridge.register_method("sessionInfosGet", self.getSessionInfos) | |
125 self.bridge.register_method("namespacesGet", self.getNamespaces) | |
126 | |
127 self.memory.initialized.addCallback(self._postMemoryInit) | |
128 | |
129 @property | |
130 def version(self): | |
131 """Return the short version of SàT""" | |
132 return C.APP_VERSION | |
133 | |
134 @property | |
135 def full_version(self): | |
136 """Return the full version of SàT (with release name and extra data when in development mode)""" | |
137 version = self.version | |
138 if version[-1] == 'D': | |
139 # we are in debug version, we add extra data | |
140 try: | |
141 return self._version_cache | |
142 except AttributeError: | |
143 self._version_cache = u"{} « {} » ({})".format(version, C.APP_RELEASE_NAME, utils.getRepositoryData(sat)) | |
144 return self._version_cache | |
145 else: | |
146 return version | |
147 | |
148 @property | |
149 def bridge_name(self): | |
150 return os.path.splitext(os.path.basename(self.bridge.__file__))[0] | |
151 | |
152 def _postMemoryInit(self, ignore): | |
153 """Method called after memory initialization is done""" | |
154 self.common_cache = cache.Cache(self, None) | |
155 log.info(_("Memory initialised")) | |
156 try: | |
157 self._import_plugins() | |
158 ui_contact_list.ContactList(self) | |
159 ui_profile_manager.ProfileManager(self) | |
160 except Exception as e: | |
161 log.error(_(u"Could not initialize backend: {reason}").format( | |
162 reason = str(e).decode('utf-8', 'ignore'))) | |
163 sys.exit(1) | |
164 self.initialised.callback(None) | |
165 log.info(_(u"Backend is ready")) | |
166 | |
167 def _unimport_plugin(self, plugin_path): | |
168 """remove a plugin from sys.modules if it is there""" | |
169 try: | |
170 del sys.modules[plugin_path] | |
171 except KeyError: | |
172 pass | |
173 | |
174 def _import_plugins(self): | |
175 """Import all plugins found in plugins directory""" | |
176 # FIXME: module imported but cancelled should be deleted | |
177 # TODO: make this more generic and reusable in tools.common | |
178 # FIXME: should use imp | |
179 # TODO: do not import all plugins if no needed: component plugins are not needed if we | |
180 # just use a client, and plugin blacklisting should be possible in sat.conf | |
181 plugins_path = os.path.dirname(sat.plugins.__file__) | |
182 plugin_glob = "plugin*." + C.PLUGIN_EXT | |
183 plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename, glob(os.path.join(plugins_path, plugin_glob)))] | |
184 plugins_to_import = {} # plugins we still have to import | |
185 for plug in plug_lst: | |
186 plugin_path = 'sat.plugins.' + plug | |
187 try: | |
188 __import__(plugin_path) | |
189 except exceptions.MissingModule as e: | |
190 self._unimport_plugin(plugin_path) | |
191 log.warning(u"Can't import plugin [{path}] because of an unavailale third party module:\n{msg}".format( | |
192 path=plugin_path, msg=e)) | |
193 continue | |
194 except exceptions.CancelError as e: | |
195 log.info(u"Plugin [{path}] cancelled its own import: {msg}".format(path=plugin_path, msg=e)) | |
196 self._unimport_plugin(plugin_path) | |
197 continue | |
198 except Exception as e: | |
199 import traceback | |
200 log.error(_(u"Can't import plugin [{path}]:\n{error}").format(path=plugin_path, error=traceback.format_exc())) | |
201 self._unimport_plugin(plugin_path) | |
202 continue | |
203 mod = sys.modules[plugin_path] | |
204 plugin_info = mod.PLUGIN_INFO | |
205 import_name = plugin_info['import_name'] | |
206 | |
207 plugin_modes = plugin_info[u'modes'] = set(plugin_info.setdefault(u"modes", C.PLUG_MODE_DEFAULT)) | |
208 | |
209 # if the plugin is an entry point, it must work in component mode | |
210 if plugin_info[u'type'] == C.PLUG_TYPE_ENTRY_POINT: | |
211 # if plugin is an entrypoint, we cache it | |
212 if C.PLUG_MODE_COMPONENT not in plugin_modes: | |
213 log.error(_(u"{type} type must be used with {mode} mode, ignoring plugin").format( | |
214 type = C.PLUG_TYPE_ENTRY_POINT, mode = C.PLUG_MODE_COMPONENT)) | |
215 self._unimport_plugin(plugin_path) | |
216 continue | |
217 | |
218 if import_name in plugins_to_import: | |
219 log.error(_(u"Name conflict for import name [{import_name}], can't import plugin [{name}]").format(**plugin_info)) | |
220 continue | |
221 plugins_to_import[import_name] = (plugin_path, mod, plugin_info) | |
222 while True: | |
223 try: | |
224 self._import_plugins_from_dict(plugins_to_import) | |
225 except ImportError: | |
226 pass | |
227 if not plugins_to_import: | |
228 break | |
229 | |
230 def _import_plugins_from_dict(self, plugins_to_import, import_name=None, optional=False): | |
231 """Recursively import and their dependencies in the right order | |
232 | |
233 @param plugins_to_import(dict): key=import_name and values=(plugin_path, module, plugin_info) | |
234 @param import_name(unicode, None): name of the plugin to import as found in PLUGIN_INFO['import_name'] | |
235 @param optional(bool): if False and plugin is not found, an ImportError exception is raised | |
236 """ | |
237 if import_name in self.plugins: | |
238 log.debug(u'Plugin {} already imported, passing'.format(import_name)) | |
239 return | |
240 if not import_name: | |
241 import_name, (plugin_path, mod, plugin_info) = plugins_to_import.popitem() | |
242 else: | |
243 if not import_name in plugins_to_import: | |
244 if optional: | |
245 log.warning(_(u"Recommended plugin not found: {}").format(import_name)) | |
246 return | |
247 msg = u"Dependency not found: {}".format(import_name) | |
248 log.error(msg) | |
249 raise ImportError(msg) | |
250 plugin_path, mod, plugin_info = plugins_to_import.pop(import_name) | |
251 dependencies = plugin_info.setdefault("dependencies", []) | |
252 recommendations = plugin_info.setdefault("recommendations", []) | |
253 for to_import in dependencies + recommendations: | |
254 if to_import not in self.plugins: | |
255 log.debug(u'Recursively import dependency of [%s]: [%s]' % (import_name, to_import)) | |
256 try: | |
257 self._import_plugins_from_dict(plugins_to_import, to_import, to_import not in dependencies) | |
258 except ImportError as e: | |
259 log.warning(_(u"Can't import plugin {name}: {error}").format(name=plugin_info['name'], error=e)) | |
260 if optional: | |
261 return | |
262 raise e | |
263 log.info("importing plugin: {}".format(plugin_info['name'])) | |
264 # we instanciate the plugin here | |
265 try: | |
266 self.plugins[import_name] = getattr(mod, plugin_info['main'])(self) | |
267 except Exception as e: | |
268 log.warning(u'Error while loading plugin "{name}", ignoring it: {error}' | |
269 .format(name=plugin_info['name'], error=e)) | |
270 if optional: | |
271 return | |
272 raise ImportError(u"Error during initiation") | |
273 if C.bool(plugin_info.get(C.PI_HANDLER, C.BOOL_FALSE)): | |
274 self.plugins[import_name].is_handler = True | |
275 else: | |
276 self.plugins[import_name].is_handler = False | |
277 # we keep metadata as a Class attribute | |
278 self.plugins[import_name]._info = plugin_info | |
279 #TODO: test xmppclient presence and register handler parent | |
280 | |
281 def pluginsUnload(self): | |
282 """Call unload method on every loaded plugin, if exists | |
283 | |
284 @return (D): A deferred which return None when all method have been called | |
285 """ | |
286 # TODO: in the futur, it should be possible to hot unload a plugin | |
287 # pluging depending on the unloaded one should be unloaded too | |
288 # for now, just a basic call on plugin.unload is done | |
289 defers_list = [] | |
290 for plugin in self.plugins.itervalues(): | |
291 try: | |
292 unload = plugin.unload | |
293 except AttributeError: | |
294 continue | |
295 else: | |
296 defers_list.append(defer.maybeDeferred(unload)) | |
297 return defers_list | |
298 | |
299 def _connect(self, profile_key, password='', options=None): | |
300 profile = self.memory.getProfileName(profile_key) | |
301 return self.connect(profile, password, options) | |
302 | |
303 def connect(self, profile, password='', options=None, max_retries=C.XMPP_MAX_RETRIES): | |
304 """Connect a profile (i.e. connect client.component to XMPP server) | |
305 | |
306 Retrieve the individual parameters, authenticate the profile | |
307 and initiate the connection to the associated XMPP server. | |
308 @param profile: %(doc_profile)s | |
309 @param password (string): the SàT profile password | |
310 @param options (dict): connection options. Key can be: | |
311 - | |
312 @param max_retries (int): max number of connection retries | |
313 @return (D(bool)): | |
314 - True if the XMPP connection was already established | |
315 - False if the XMPP connection has been initiated (it may still fail) | |
316 @raise exceptions.PasswordError: Profile password is wrong | |
317 """ | |
318 if options is None: | |
319 options={} | |
320 def connectProfile(dummy=None): | |
321 if self.isConnected(profile): | |
322 log.info(_("already connected !")) | |
323 return True | |
324 | |
325 if self.memory.isComponent(profile): | |
326 d = xmpp.SatXMPPComponent.startConnection(self, profile, max_retries) | |
327 else: | |
328 d = xmpp.SatXMPPClient.startConnection(self, profile, max_retries) | |
329 return d.addCallback(lambda dummy: False) | |
330 | |
331 d = self.memory.startSession(password, profile) | |
332 d.addCallback(connectProfile) | |
333 return d | |
334 | |
335 def disconnect(self, profile_key): | |
336 """disconnect from jabber server""" | |
337 # FIXME: client should not be deleted if only disconnected | |
338 # it shoud be deleted only when session is finished | |
339 if not self.isConnected(profile_key): | |
340 # isConnected is checked here and not on client | |
341 # because client is deleted when session is ended | |
342 log.info(_(u"not connected !")) | |
343 return defer.succeed(None) | |
344 client = self.getClient(profile_key) | |
345 return client.entityDisconnect() | |
346 | |
347 def getFeatures(self, profile_key=C.PROF_KEY_NONE): | |
348 """Get available features | |
349 | |
350 Return list of activated plugins and plugin specific data | |
351 @param profile_key: %(doc_profile_key)s | |
352 C.PROF_KEY_NONE can be used to have general plugins data (i.e. not profile dependent) | |
353 @return (dict)[Deferred]: features data where: | |
354 - key is plugin import name, present only for activated plugins | |
355 - value is a an other dict, when meaning is specific to each plugin. | |
356 this dict is return by plugin's getFeature method. | |
357 If this method doesn't exists, an empty dict is returned. | |
358 """ | |
359 try: | |
360 # FIXME: there is no method yet to check profile session | |
361 # as soon as one is implemented, it should be used here | |
362 self.getClient(profile_key) | |
363 except KeyError: | |
364 log.warning("Requesting features for a profile outside a session") | |
365 profile_key = C.PROF_KEY_NONE | |
366 except exceptions.ProfileNotSetError: | |
367 pass | |
368 | |
369 features = [] | |
370 for import_name, plugin in self.plugins.iteritems(): | |
371 try: | |
372 features_d = defer.maybeDeferred(plugin.getFeatures, profile_key) | |
373 except AttributeError: | |
374 features_d = defer.succeed({}) | |
375 features.append(features_d) | |
376 | |
377 d_list = defer.DeferredList(features) | |
378 def buildFeatures(result, import_names): | |
379 assert len(result) == len(import_names) | |
380 ret = {} | |
381 for name, (success, data) in zip (import_names, result): | |
382 if success: | |
383 ret[name] = data | |
384 else: | |
385 log.warning(u"Error while getting features for {name}: {failure}".format( | |
386 name=name, failure=data)) | |
387 ret[name] = {} | |
388 return ret | |
389 | |
390 d_list.addCallback(buildFeatures, self.plugins.keys()) | |
391 return d_list | |
392 | |
393 def getContacts(self, profile_key): | |
394 client = self.getClient(profile_key) | |
395 def got_roster(dummy): | |
396 ret = [] | |
397 for item in client.roster.getItems(): # we get all items for client's roster | |
398 # and convert them to expected format | |
399 attr = client.roster.getAttributes(item) | |
400 ret.append([item.jid.userhost(), attr, item.groups]) | |
401 return ret | |
402 | |
403 return client.roster.got_roster.addCallback(got_roster) | |
404 | |
405 def getContactsFromGroup(self, group, profile_key): | |
406 client = self.getClient(profile_key) | |
407 return [jid_.full() for jid_ in client.roster.getJidsFromGroup(group)] | |
408 | |
409 def purgeEntity(self, profile): | |
410 """Remove reference to a profile client/component and purge cache | |
411 | |
412 the garbage collector can then free the memory | |
413 """ | |
414 try: | |
415 del self.profiles[profile] | |
416 except KeyError: | |
417 log.error(_("Trying to remove reference to a client not referenced")) | |
418 else: | |
419 self.memory.purgeProfileSession(profile) | |
420 | |
421 def startService(self): | |
422 log.info(u"Salut à toi ô mon frère !") | |
423 | |
424 def stopService(self): | |
425 log.info(u"Salut aussi à Rantanplan") | |
426 return self.pluginsUnload() | |
427 | |
428 def run(self): | |
429 log.debug(_("running app")) | |
430 reactor.run() | |
431 | |
432 def stop(self): | |
433 log.debug(_("stopping app")) | |
434 reactor.stop() | |
435 | |
436 ## Misc methods ## | |
437 | |
438 def getJidNStream(self, profile_key): | |
439 """Convenient method to get jid and stream from profile key | |
440 @return: tuple (jid, xmlstream) from profile, can be None""" | |
441 # TODO: deprecate this method (getClient is enough) | |
442 profile = self.memory.getProfileName(profile_key) | |
443 if not profile or not self.profiles[profile].isConnected(): | |
444 return (None, None) | |
445 return (self.profiles[profile].jid, self.profiles[profile].xmlstream) | |
446 | |
447 def getClient(self, profile_key): | |
448 """Convenient method to get client from profile key | |
449 | |
450 @return: client or None if it doesn't exist | |
451 @raise exceptions.ProfileKeyUnknown: the profile or profile key doesn't exist | |
452 @raise exceptions.NotFound: client is not available | |
453 This happen if profile has not been used yet | |
454 """ | |
455 profile = self.memory.getProfileName(profile_key) | |
456 if not profile: | |
457 raise exceptions.ProfileKeyUnknown | |
458 try: | |
459 return self.profiles[profile] | |
460 except KeyError: | |
461 raise exceptions.NotFound(profile_key) | |
462 | |
463 def getClients(self, profile_key): | |
464 """Convenient method to get list of clients from profile key (manage list through profile_key like C.PROF_KEY_ALL) | |
465 | |
466 @param profile_key: %(doc_profile_key)s | |
467 @return: list of clients | |
468 """ | |
469 if not profile_key: | |
470 raise exceptions.DataError(_(u'profile_key must not be empty')) | |
471 try: | |
472 profile = self.memory.getProfileName(profile_key, True) | |
473 except exceptions.ProfileUnknownError: | |
474 return [] | |
475 if profile == C.PROF_KEY_ALL: | |
476 return self.profiles.values() | |
477 elif profile[0] == '@': # only profile keys can start with "@" | |
478 raise exceptions.ProfileKeyUnknown | |
479 return [self.profiles[profile]] | |
480 | |
481 def _getConfig(self, section, name): | |
482 """Get the main configuration option | |
483 | |
484 @param section: section of the config file (None or '' for DEFAULT) | |
485 @param name: name of the option | |
486 @return: unicode representation of the option | |
487 """ | |
488 return unicode(self.memory.getConfig(section, name, '')) | |
489 | |
490 def logErrback(self, failure_): | |
491 """generic errback logging | |
492 | |
493 can be used as last errback to show unexpected error | |
494 """ | |
495 log.error(_(u"Unexpected error: {}".format(failure_))) | |
496 return failure_ | |
497 | |
498 # namespaces | |
499 | |
500 def registerNamespace(self, short_name, namespace): | |
501 """associate a namespace to a short name""" | |
502 if short_name in self.ns_map: | |
503 raise exceptions.ConflictError(u'this short name is already used') | |
504 self.ns_map[short_name] = namespace | |
505 | |
506 def getNamespaces(self): | |
507 return self.ns_map | |
508 | |
509 def getSessionInfos(self, profile_key): | |
510 """compile interesting data on current profile session""" | |
511 client = self.getClient(profile_key) | |
512 data = { | |
513 "jid": client.jid.full(), | |
514 "started": unicode(int(client.started)), | |
515 } | |
516 return defer.succeed(data) | |
517 | |
518 # local dirs | |
519 | |
520 def getLocalPath(self, client, dir_name, *extra_path, **kwargs): | |
521 """retrieve path for local data | |
522 | |
523 if path doesn't exist, it will be created | |
524 @param client(SatXMPPClient, None): client instance | |
525 used when profile is set, can be None if profile is False | |
526 @param dir_name(unicode): name of the main path directory | |
527 @param component(bool): if True, path will be prefixed with C.COMPONENTS_DIR | |
528 @param profile(bool): if True, path will be suffixed by profile name | |
529 @param *extra_path: extra path element(s) to use | |
530 @return (unicode): path | |
531 """ | |
532 # FIXME: component and profile are parsed with **kwargs because of python 2 limitations | |
533 # once moved to python 3, this can be fixed | |
534 component = kwargs.pop('component', False) | |
535 profile = kwargs.pop('profile', True) | |
536 assert not kwargs | |
537 | |
538 path_elts = [self.memory.getConfig('', 'local_dir')] | |
539 if component: | |
540 path_elts.append(C.COMPONENTS_DIR) | |
541 path_elts.append(regex.pathEscape(dir_name)) | |
542 if extra_path: | |
543 path_elts.extend([regex.pathEscape(p) for p in extra_path]) | |
544 if profile: | |
545 regex.pathEscape(client.profile) | |
546 path = os.path.join(*path_elts) | |
547 if not os.path.exists(path): | |
548 os.makedirs(path) | |
549 return path | |
550 | |
551 ## Client management ## | |
552 | |
553 def setParam(self, name, value, category, security_limit, profile_key): | |
554 """set wanted paramater and notice observers""" | |
555 self.memory.setParam(name, value, category, security_limit, profile_key) | |
556 | |
557 def isConnected(self, profile_key): | |
558 """Return connection status of profile | |
559 @param profile_key: key_word or profile name to determine profile name | |
560 @return: True if connected | |
561 """ | |
562 profile = self.memory.getProfileName(profile_key) | |
563 if not profile: | |
564 log.error(_('asking connection status for a non-existant profile')) | |
565 raise exceptions.ProfileUnknownError(profile_key) | |
566 if profile not in self.profiles: | |
567 return False | |
568 return self.profiles[profile].isConnected() | |
569 | |
570 ## XMPP methods ## | |
571 | |
572 def _messageSend(self, to_jid_s, message, subject=None, mess_type='auto', extra=None, profile_key=C.PROF_KEY_NONE): | |
573 client = self.getClient(profile_key) | |
574 to_jid = jid.JID(to_jid_s) | |
575 #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 | |
576 return client.sendMessage(to_jid, message, subject, mess_type, {unicode(key): unicode(value) for key, value in extra.items()}) | |
577 | |
578 def _setPresence(self, to="", show="", statuses=None, profile_key=C.PROF_KEY_NONE): | |
579 return self.setPresence(jid.JID(to) if to else None, show, statuses, profile_key) | |
580 | |
581 def setPresence(self, to_jid=None, show="", statuses=None, profile_key=C.PROF_KEY_NONE): | |
582 """Send our presence information""" | |
583 if statuses is None: | |
584 statuses = {} | |
585 profile = self.memory.getProfileName(profile_key) | |
586 assert profile | |
587 priority = int(self.memory.getParamA("Priority", "Connection", profile_key=profile)) | |
588 self.profiles[profile].presence.available(to_jid, show, statuses, priority) | |
589 #XXX: FIXME: temporary fix to work around openfire 3.7.0 bug (presence is not broadcasted to generating resource) | |
590 if '' in statuses: | |
591 statuses[C.PRESENCE_STATUSES_DEFAULT] = statuses.pop('') | |
592 self.bridge.presenceUpdate(self.profiles[profile].jid.full(), show, | |
593 int(priority), statuses, profile) | |
594 | |
595 def subscription(self, subs_type, raw_jid, profile_key): | |
596 """Called to manage subscription | |
597 @param subs_type: subsciption type (cf RFC 3921) | |
598 @param raw_jid: unicode entity's jid | |
599 @param profile_key: profile""" | |
600 profile = self.memory.getProfileName(profile_key) | |
601 assert profile | |
602 to_jid = jid.JID(raw_jid) | |
603 log.debug(_(u'subsciption request [%(subs_type)s] for %(jid)s') % {'subs_type': subs_type, 'jid': to_jid.full()}) | |
604 if subs_type == "subscribe": | |
605 self.profiles[profile].presence.subscribe(to_jid) | |
606 elif subs_type == "subscribed": | |
607 self.profiles[profile].presence.subscribed(to_jid) | |
608 elif subs_type == "unsubscribe": | |
609 self.profiles[profile].presence.unsubscribe(to_jid) | |
610 elif subs_type == "unsubscribed": | |
611 self.profiles[profile].presence.unsubscribed(to_jid) | |
612 | |
613 def _addContact(self, to_jid_s, profile_key): | |
614 return self.addContact(jid.JID(to_jid_s), profile_key) | |
615 | |
616 def addContact(self, to_jid, profile_key): | |
617 """Add a contact in roster list""" | |
618 profile = self.memory.getProfileName(profile_key) | |
619 assert profile | |
620 # presence is sufficient, as a roster push will be sent according to RFC 6121 §3.1.2 | |
621 self.profiles[profile].presence.subscribe(to_jid) | |
622 | |
623 def _updateContact(self, to_jid_s, name, groups, profile_key): | |
624 return self.updateContact(jid.JID(to_jid_s), name, groups, profile_key) | |
625 | |
626 def updateContact(self, to_jid, name, groups, profile_key): | |
627 """update a contact in roster list""" | |
628 profile = self.memory.getProfileName(profile_key) | |
629 assert profile | |
630 groups = set(groups) | |
631 roster_item = RosterItem(to_jid) | |
632 roster_item.name = name or None | |
633 roster_item.groups = set(groups) | |
634 return self.profiles[profile].roster.setItem(roster_item) | |
635 | |
636 def _delContact(self, to_jid_s, profile_key): | |
637 return self.delContact(jid.JID(to_jid_s), profile_key) | |
638 | |
639 def delContact(self, to_jid, profile_key): | |
640 """Remove contact from roster list""" | |
641 profile = self.memory.getProfileName(profile_key) | |
642 assert profile | |
643 self.profiles[profile].presence.unsubscribe(to_jid) # is not asynchronous | |
644 return self.profiles[profile].roster.removeItem(to_jid) | |
645 | |
646 ## Discovery ## | |
647 # discovery methods are shortcuts to self.memory.disco | |
648 # the main difference with client.disco is that self.memory.disco manage cache | |
649 | |
650 def hasFeature(self, *args, **kwargs): | |
651 return self.memory.disco.hasFeature(*args, **kwargs) | |
652 | |
653 def checkFeature(self, *args, **kwargs): | |
654 return self.memory.disco.checkFeature(*args, **kwargs) | |
655 | |
656 def checkFeatures(self, *args, **kwargs): | |
657 return self.memory.disco.checkFeatures(*args, **kwargs) | |
658 | |
659 def getDiscoInfos(self, *args, **kwargs): | |
660 return self.memory.disco.getInfos(*args, **kwargs) | |
661 | |
662 def getDiscoItems(self, *args, **kwargs): | |
663 return self.memory.disco.getItems(*args, **kwargs) | |
664 | |
665 def findServiceEntity(self, *args, **kwargs): | |
666 return self.memory.disco.findServiceEntity(*args, **kwargs) | |
667 | |
668 def findServiceEntities(self, *args, **kwargs): | |
669 return self.memory.disco.findServiceEntities(*args, **kwargs) | |
670 | |
671 def findFeaturesSet(self, *args, **kwargs): | |
672 return self.memory.disco.findFeaturesSet(*args, **kwargs) | |
673 | |
674 def _findByFeatures(self, namespaces, identities, bare_jids, service, roster, own_jid, profile_key): | |
675 client = self.getClient(profile_key) | |
676 return self.findByFeatures(client, namespaces, identities, bare_jids, service, roster, own_jid) | |
677 | |
678 @defer.inlineCallbacks | |
679 def findByFeatures(self, client, namespaces, identities=None, bare_jids=False, service=True, roster=True, own_jid=True): | |
680 """retrieve all services or contacts managing a set a features | |
681 | |
682 @param namespaces(list[unicode]): features which must be handled | |
683 @param identities(list[tuple[unicode,unicode]], None): if not None or empty, only keep those identities | |
684 tuple must by (category, type) | |
685 @param bare_jids(bool): retrieve only bare_jids if True | |
686 if False, retrieve full jid of connected devices | |
687 @param service(bool): if True return service from our roster | |
688 @param roster(bool): if True, return entities in roster | |
689 full jid of all matching resources available will be returned | |
690 @param own_jid(bool): if True, return profile's jid resources | |
691 @return (tuple(dict[jid.JID(), tuple[unicode, unicode, unicode]]*3)): found entities in a tuple with: | |
692 - service entities | |
693 - own entities | |
694 - roster entities | |
695 """ | |
696 if not identities: | |
697 identities = None | |
698 if not namespaces and not identities: | |
699 raise exceptions.DataError("at least one namespace or one identity must be set") | |
700 found_service = {} | |
701 found_own = {} | |
702 found_roster = {} | |
703 if service: | |
704 services_jids = yield self.findFeaturesSet(client, namespaces) | |
705 for service_jid in services_jids: | |
706 infos = yield self.getDiscoInfos(client, service_jid) | |
707 if identities is not None and not set(infos.identities.keys()).issuperset(identities): | |
708 continue | |
709 found_identities = [(cat, type_, name or u'') for (cat, type_), name in infos.identities.iteritems()] | |
710 found_service[service_jid.full()] = found_identities | |
711 | |
712 jids = [] | |
713 if roster: | |
714 jids.extend(client.roster.getJids()) | |
715 if own_jid: | |
716 jids.append(client.jid.userhostJID()) | |
717 | |
718 for found, jids in ((found_own, [client.jid.userhostJID()]), | |
719 (found_roster, client.roster.getJids())): | |
720 for jid_ in jids: | |
721 if jid_.resource: | |
722 if bare_jids: | |
723 continue | |
724 resources = [jid_.resource] | |
725 else: | |
726 if bare_jids: | |
727 resources = [None] | |
728 else: | |
729 try: | |
730 resources = self.memory.getAllResources(client, jid_) | |
731 except exceptions.UnknownEntityError: | |
732 continue | |
733 for resource in resources: | |
734 full_jid = jid.JID(tuple=(jid_.user, jid_.host, resource)) | |
735 infos = yield self.getDiscoInfos(client, full_jid) | |
736 if infos.features.issuperset(namespaces): | |
737 if identities is not None and not set(infos.identities.keys()).issuperset(identities): | |
738 continue | |
739 found_identities = [(cat, type_, name or u'') for (cat, type_), name in infos.identities.iteritems()] | |
740 found[full_jid.full()] = found_identities | |
741 | |
742 defer.returnValue((found_service, found_own, found_roster)) | |
743 | |
744 ## Generic HMI ## | |
745 | |
746 def _killAction(self, keep_id, client): | |
747 log.debug(u"Killing action {} for timeout".format(keep_id)) | |
748 client.actions[keep_id] | |
749 | |
750 def actionNew(self, action_data, security_limit=C.NO_SECURITY_LIMIT, keep_id=None, profile=C.PROF_KEY_NONE): | |
751 """Shortcut to bridge.actionNew which generate and id and keep for retrieval | |
752 | |
753 @param action_data(dict): action data (see bridge documentation) | |
754 @param security_limit: %(doc_security_limit)s | |
755 @param keep_id(None, unicode): if not None, used to keep action for differed retrieval | |
756 must be set to the callback_id | |
757 action will be deleted after 30 min. | |
758 @param profile: %(doc_profile)s | |
759 """ | |
760 id_ = unicode(uuid.uuid4()) | |
761 if keep_id is not None: | |
762 client = self.getClient(profile) | |
763 action_timer = reactor.callLater(60*30, self._killAction, keep_id, client) | |
764 client.actions[keep_id] = (action_data, id_, security_limit, action_timer) | |
765 | |
766 self.bridge.actionNew(action_data, id_, security_limit, profile) | |
767 | |
768 def actionsGet(self, profile): | |
769 """Return current non answered actions | |
770 | |
771 @param profile: %(doc_profile)s | |
772 """ | |
773 client = self.getClient(profile) | |
774 return [action_tuple[:-1] for action_tuple in client.actions.itervalues()] | |
775 | |
776 def registerProgressCb(self, progress_id, callback, metadata=None, profile=C.PROF_KEY_NONE): | |
777 """Register a callback called when progress is requested for id""" | |
778 if metadata is None: | |
779 metadata = {} | |
780 client = self.getClient(profile) | |
781 if progress_id in client._progress_cb: | |
782 raise exceptions.ConflictError(u"Progress ID is not unique !") | |
783 client._progress_cb[progress_id] = (callback, metadata) | |
784 | |
785 def removeProgressCb(self, progress_id, profile): | |
786 """Remove a progress callback""" | |
787 client = self.getClient(profile) | |
788 try: | |
789 del client._progress_cb[progress_id] | |
790 except KeyError: | |
791 log.error(_(u"Trying to remove an unknow progress callback")) | |
792 | |
793 def _progressGet(self, progress_id, profile): | |
794 data = self.progressGet(progress_id, profile) | |
795 return {k: unicode(v) for k,v in data.iteritems()} | |
796 | |
797 def progressGet(self, progress_id, profile): | |
798 """Return a dict with progress information | |
799 | |
800 @param progress_id(unicode): unique id of the progressing element | |
801 @param profile: %(doc_profile)s | |
802 @return (dict): data with the following keys: | |
803 'position' (int): current possition | |
804 'size' (int): end_position | |
805 if id doesn't exists (may be a finished progression), and empty dict is returned | |
806 """ | |
807 client = self.getClient(profile) | |
808 try: | |
809 data = client._progress_cb[progress_id][0](progress_id, profile) | |
810 except KeyError: | |
811 data = {} | |
812 return data | |
813 | |
814 def _progressGetAll(self, profile_key): | |
815 progress_all = self.progressGetAll(profile_key) | |
816 for profile, progress_dict in progress_all.iteritems(): | |
817 for progress_id, data in progress_dict.iteritems(): | |
818 for key, value in data.iteritems(): | |
819 data[key] = unicode(value) | |
820 return progress_all | |
821 | |
822 def progressGetAllMetadata(self, profile_key): | |
823 """Return all progress metadata at once | |
824 | |
825 @param profile_key: %(doc_profile)s | |
826 if C.PROF_KEY_ALL is used, all progress metadata from all profiles are returned | |
827 @return (dict[dict[dict]]): a dict which map profile to progress_dict | |
828 progress_dict map progress_id to progress_data | |
829 progress_metadata is the same dict as sent by [progressStarted] | |
830 """ | |
831 clients = self.getClients(profile_key) | |
832 progress_all = {} | |
833 for client in clients: | |
834 profile = client.profile | |
835 progress_dict = {} | |
836 progress_all[profile] = progress_dict | |
837 for progress_id, (dummy, progress_metadata) in client._progress_cb.iteritems(): | |
838 progress_dict[progress_id] = progress_metadata | |
839 return progress_all | |
840 | |
841 def progressGetAll(self, profile_key): | |
842 """Return all progress status at once | |
843 | |
844 @param profile_key: %(doc_profile)s | |
845 if C.PROF_KEY_ALL is used, all progress status from all profiles are returned | |
846 @return (dict[dict[dict]]): a dict which map profile to progress_dict | |
847 progress_dict map progress_id to progress_data | |
848 progress_data is the same dict as returned by [progressGet] | |
849 """ | |
850 clients = self.getClients(profile_key) | |
851 progress_all = {} | |
852 for client in clients: | |
853 profile = client.profile | |
854 progress_dict = {} | |
855 progress_all[profile] = progress_dict | |
856 for progress_id, (progress_cb, dummy) in client._progress_cb.iteritems(): | |
857 progress_dict[progress_id] = progress_cb(progress_id, profile) | |
858 return progress_all | |
859 | |
860 def registerCallback(self, callback, *args, **kwargs): | |
861 """Register a callback. | |
862 | |
863 @param callback(callable): method to call | |
864 @param kwargs: can contain: | |
865 with_data(bool): True if the callback use the optional data dict | |
866 force_id(unicode): id to avoid generated id. Can lead to name conflict, avoid if possible | |
867 one_shot(bool): True to delete callback once it have been called | |
868 @return: id of the registered callback | |
869 """ | |
870 callback_id = kwargs.pop('force_id', None) | |
871 if callback_id is None: | |
872 callback_id = str(uuid.uuid4()) | |
873 else: | |
874 if callback_id in self._cb_map: | |
875 raise exceptions.ConflictError(_(u"id already registered")) | |
876 self._cb_map[callback_id] = (callback, args, kwargs) | |
877 | |
878 if "one_shot" in kwargs: # One Shot callback are removed after 30 min | |
879 def purgeCallback(): | |
880 try: | |
881 self.removeCallback(callback_id) | |
882 except KeyError: | |
883 pass | |
884 reactor.callLater(1800, purgeCallback) | |
885 | |
886 return callback_id | |
887 | |
888 def removeCallback(self, callback_id): | |
889 """ Remove a previously registered callback | |
890 @param callback_id: id returned by [registerCallback] """ | |
891 log.debug("Removing callback [%s]" % callback_id) | |
892 del self._cb_map[callback_id] | |
893 | |
894 def launchCallback(self, callback_id, data=None, profile_key=C.PROF_KEY_NONE): | |
895 """Launch a specific callback | |
896 | |
897 @param callback_id: id of the action (callback) to launch | |
898 @param data: optional data | |
899 @profile_key: %(doc_profile_key)s | |
900 @return: a deferred which fire a dict where key can be: | |
901 - xmlui: a XMLUI need to be displayed | |
902 - validated: if present, can be used to launch a callback, it can have the values | |
903 - C.BOOL_TRUE | |
904 - C.BOOL_FALSE | |
905 """ | |
906 # FIXME: security limit need to be checked here | |
907 try: | |
908 client = self.getClient(profile_key) | |
909 except exceptions.NotFound: | |
910 # client is not available yet | |
911 profile = self.memory.getProfileName(profile_key) | |
912 if not profile: | |
913 raise exceptions.ProfileUnknownError(_(u'trying to launch action with a non-existant profile')) | |
914 else: | |
915 profile = client.profile | |
916 # we check if the action is kept, and remove it | |
917 try: | |
918 action_tuple = client.actions[callback_id] | |
919 except KeyError: | |
920 pass | |
921 else: | |
922 action_tuple[-1].cancel() # the last item is the action timer | |
923 del client.actions[callback_id] | |
924 | |
925 try: | |
926 callback, args, kwargs = self._cb_map[callback_id] | |
927 except KeyError: | |
928 raise exceptions.DataError(u"Unknown callback id {}".format(callback_id)) | |
929 | |
930 if kwargs.get("with_data", False): | |
931 if data is None: | |
932 raise exceptions.DataError("Required data for this callback is missing") | |
933 args,kwargs=list(args)[:],kwargs.copy() # we don't want to modify the original (kw)args | |
934 args.insert(0, data) | |
935 kwargs["profile"] = profile | |
936 del kwargs["with_data"] | |
937 | |
938 if kwargs.pop('one_shot', False): | |
939 self.removeCallback(callback_id) | |
940 | |
941 return defer.maybeDeferred(callback, *args, **kwargs) | |
942 | |
943 #Menus management | |
944 | |
945 def _getMenuCanonicalPath(self, path): | |
946 """give canonical form of path | |
947 | |
948 canonical form is a tuple of the path were every element is stripped and lowercase | |
949 @param path(iterable[unicode]): untranslated path to menu | |
950 @return (tuple[unicode]): canonical form of path | |
951 """ | |
952 return tuple((p.lower().strip() for p in path)) | |
953 | |
954 def importMenu(self, path, callback, security_limit=C.NO_SECURITY_LIMIT, help_string="", type_=C.MENU_GLOBAL): | |
955 """register a new menu for frontends | |
956 | |
957 @param path(iterable[unicode]): path to go to the menu (category/subcategory/.../item) (e.g.: ("File", "Open")) | |
958 /!\ use D_() instead of _() for translations (e.g. (D_("File"), D_("Open"))) | |
959 untranslated/lower case path can be used to identity a menu, for this reason it must be unique independently of case. | |
960 @param callback(callable): method to be called when menuitem is selected, callable or a callback id (string) as returned by [registerCallback] | |
961 @param security_limit(int): %(doc_security_limit)s | |
962 /!\ security_limit MUST be added to data in launchCallback if used #TODO | |
963 @param help_string(unicode): string used to indicate what the menu do (can be show as a tooltip). | |
964 /!\ use D_() instead of _() for translations | |
965 @param type(unicode): one of: | |
966 - C.MENU_GLOBAL: classical menu, can be shown in a menubar on top (e.g. something like File/Open) | |
967 - C.MENU_ROOM: like a global menu, but only shown in multi-user chat | |
968 menu_data must contain a "room_jid" data | |
969 - C.MENU_SINGLE: like a global menu, but only shown in one2one chat | |
970 menu_data must contain a "jid" data | |
971 - C.MENU_JID_CONTEXT: contextual menu, used with any jid (e.g.: ad hoc commands, jid is already filled) | |
972 menu_data must contain a "jid" data | |
973 - C.MENU_ROSTER_JID_CONTEXT: like JID_CONTEXT, but restricted to jids in roster. | |
974 menu_data must contain a "room_jid" data | |
975 - C.MENU_ROSTER_GROUP_CONTEXT: contextual menu, used with group (e.g.: publish microblog, group is already filled) | |
976 menu_data must contain a "group" data | |
977 @return (unicode): menu_id (same as callback_id) | |
978 """ | |
979 | |
980 if callable(callback): | |
981 callback_id = self.registerCallback(callback, with_data=True) | |
982 elif isinstance(callback, basestring): | |
983 # The callback is already registered | |
984 callback_id = callback | |
985 try: | |
986 callback, args, kwargs = self._cb_map[callback_id] | |
987 except KeyError: | |
988 raise exceptions.DataError("Unknown callback id") | |
989 kwargs["with_data"] = True # we have to be sure that we use extra data | |
990 else: | |
991 raise exceptions.DataError("Unknown callback type") | |
992 | |
993 for menu_data in self._menus.itervalues(): | |
994 if menu_data['path'] == path and menu_data['type'] == type_: | |
995 raise exceptions.ConflictError(_("A menu with the same path and type already exists")) | |
996 | |
997 path_canonical = self._getMenuCanonicalPath(path) | |
998 menu_key = (type_, path_canonical) | |
999 | |
1000 if menu_key in self._menus_paths: | |
1001 raise exceptions.ConflictError(u"this menu path is already used: {path} ({menu_key})".format( | |
1002 path=path_canonical, menu_key=menu_key)) | |
1003 | |
1004 menu_data = {'path': tuple(path), | |
1005 'path_canonical': path_canonical, | |
1006 'security_limit': security_limit, | |
1007 'help_string': help_string, | |
1008 'type': type_ | |
1009 } | |
1010 | |
1011 self._menus[callback_id] = menu_data | |
1012 self._menus_paths[menu_key] = callback_id | |
1013 | |
1014 return callback_id | |
1015 | |
1016 def getMenus(self, language='', security_limit=C.NO_SECURITY_LIMIT): | |
1017 """Return all menus registered | |
1018 | |
1019 @param language: language used for translation, or empty string for default | |
1020 @param security_limit: %(doc_security_limit)s | |
1021 @return: array of tuple with: | |
1022 - menu id (same as callback_id) | |
1023 - menu type | |
1024 - raw menu path (array of strings) | |
1025 - translated menu path | |
1026 - extra (dict(unicode, unicode)): extra data where key can be: | |
1027 - icon: name of the icon to use (TODO) | |
1028 - help_url: link to a page with more complete documentation (TODO) | |
1029 """ | |
1030 ret = [] | |
1031 for menu_id, menu_data in self._menus.iteritems(): | |
1032 type_ = menu_data['type'] | |
1033 path = menu_data['path'] | |
1034 menu_security_limit = menu_data['security_limit'] | |
1035 if security_limit!=C.NO_SECURITY_LIMIT and (menu_security_limit==C.NO_SECURITY_LIMIT or menu_security_limit>security_limit): | |
1036 continue | |
1037 languageSwitch(language) | |
1038 path_i18n = [_(elt) for elt in path] | |
1039 languageSwitch() | |
1040 extra = {} # TODO: manage extra data like icon | |
1041 ret.append((menu_id, type_, path, path_i18n, extra)) | |
1042 | |
1043 return ret | |
1044 | |
1045 def _launchMenu(self, menu_type, path, data=None, security_limit=C.NO_SECURITY_LIMIT, profile_key=C.PROF_KEY_NONE): | |
1046 client = self.getClient(profile_key) | |
1047 return self.launchMenu(client, menu_type, path, data, security_limit) | |
1048 | |
1049 def launchMenu(self, client, menu_type, path, data=None, security_limit=C.NO_SECURITY_LIMIT): | |
1050 """launch action a menu action | |
1051 | |
1052 @param menu_type(unicode): type of menu to launch | |
1053 @param path(iterable[unicode]): canonical path of the menu | |
1054 @params data(dict): menu data | |
1055 @raise NotFound: this path is not known | |
1056 """ | |
1057 # FIXME: manage security_limit here | |
1058 # defaut security limit should be high instead of C.NO_SECURITY_LIMIT | |
1059 canonical_path = self._getMenuCanonicalPath(path) | |
1060 menu_key = (menu_type, canonical_path) | |
1061 try: | |
1062 callback_id = self._menus_paths[menu_key] | |
1063 except KeyError: | |
1064 raise exceptions.NotFound(u"Can't find menu {path} ({menu_type})".format( | |
1065 path=canonical_path, menu_type=menu_type)) | |
1066 return self.launchCallback(callback_id, data, client.profile) | |
1067 | |
1068 def getMenuHelp(self, menu_id, language=''): | |
1069 """return the help string of the menu | |
1070 | |
1071 @param menu_id: id of the menu (same as callback_id) | |
1072 @param language: language used for translation, or empty string for default | |
1073 @param return: translated help | |
1074 | |
1075 """ | |
1076 try: | |
1077 menu_data = self._menus[menu_id] | |
1078 except KeyError: | |
1079 raise exceptions.DataError("Trying to access an unknown menu") | |
1080 languageSwitch(language) | |
1081 help_string = _(menu_data['help_string']) | |
1082 languageSwitch() | |
1083 return help_string |