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