Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0050.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/plugins/plugin_xep_0050.py@0046283a285d |
children | 0112c1f7dcf0 |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # SAT plugin for Ad-Hoc Commands (XEP-0050) | |
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 from sat.core.i18n import _, D_ | |
21 from sat.core.constants import Const as C | |
22 from sat.core.log import getLogger | |
23 log = getLogger(__name__) | |
24 from twisted.words.protocols.jabber import jid | |
25 from twisted.words.protocols import jabber | |
26 from twisted.words.xish import domish | |
27 from twisted.internet import defer | |
28 from wokkel import disco, iwokkel, data_form, compat | |
29 from sat.core import exceptions | |
30 from sat.memory.memory import Sessions | |
31 from uuid import uuid4 | |
32 from sat.tools import xml_tools | |
33 | |
34 from zope.interface import implements | |
35 | |
36 try: | |
37 from twisted.words.protocols.xmlstream import XMPPHandler | |
38 except ImportError: | |
39 from wokkel.subprotocols import XMPPHandler | |
40 | |
41 from collections import namedtuple | |
42 | |
43 try: | |
44 from collections import OrderedDict # only available from python 2.7 | |
45 except ImportError: | |
46 from ordereddict import OrderedDict | |
47 | |
48 IQ_SET = '/iq[@type="set"]' | |
49 NS_COMMANDS = "http://jabber.org/protocol/commands" | |
50 ID_CMD_LIST = disco.DiscoIdentity("automation", "command-list") | |
51 ID_CMD_NODE = disco.DiscoIdentity("automation", "command-node") | |
52 CMD_REQUEST = IQ_SET + '/command[@xmlns="' + NS_COMMANDS + '"]' | |
53 | |
54 SHOWS = OrderedDict([('default', _('Online')), | |
55 ('away', _('Away')), | |
56 ('chat', _('Free for chat')), | |
57 ('dnd', _('Do not disturb')), | |
58 ('xa', _('Left')), | |
59 ('disconnect', _('Disconnect'))]) | |
60 | |
61 PLUGIN_INFO = { | |
62 C.PI_NAME: "Ad-Hoc Commands", | |
63 C.PI_IMPORT_NAME: "XEP-0050", | |
64 C.PI_TYPE: "XEP", | |
65 C.PI_PROTOCOLS: ["XEP-0050"], | |
66 C.PI_MAIN: "XEP_0050", | |
67 C.PI_HANDLER: "yes", | |
68 C.PI_DESCRIPTION: _("""Implementation of Ad-Hoc Commands""") | |
69 } | |
70 | |
71 | |
72 class AdHocError(Exception): | |
73 | |
74 def __init__(self, error_const): | |
75 """ Error to be used from callback | |
76 @param error_const: one of XEP_0050.ERROR | |
77 """ | |
78 assert error_const in XEP_0050.ERROR | |
79 self.callback_error = error_const | |
80 | |
81 class AdHocCommand(XMPPHandler): | |
82 implements(iwokkel.IDisco) | |
83 | |
84 def __init__(self, parent, callback, label, node, features, timeout, allowed_jids, allowed_groups, allowed_magics, forbidden_jids, forbidden_groups, client): | |
85 self.parent = parent | |
86 self.callback = callback | |
87 self.label = label | |
88 self.node = node | |
89 self.features = [disco.DiscoFeature(feature) for feature in features] | |
90 self.allowed_jids, self.allowed_groups, self.allowed_magics, self.forbidden_jids, self.forbidden_groups = allowed_jids, allowed_groups, allowed_magics, forbidden_jids, forbidden_groups | |
91 self.client = client | |
92 self.sessions = Sessions(timeout=timeout) | |
93 | |
94 def getName(self, xml_lang=None): | |
95 return self.label | |
96 | |
97 def isAuthorised(self, requestor): | |
98 if '@ALL@' in self.allowed_magics: | |
99 return True | |
100 forbidden = set(self.forbidden_jids) | |
101 for group in self.forbidden_groups: | |
102 forbidden.update(self.client.roster.getJidsFromGroup(group)) | |
103 if requestor.userhostJID() in forbidden: | |
104 return False | |
105 allowed = set(self.allowed_jids) | |
106 for group in self.allowed_groups: | |
107 try: | |
108 allowed.update(self.client.roster.getJidsFromGroup(group)) | |
109 except exceptions.UnknownGroupError: | |
110 log.warning(_(u"The groups [%(group)s] is unknown for profile [%(profile)s])" % {'group':group, 'profile':self.client.profile})) | |
111 if requestor.userhostJID() in allowed: | |
112 return True | |
113 return False | |
114 | |
115 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
116 if nodeIdentifier != NS_COMMANDS: # FIXME: we should manage other disco nodes here | |
117 return [] | |
118 # identities = [ID_CMD_LIST if self.node == NS_COMMANDS else ID_CMD_NODE] # FIXME | |
119 return [disco.DiscoFeature(NS_COMMANDS)] + self.features | |
120 | |
121 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
122 return [] | |
123 | |
124 def _sendAnswer(self, callback_data, session_id, request): | |
125 """ Send result of the command | |
126 @param callback_data: tuple (payload, status, actions, note) with: | |
127 - payload (domish.Element) usualy containing data form | |
128 - status: current status, see XEP_0050.STATUS | |
129 - actions: list of allowed actions (see XEP_0050.ACTION). First action is the default one. Default to EXECUTE | |
130 - note: optional additional note: either None or a tuple with (note type, human readable string), | |
131 note type being in XEP_0050.NOTE | |
132 @param session_id: current session id | |
133 @param request: original request (domish.Element) | |
134 @return: deferred | |
135 """ | |
136 payload, status, actions, note = callback_data | |
137 assert(isinstance(payload, domish.Element) or payload is None) | |
138 assert(status in XEP_0050.STATUS) | |
139 if not actions: | |
140 actions = [XEP_0050.ACTION.EXECUTE] | |
141 result = domish.Element((None, 'iq')) | |
142 result['type'] = 'result' | |
143 result['id'] = request['id'] | |
144 result['to'] = request['from'] | |
145 command_elt = result.addElement('command', NS_COMMANDS) | |
146 command_elt['sessionid'] = session_id | |
147 command_elt['node'] = self.node | |
148 command_elt['status'] = status | |
149 | |
150 if status != XEP_0050.STATUS.CANCELED: | |
151 if status != XEP_0050.STATUS.COMPLETED: | |
152 actions_elt = command_elt.addElement('actions') | |
153 actions_elt['execute'] = actions[0] | |
154 for action in actions: | |
155 actions_elt.addElement(action) | |
156 | |
157 if note is not None: | |
158 note_type, note_mess = note | |
159 note_elt = command_elt.addElement('note', content=note_mess) | |
160 note_elt['type'] = note_type | |
161 | |
162 if payload is not None: | |
163 command_elt.addChild(payload) | |
164 | |
165 self.client.send(result) | |
166 if status in (XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED): | |
167 del self.sessions[session_id] | |
168 | |
169 def _sendError(self, error_constant, session_id, request): | |
170 """ Send error stanza | |
171 @param error_constant: one of XEP_OO50.ERROR | |
172 @param request: original request (domish.Element) | |
173 """ | |
174 xmpp_condition, cmd_condition = error_constant | |
175 iq_elt = jabber.error.StanzaError(xmpp_condition).toResponse(request) | |
176 if cmd_condition: | |
177 error_elt = iq_elt.elements(None, "error").next() | |
178 error_elt.addElement(cmd_condition, NS_COMMANDS) | |
179 self.client.send(iq_elt) | |
180 del self.sessions[session_id] | |
181 | |
182 def onRequest(self, command_elt, requestor, action, session_id): | |
183 if not self.isAuthorised(requestor): | |
184 return self._sendError(XEP_0050.ERROR.FORBIDDEN, session_id, command_elt.parent) | |
185 if session_id: | |
186 try: | |
187 session_data = self.sessions[session_id] | |
188 except KeyError: | |
189 return self._sendError(XEP_0050.ERROR.SESSION_EXPIRED, session_id, command_elt.parent) | |
190 if session_data['requestor'] != requestor: | |
191 return self._sendError(XEP_0050.ERROR.FORBIDDEN, session_id, command_elt.parent) | |
192 else: | |
193 session_id, session_data = self.sessions.newSession() | |
194 session_data['requestor'] = requestor | |
195 if action == XEP_0050.ACTION.CANCEL: | |
196 d = defer.succeed((None, XEP_0050.STATUS.CANCELED, None, None)) | |
197 else: | |
198 d = defer.maybeDeferred(self.callback, command_elt, session_data, action, self.node, self.client.profile) | |
199 d.addCallback(self._sendAnswer, session_id, command_elt.parent) | |
200 d.addErrback(lambda failure, request: self._sendError(failure.value.callback_error, session_id, request), command_elt.parent) | |
201 | |
202 | |
203 class XEP_0050(object): | |
204 STATUS = namedtuple('Status', ('EXECUTING', 'COMPLETED', 'CANCELED'))('executing', 'completed', 'canceled') | |
205 ACTION = namedtuple('Action', ('EXECUTE', 'CANCEL', 'NEXT', 'PREV'))('execute', 'cancel', 'next', 'prev') | |
206 NOTE = namedtuple('Note', ('INFO','WARN','ERROR'))('info','warn','error') | |
207 ERROR = namedtuple('Error', ('MALFORMED_ACTION', 'BAD_ACTION', 'BAD_LOCALE', 'BAD_PAYLOAD', 'BAD_SESSIONID', 'SESSION_EXPIRED', | |
208 'FORBIDDEN', 'ITEM_NOT_FOUND', 'FEATURE_NOT_IMPLEMENTED', 'INTERNAL'))(('bad-request', 'malformed-action'), | |
209 ('bad-request', 'bad-action'), ('bad-request', 'bad-locale'), ('bad-request','bad-payload'), | |
210 ('bad-request','bad-sessionid'), ('not-allowed','session-expired'), ('forbidden', None), | |
211 ('item-not-found', None), ('feature-not-implemented', None), ('internal-server-error', None)) # XEP-0050 §4.4 Table 5 | |
212 | |
213 def __init__(self, host): | |
214 log.info(_("plugin XEP-0050 initialization")) | |
215 self.host = host | |
216 self.requesting = Sessions() | |
217 self.answering = {} | |
218 host.bridge.addMethod("adHocRun", ".plugin", in_sign='sss', out_sign='s', | |
219 method=self._run, | |
220 async=True) | |
221 host.bridge.addMethod("adHocList", ".plugin", in_sign='ss', out_sign='s', | |
222 method=self._list, | |
223 async=True) | |
224 self.__requesting_id = host.registerCallback(self._requestingEntity, with_data=True) | |
225 host.importMenu((D_("Service"), D_("Commands")), self._commandsMenu, security_limit=2, help_string=D_("Execute ad-hoc commands")) | |
226 | |
227 def getHandler(self, client): | |
228 return XEP_0050_handler(self) | |
229 | |
230 def profileConnected(self, client): | |
231 self.addAdHocCommand(self._statusCallback, _("Status"), profile_key=client.profile) | |
232 | |
233 def profileDisconnected(self, client): | |
234 try: | |
235 del self.answering[client.profile] | |
236 except KeyError: | |
237 pass | |
238 | |
239 def _items2XMLUI(self, items, no_instructions): | |
240 """ Convert discovery items to XMLUI dialog """ | |
241 # TODO: manage items on different jids | |
242 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) | |
243 | |
244 if not no_instructions: | |
245 form_ui.addText(_("Please select a command"), 'instructions') | |
246 | |
247 options = [(item.nodeIdentifier, item.name) for item in items] | |
248 form_ui.addList("node", options) | |
249 return form_ui | |
250 | |
251 def _getDataLvl(self, type_): | |
252 """Return the constant corresponding to <note/> type attribute value | |
253 | |
254 @param type_: note type (see XEP-0050 §4.3) | |
255 @return: a C.XMLUI_DATA_LVL_* constant | |
256 """ | |
257 if type_ == 'error': | |
258 return C.XMLUI_DATA_LVL_ERROR | |
259 elif type_ == 'warn': | |
260 return C.XMLUI_DATA_LVL_WARNING | |
261 else: | |
262 if type_ != 'info': | |
263 log.warning(_(u"Invalid note type [%s], using info") % type_) | |
264 return C.XMLUI_DATA_LVL_INFO | |
265 | |
266 def _mergeNotes(self, notes): | |
267 """Merge notes with level prefix (e.g. "ERROR: the message") | |
268 | |
269 @param notes (list): list of tuple (level, message) | |
270 @return: list of messages | |
271 """ | |
272 lvl_map = {C.XMLUI_DATA_LVL_INFO: '', | |
273 C.XMLUI_DATA_LVL_WARNING: "%s: " % _("WARNING"), | |
274 C.XMLUI_DATA_LVL_ERROR: "%s: " % _("ERROR") | |
275 } | |
276 return [u"%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes] | |
277 | |
278 def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): | |
279 """ | |
280 Convert command answer to an ui for frontend | |
281 @param iq_elt: command result | |
282 @param session_id: id of the session used with the frontend | |
283 @param profile_key: %(doc_profile_key)s | |
284 | |
285 """ | |
286 command_elt = iq_elt.elements(NS_COMMANDS, "command").next() | |
287 status = command_elt.getAttribute('status', XEP_0050.STATUS.EXECUTING) | |
288 if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]: | |
289 # the command session is finished, we purge our session | |
290 del self.requesting[session_id] | |
291 if status == XEP_0050.STATUS.COMPLETED: | |
292 session_id = None | |
293 else: | |
294 return None | |
295 remote_session_id = command_elt.getAttribute('sessionid') | |
296 if remote_session_id: | |
297 session_data['remote_id'] = remote_session_id | |
298 notes = [] | |
299 for note_elt in command_elt.elements(NS_COMMANDS, 'note'): | |
300 notes.append((self._getDataLvl(note_elt.getAttribute('type', 'info')), | |
301 unicode(note_elt))) | |
302 try: | |
303 data_elt = command_elt.elements(data_form.NS_X_DATA, 'x').next() | |
304 except StopIteration: | |
305 if status != XEP_0050.STATUS.COMPLETED: | |
306 log.warning(_("No known payload found in ad-hoc command result, aborting")) | |
307 del self.requesting[session_id] | |
308 return xml_tools.XMLUI(C.XMLUI_DIALOG, | |
309 dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, | |
310 C.XMLUI_DATA_MESS: _("No payload found"), | |
311 C.XMLUI_DATA_LVL: C.XMLUI_DATA_LVL_ERROR, | |
312 } | |
313 ) | |
314 if not notes: | |
315 # the status is completed, and we have no note to show | |
316 return None | |
317 | |
318 # if we have only one note, we show a dialog with the level of the note | |
319 # if we have more, we show a dialog with "info" level, and all notes merged | |
320 dlg_level = notes[0][0] if len(notes) == 1 else C.XMLUI_DATA_LVL_INFO | |
321 return xml_tools.XMLUI( | |
322 C.XMLUI_DIALOG, | |
323 dialog_opt = {C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, | |
324 C.XMLUI_DATA_MESS: u'\n'.join(self._mergeNotes(notes)), | |
325 C.XMLUI_DATA_LVL: dlg_level, | |
326 }, | |
327 session_id = session_id | |
328 ) | |
329 | |
330 if session_id is None: | |
331 return xml_tools.dataFormEltResult2XMLUI(data_elt) | |
332 form = data_form.Form.fromElement(data_elt) | |
333 # we add any present note to the instructions | |
334 form.instructions.extend(self._mergeNotes(notes)) | |
335 return xml_tools.dataForm2XMLUI(form, self.__requesting_id, session_id=session_id) | |
336 | |
337 def _requestingEntity(self, data, profile): | |
338 def serialise(ret_data): | |
339 if 'xmlui' in ret_data: | |
340 ret_data['xmlui'] = ret_data['xmlui'].toXml() | |
341 return ret_data | |
342 | |
343 d = self.requestingEntity(data, profile) | |
344 d.addCallback(serialise) | |
345 return d | |
346 | |
347 def requestingEntity(self, data, profile): | |
348 """ | |
349 request and entity and create XMLUI accordingly | |
350 @param data: data returned by previous XMLUI (first one must come from self._commandsMenu) | |
351 @param profile: %(doc_profile)s | |
352 @return: callback dict result (with "xmlui" corresponding to the answering dialog, or empty if it's finished without error) | |
353 | |
354 """ | |
355 if C.bool(data.get('cancelled', C.BOOL_FALSE)): | |
356 return defer.succeed({}) | |
357 client = self.host.getClient(profile) | |
358 # TODO: cancel, prev and next are not managed | |
359 # TODO: managed answerer errors | |
360 # TODO: manage nodes with a non data form payload | |
361 if "session_id" not in data: | |
362 # we just had the jid, we now request it for the available commands | |
363 session_id, session_data = self.requesting.newSession(profile=client.profile) | |
364 entity = jid.JID(data[xml_tools.SAT_FORM_PREFIX+'jid']) | |
365 session_data['jid'] = entity | |
366 d = self.list(client, entity) | |
367 | |
368 def sendItems(xmlui): | |
369 xmlui.session_id = session_id # we need to keep track of the session | |
370 return {'xmlui': xmlui} | |
371 | |
372 d.addCallback(sendItems) | |
373 else: | |
374 # we have started a several forms sessions | |
375 try: | |
376 session_data = self.requesting.profileGet(data["session_id"], client.profile) | |
377 except KeyError: | |
378 log.warning ("session id doesn't exist, session has probably expired") | |
379 # TODO: send error dialog | |
380 return defer.succeed({}) | |
381 session_id = data["session_id"] | |
382 entity = session_data['jid'] | |
383 try: | |
384 session_data['node'] | |
385 # node has already been received | |
386 except KeyError: | |
387 # it's the first time we know the node, we save it in session data | |
388 session_data['node'] = data[xml_tools.SAT_FORM_PREFIX+'node'] | |
389 | |
390 # we request execute node's command | |
391 iq_elt = compat.IQ(client.xmlstream, 'set') | |
392 iq_elt['to'] = entity.full() | |
393 command_elt = iq_elt.addElement("command", NS_COMMANDS) | |
394 command_elt['node'] = session_data['node'] | |
395 command_elt['action'] = XEP_0050.ACTION.EXECUTE | |
396 try: | |
397 # remote_id is the XEP_0050 sessionid used by answering command | |
398 # while session_id is our own session id used with the frontend | |
399 command_elt['sessionid'] = session_data['remote_id'] | |
400 except KeyError: | |
401 pass | |
402 | |
403 command_elt.addChild(xml_tools.XMLUIResultToElt(data)) # We add the XMLUI result to the command payload | |
404 d = iq_elt.send() | |
405 d.addCallback(self._commandsAnswer2XMLUI, session_id, session_data) | |
406 d.addCallback(lambda xmlui: {'xmlui': xmlui} if xmlui is not None else {}) | |
407 | |
408 return d | |
409 | |
410 def _commandsMenu(self, menu_data, profile): | |
411 """ First XMLUI activated by menu: ask for target jid | |
412 @param profile: %(doc_profile)s | |
413 | |
414 """ | |
415 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) | |
416 form_ui.addText(_("Please enter target jid"), 'instructions') | |
417 form_ui.changeContainer("pairs") | |
418 form_ui.addLabel("jid") | |
419 form_ui.addString("jid", value=self.host.getClient(profile).jid.host) | |
420 return {'xmlui': form_ui.toXml()} | |
421 | |
422 def _statusCallback(self, command_elt, session_data, action, node, profile): | |
423 """ Ad-hoc command used to change the "show" part of status """ | |
424 actions = session_data.setdefault('actions',[]) | |
425 actions.append(action) | |
426 | |
427 if len(actions) == 1: | |
428 # it's our first request, we ask the desired new status | |
429 status = XEP_0050.STATUS.EXECUTING | |
430 form = data_form.Form('form', title=_('status selection')) | |
431 show_options = [data_form.Option(name, label) for name, label in SHOWS.items()] | |
432 field = data_form.Field('list-single', 'show', options=show_options, required=True) | |
433 form.addField(field) | |
434 | |
435 payload = form.toElement() | |
436 note = None | |
437 | |
438 elif len(actions) == 2: | |
439 # we should have the answer here | |
440 try: | |
441 x_elt = command_elt.elements(data_form.NS_X_DATA,'x').next() | |
442 answer_form = data_form.Form.fromElement(x_elt) | |
443 show = answer_form['show'] | |
444 except (KeyError, StopIteration): | |
445 raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD) | |
446 if show not in SHOWS: | |
447 raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD) | |
448 if show == "disconnect": | |
449 self.host.disconnect(profile) | |
450 else: | |
451 self.host.setPresence(show=show, profile_key=profile) | |
452 | |
453 # job done, we can end the session | |
454 form = data_form.Form('form', title=_(u'Updated')) | |
455 form.addField(data_form.Field('fixed', u'Status updated')) | |
456 status = XEP_0050.STATUS.COMPLETED | |
457 payload = None | |
458 note = (self.NOTE.INFO, _(u"Status updated")) | |
459 else: | |
460 raise AdHocError(XEP_0050.ERROR.INTERNAL) | |
461 | |
462 return (payload, status, None, note) | |
463 | |
464 def _run(self, service_jid_s='', node='', profile_key=C.PROF_KEY_NONE): | |
465 client = self.host.getClient(profile_key) | |
466 service_jid = jid.JID(service_jid_s) if service_jid_s else None | |
467 d = self.run(client, service_jid, node or None) | |
468 d.addCallback(lambda xmlui: xmlui.toXml()) | |
469 return d | |
470 | |
471 @defer.inlineCallbacks | |
472 def run(self, client, service_jid=None, node=None): | |
473 """run an ad-hoc command | |
474 | |
475 @param service_jid(jid.JID, None): jid of the ad-hoc service | |
476 None to use profile's server | |
477 @param node(unicode, None): node of the ad-hoc commnad | |
478 None to get initial list | |
479 @return(unicode): command page XMLUI | |
480 """ | |
481 if service_jid is None: | |
482 service_jid = jid.JID(client.jid.host) | |
483 session_id, session_data = self.requesting.newSession(profile=client.profile) | |
484 session_data['jid'] = service_jid | |
485 if node is None: | |
486 xmlui = yield self.list(client, service_jid) | |
487 else: | |
488 session_data['node'] = node | |
489 cb_data = yield self.requestingEntity({'session_id': session_id}, client.profile) | |
490 xmlui = cb_data['xmlui'] | |
491 | |
492 xmlui.session_id = session_id | |
493 defer.returnValue(xmlui) | |
494 | |
495 def _list(self, to_jid_s, profile_key): | |
496 client = self.host.getClient(profile_key) | |
497 to_jid = jid.JID(to_jid_s) if to_jid_s else None | |
498 d = self.list(client, to_jid, no_instructions=True) | |
499 d.addCallback(lambda xmlui: xmlui.toXml()) | |
500 return d | |
501 | |
502 def list(self, client, to_jid, no_instructions=False): | |
503 """Request available commands | |
504 | |
505 @param to_jid(jid.JID, None): the entity answering the commands | |
506 None to use profile's server | |
507 @param no_instructions(bool): if True, don't add instructions widget | |
508 """ | |
509 d = self.host.getDiscoItems(client, to_jid, NS_COMMANDS) | |
510 d.addCallback(self._items2XMLUI, no_instructions) | |
511 return d | |
512 | |
513 def addAdHocCommand(self, callback, label, node=None, features=None, timeout=600, allowed_jids=None, allowed_groups=None, | |
514 allowed_magics=None, forbidden_jids=None, forbidden_groups=None, profile_key=C.PROF_KEY_NONE): | |
515 """Add an ad-hoc command for the current profile | |
516 | |
517 @param callback: method associated with this ad-hoc command which return the payload data (see AdHocCommand._sendAnswer), can return a deferred | |
518 @param label: label associated with this command on the main menu | |
519 @param node: disco item node associated with this command. None to use autogenerated node | |
520 @param features: features associated with the payload (list of strings), usualy data form | |
521 @param timeout: delay between two requests before canceling the session (in seconds) | |
522 @param allowed_jids: list of allowed entities | |
523 @param allowed_groups: list of allowed roster groups | |
524 @param allowed_magics: list of allowed magic keys, can be: | |
525 @ALL@: allow everybody | |
526 @PROFILE_BAREJID@: allow only the jid of the profile | |
527 @param forbidden_jids: black list of entities which can't access this command | |
528 @param forbidden_groups: black list of groups which can't access this command | |
529 @param profile_key: profile key associated with this command, @ALL@ means can be accessed with every profiles | |
530 @return: node of the added command, useful to remove the command later | |
531 """ | |
532 # FIXME: "@ALL@" for profile_key seems useless and dangerous | |
533 | |
534 if node is None: | |
535 node = "%s_%s" % ('COMMANDS', uuid4()) | |
536 | |
537 if features is None: | |
538 features = [data_form.NS_X_DATA] | |
539 | |
540 if allowed_jids is None: | |
541 allowed_jids = [] | |
542 if allowed_groups is None: | |
543 allowed_groups = [] | |
544 if allowed_magics is None: | |
545 allowed_magics = ['@PROFILE_BAREJID@'] | |
546 if forbidden_jids is None: | |
547 forbidden_jids = [] | |
548 if forbidden_groups is None: | |
549 forbidden_groups = [] | |
550 | |
551 for client in self.host.getClients(profile_key): | |
552 #TODO: manage newly created/removed profiles | |
553 _allowed_jids = (allowed_jids + [client.jid.userhostJID()]) if '@PROFILE_BAREJID@' in allowed_magics else allowed_jids | |
554 ad_hoc_command = AdHocCommand(self, callback, label, node, features, timeout, _allowed_jids, | |
555 allowed_groups, allowed_magics, forbidden_jids, forbidden_groups, client) | |
556 ad_hoc_command.setHandlerParent(client) | |
557 profile_commands = self.answering.setdefault(client.profile, {}) | |
558 profile_commands[node] = ad_hoc_command | |
559 | |
560 def onCmdRequest(self, request, profile): | |
561 request.handled = True | |
562 requestor = jid.JID(request['from']) | |
563 command_elt = request.elements(NS_COMMANDS, 'command').next() | |
564 action = command_elt.getAttribute('action', self.ACTION.EXECUTE) | |
565 node = command_elt.getAttribute('node') | |
566 if not node: | |
567 raise exceptions.DataError | |
568 sessionid = command_elt.getAttribute('sessionid') | |
569 try: | |
570 command = self.answering[profile][node] | |
571 except KeyError: | |
572 raise exceptions.DataError | |
573 command.onRequest(command_elt, requestor, action, sessionid) | |
574 | |
575 | |
576 class XEP_0050_handler(XMPPHandler): | |
577 implements(iwokkel.IDisco) | |
578 | |
579 def __init__(self, plugin_parent): | |
580 self.plugin_parent = plugin_parent | |
581 | |
582 def connectionInitialized(self): | |
583 self.xmlstream.addObserver(CMD_REQUEST, self.plugin_parent.onCmdRequest, profile=self.parent.profile) | |
584 | |
585 def getDiscoInfo(self, requestor, target, nodeIdentifier=''): | |
586 identities = [] | |
587 if nodeIdentifier == NS_COMMANDS and self.plugin_parent.answering.get(self.parent.profile): # we only add the identity if we have registred commands | |
588 identities.append(ID_CMD_LIST) | |
589 return [disco.DiscoFeature(NS_COMMANDS)] + identities | |
590 | |
591 def getDiscoItems(self, requestor, target, nodeIdentifier=''): | |
592 ret = [] | |
593 if nodeIdentifier == NS_COMMANDS: | |
594 for command in self.plugin_parent.answering.get(self.parent.profile,{}).values(): | |
595 if command.isAuthorised(requestor): | |
596 ret.append(disco.DiscoItem(self.parent.jid, command.node, command.getName())) #TODO: manage name language | |
597 return ret |