Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0050.py @ 2667:8dd9db785ac8
plugin XEP-0050, adhoc D-Bus: Ad-Hoc improvment + remote media control:
- "commands" namespace is now registered
- added "do" and "getCommandElt" methods to XEP-0050 to run ad-hoc commands from backend
- commands for a profile are now stored in client._XEP_0050_commands
- Ad-Hoc D-Bus plugin can now be run without lxml or dbus (degraded)
- use MPRIS to control media players
- new adHocRemotesGet bridge method retrieve media players announced in all devices of the profile
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 31 Aug 2018 15:47:00 +0200 |
parents | 56f94936df1e |
children | 003b8b4b56a7 |
comparison
equal
deleted
inserted
replaced
2666:bc122b68eacd | 2667:8dd9db785ac8 |
---|---|
24 log = getLogger(__name__) | 24 log = getLogger(__name__) |
25 from twisted.words.protocols.jabber import jid | 25 from twisted.words.protocols.jabber import jid |
26 from twisted.words.protocols import jabber | 26 from twisted.words.protocols import jabber |
27 from twisted.words.xish import domish | 27 from twisted.words.xish import domish |
28 from twisted.internet import defer | 28 from twisted.internet import defer |
29 from wokkel import disco, iwokkel, data_form, compat | 29 from wokkel import disco, iwokkel, data_form |
30 from sat.core import exceptions | 30 from sat.core import exceptions |
31 from sat.memory.memory import Sessions | 31 from sat.memory.memory import Sessions |
32 from uuid import uuid4 | 32 from uuid import uuid4 |
33 from sat.tools import xml_tools | 33 from sat.tools import xml_tools |
34 | 34 |
68 C.PI_IMPORT_NAME: "XEP-0050", | 68 C.PI_IMPORT_NAME: "XEP-0050", |
69 C.PI_TYPE: "XEP", | 69 C.PI_TYPE: "XEP", |
70 C.PI_PROTOCOLS: ["XEP-0050"], | 70 C.PI_PROTOCOLS: ["XEP-0050"], |
71 C.PI_MAIN: "XEP_0050", | 71 C.PI_MAIN: "XEP_0050", |
72 C.PI_HANDLER: "yes", | 72 C.PI_HANDLER: "yes", |
73 C.PI_DESCRIPTION: _("""Implementation of Ad-Hoc Commands"""), | 73 C.PI_DESCRIPTION: _(u"""Implementation of Ad-Hoc Commands"""), |
74 } | 74 } |
75 | 75 |
76 | 76 |
77 class AdHocError(Exception): | 77 class AdHocError(Exception): |
78 def __init__(self, error_const): | 78 def __init__(self, error_const): |
84 | 84 |
85 | 85 |
86 class AdHocCommand(XMPPHandler): | 86 class AdHocCommand(XMPPHandler): |
87 implements(iwokkel.IDisco) | 87 implements(iwokkel.IDisco) |
88 | 88 |
89 def __init__( | 89 def __init__(self, callback, label, node, features, timeout, |
90 self, | 90 allowed_jids, allowed_groups, allowed_magics, forbidden_jids, |
91 parent, | 91 forbidden_groups): |
92 callback, | 92 XMPPHandler.__init__(self) |
93 label, | |
94 node, | |
95 features, | |
96 timeout, | |
97 allowed_jids, | |
98 allowed_groups, | |
99 allowed_magics, | |
100 forbidden_jids, | |
101 forbidden_groups, | |
102 client, | |
103 ): | |
104 self.parent = parent | |
105 self.callback = callback | 93 self.callback = callback |
106 self.label = label | 94 self.label = label |
107 self.node = node | 95 self.node = node |
108 self.features = [disco.DiscoFeature(feature) for feature in features] | 96 self.features = [disco.DiscoFeature(feature) for feature in features] |
109 self.allowed_jids, self.allowed_groups, self.allowed_magics, self.forbidden_jids, self.forbidden_groups = ( | 97 ( |
98 self.allowed_jids, | |
99 self.allowed_groups, | |
100 self.allowed_magics, | |
101 self.forbidden_jids, | |
102 self.forbidden_groups, | |
103 ) = ( | |
110 allowed_jids, | 104 allowed_jids, |
111 allowed_groups, | 105 allowed_groups, |
112 allowed_magics, | 106 allowed_magics, |
113 forbidden_jids, | 107 forbidden_jids, |
114 forbidden_groups, | 108 forbidden_groups, |
115 ) | 109 ) |
116 self.client = client | |
117 self.sessions = Sessions(timeout=timeout) | 110 self.sessions = Sessions(timeout=timeout) |
111 | |
112 @property | |
113 def client(self): | |
114 return self.parent | |
118 | 115 |
119 def getName(self, xml_lang=None): | 116 def getName(self, xml_lang=None): |
120 return self.label | 117 return self.label |
121 | 118 |
122 def isAuthorised(self, requestor): | 119 def isAuthorised(self, requestor): |
130 allowed = set(self.allowed_jids) | 127 allowed = set(self.allowed_jids) |
131 for group in self.allowed_groups: | 128 for group in self.allowed_groups: |
132 try: | 129 try: |
133 allowed.update(self.client.roster.getJidsFromGroup(group)) | 130 allowed.update(self.client.roster.getJidsFromGroup(group)) |
134 except exceptions.UnknownGroupError: | 131 except exceptions.UnknownGroupError: |
135 log.warning( | 132 log.warning(_(u"The groups [{group}] is unknown for profile [{profile}])") |
136 _( | 133 .format(group=group, profile=self.client.profile)) |
137 u"The groups [%(group)s] is unknown for profile [%(profile)s])" | |
138 % {"group": group, "profile": self.client.profile} | |
139 ) | |
140 ) | |
141 if requestor.userhostJID() in allowed: | 134 if requestor.userhostJID() in allowed: |
142 return True | 135 return True |
143 return False | 136 return False |
144 | 137 |
145 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | 138 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): |
153 def getDiscoItems(self, requestor, target, nodeIdentifier=""): | 146 def getDiscoItems(self, requestor, target, nodeIdentifier=""): |
154 return [] | 147 return [] |
155 | 148 |
156 def _sendAnswer(self, callback_data, session_id, request): | 149 def _sendAnswer(self, callback_data, session_id, request): |
157 """ Send result of the command | 150 """ Send result of the command |
151 | |
158 @param callback_data: tuple (payload, status, actions, note) with: | 152 @param callback_data: tuple (payload, status, actions, note) with: |
159 - payload (domish.Element) usualy containing data form | 153 - payload (domish.Element, None) usualy containing data form |
160 - status: current status, see XEP_0050.STATUS | 154 - status: current status, see XEP_0050.STATUS |
161 - actions: list of allowed actions (see XEP_0050.ACTION). First action is the default one. Default to EXECUTE | 155 - actions(list[str], None): list of allowed actions (see XEP_0050.ACTION). |
162 - note: optional additional note: either None or a tuple with (note type, human readable string), | 156 First action is the default one. Default to EXECUTE |
163 note type being in XEP_0050.NOTE | 157 - note(tuple[str, unicode]): optional additional note: either None or a |
158 tuple with (note type, human readable string), "note type" being in | |
159 XEP_0050.NOTE | |
164 @param session_id: current session id | 160 @param session_id: current session id |
165 @param request: original request (domish.Element) | 161 @param request: original request (domish.Element) |
166 @return: deferred | 162 @return: deferred |
167 """ | 163 """ |
168 payload, status, actions, note = callback_data | 164 payload, status, actions, note = callback_data |
198 if status in (XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED): | 194 if status in (XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED): |
199 del self.sessions[session_id] | 195 del self.sessions[session_id] |
200 | 196 |
201 def _sendError(self, error_constant, session_id, request): | 197 def _sendError(self, error_constant, session_id, request): |
202 """ Send error stanza | 198 """ Send error stanza |
199 | |
203 @param error_constant: one of XEP_OO50.ERROR | 200 @param error_constant: one of XEP_OO50.ERROR |
204 @param request: original request (domish.Element) | 201 @param request: original request (domish.Element) |
205 """ | 202 """ |
206 xmpp_condition, cmd_condition = error_constant | 203 xmpp_condition, cmd_condition = error_constant |
207 iq_elt = jabber.error.StanzaError(xmpp_condition).toResponse(request) | 204 iq_elt = jabber.error.StanzaError(xmpp_condition).toResponse(request) |
233 if action == XEP_0050.ACTION.CANCEL: | 230 if action == XEP_0050.ACTION.CANCEL: |
234 d = defer.succeed((None, XEP_0050.STATUS.CANCELED, None, None)) | 231 d = defer.succeed((None, XEP_0050.STATUS.CANCELED, None, None)) |
235 else: | 232 else: |
236 d = defer.maybeDeferred( | 233 d = defer.maybeDeferred( |
237 self.callback, | 234 self.callback, |
235 self.client, | |
238 command_elt, | 236 command_elt, |
239 session_data, | 237 session_data, |
240 action, | 238 action, |
241 self.node, | 239 self.node, |
242 self.client.profile, | |
243 ) | 240 ) |
244 d.addCallback(self._sendAnswer, session_id, command_elt.parent) | 241 d.addCallback(self._sendAnswer, session_id, command_elt.parent) |
245 d.addErrback( | 242 d.addErrback( |
246 lambda failure, request: self._sendError( | 243 lambda failure, request: self._sendError( |
247 failure.value.callback_error, session_id, request | 244 failure.value.callback_error, session_id, request |
287 | 284 |
288 def __init__(self, host): | 285 def __init__(self, host): |
289 log.info(_("plugin XEP-0050 initialization")) | 286 log.info(_("plugin XEP-0050 initialization")) |
290 self.host = host | 287 self.host = host |
291 self.requesting = Sessions() | 288 self.requesting = Sessions() |
292 self.answering = {} | |
293 host.bridge.addMethod( | 289 host.bridge.addMethod( |
294 "adHocRun", | 290 "adHocRun", |
295 ".plugin", | 291 ".plugin", |
296 in_sign="sss", | 292 in_sign="sss", |
297 out_sign="s", | 293 out_sign="s", |
301 host.bridge.addMethod( | 297 host.bridge.addMethod( |
302 "adHocList", | 298 "adHocList", |
303 ".plugin", | 299 ".plugin", |
304 in_sign="ss", | 300 in_sign="ss", |
305 out_sign="s", | 301 out_sign="s", |
306 method=self._list, | 302 method=self._listUI, |
307 async=True, | 303 async=True, |
308 ) | 304 ) |
309 self.__requesting_id = host.registerCallback( | 305 self.__requesting_id = host.registerCallback( |
310 self._requestingEntity, with_data=True | 306 self._requestingEntity, with_data=True |
311 ) | 307 ) |
313 (D_("Service"), D_("Commands")), | 309 (D_("Service"), D_("Commands")), |
314 self._commandsMenu, | 310 self._commandsMenu, |
315 security_limit=2, | 311 security_limit=2, |
316 help_string=D_("Execute ad-hoc commands"), | 312 help_string=D_("Execute ad-hoc commands"), |
317 ) | 313 ) |
314 host.registerNamespace(u'commands', NS_COMMANDS) | |
318 | 315 |
319 def getHandler(self, client): | 316 def getHandler(self, client): |
320 return XEP_0050_handler(self) | 317 return XEP_0050_handler(self) |
321 | 318 |
322 def profileConnected(self, client): | 319 def profileConnected(self, client): |
323 self.addAdHocCommand( | 320 # map from node to AdHocCommand instance |
324 self._statusCallback, _("Status"), profile_key=client.profile | 321 client._XEP_0050_commands = {} |
325 ) | 322 self.addAdHocCommand(client, self._statusCallback, _("Status")) |
326 | 323 |
327 def profileDisconnected(self, client): | 324 def do(self, client, entity, node, action=ACTION.EXECUTE, session_id=None, |
325 form_values=None, timeout=30): | |
326 """Do an Ad-Hoc Command | |
327 | |
328 @param entity(jid.JID): entity which will execture the command | |
329 @param node(unicode): node of the command | |
330 @param action(unicode): one of XEP_0050.ACTION | |
331 @param session_id(unicode, None): id of the ad-hoc session | |
332 None if no session is involved | |
333 @param form_values(dict, None): values to use to create command form | |
334 values will be passed to data_form.Form.makeFields | |
335 @return | |
336 """ | |
337 iq_elt = client.IQ(timeout=timeout) | |
338 iq_elt["to"] = entity.full() | |
339 command_elt = iq_elt.addElement("command", NS_COMMANDS) | |
340 command_elt["node"] = node | |
341 command_elt["action"] = action | |
342 if session_id is not None: | |
343 command_elt["sessionid"] = session_id | |
344 | |
345 if form_values: | |
346 # We add the XMLUI result to the command payload | |
347 form = data_form.Form("submit") | |
348 form.makeFields(form_values) | |
349 command_elt.addChild(form.toElement()) | |
350 d = iq_elt.send() | |
351 return d | |
352 | |
353 def getCommandElt(self, iq_elt): | |
328 try: | 354 try: |
329 del self.answering[client.profile] | 355 return iq_elt.elements(NS_COMMANDS, "command").next() |
330 except KeyError: | 356 except StopIteration: |
331 pass | 357 raise exceptions.NotFound(_(u"Missing command element")) |
332 | 358 |
333 def _items2XMLUI(self, items, no_instructions): | 359 def _items2XMLUI(self, items, no_instructions): |
334 """ Convert discovery items to XMLUI dialog """ | 360 """Convert discovery items to XMLUI dialog """ |
335 # TODO: manage items on different jids | 361 # TODO: manage items on different jids |
336 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) | 362 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) |
337 | 363 |
338 if not no_instructions: | 364 if not no_instructions: |
339 form_ui.addText(_("Please select a command"), "instructions") | 365 form_ui.addText(_("Please select a command"), "instructions") |
369 C.XMLUI_DATA_LVL_ERROR: "%s: " % _("ERROR"), | 395 C.XMLUI_DATA_LVL_ERROR: "%s: " % _("ERROR"), |
370 } | 396 } |
371 return [u"%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes] | 397 return [u"%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes] |
372 | 398 |
373 def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): | 399 def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): |
374 """ | 400 """Convert command answer to an ui for frontend |
375 Convert command answer to an ui for frontend | 401 |
376 @param iq_elt: command result | 402 @param iq_elt: command result |
377 @param session_id: id of the session used with the frontend | 403 @param session_id: id of the session used with the frontend |
378 @param profile_key: %(doc_profile_key)s | 404 @param profile_key: %(doc_profile_key)s |
379 | 405 """ |
380 """ | 406 command_elt = self.getCommandElt(iq_elt) |
381 command_elt = iq_elt.elements(NS_COMMANDS, "command").next() | |
382 status = command_elt.getAttribute("status", XEP_0050.STATUS.EXECUTING) | 407 status = command_elt.getAttribute("status", XEP_0050.STATUS.EXECUTING) |
383 if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]: | 408 if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]: |
384 # the command session is finished, we purge our session | 409 # the command session is finished, we purge our session |
385 del self.requesting[session_id] | 410 del self.requesting[session_id] |
386 if status == XEP_0050.STATUS.COMPLETED: | 411 if status == XEP_0050.STATUS.COMPLETED: |
449 d = self.requestingEntity(data, profile) | 474 d = self.requestingEntity(data, profile) |
450 d.addCallback(serialise) | 475 d.addCallback(serialise) |
451 return d | 476 return d |
452 | 477 |
453 def requestingEntity(self, data, profile): | 478 def requestingEntity(self, data, profile): |
454 """ | 479 """Request and entity and create XMLUI accordingly. |
455 request and entity and create XMLUI accordingly | 480 |
456 @param data: data returned by previous XMLUI (first one must come from self._commandsMenu) | 481 @param data: data returned by previous XMLUI (first one must come from |
482 self._commandsMenu) | |
457 @param profile: %(doc_profile)s | 483 @param profile: %(doc_profile)s |
458 @return: callback dict result (with "xmlui" corresponding to the answering dialog, or empty if it's finished without error) | 484 @return: callback dict result (with "xmlui" corresponding to the answering |
459 | 485 dialog, or empty if it's finished without error) |
460 """ | 486 """ |
461 if C.bool(data.get("cancelled", C.BOOL_FALSE)): | 487 if C.bool(data.get("cancelled", C.BOOL_FALSE)): |
462 return defer.succeed({}) | 488 return defer.succeed({}) |
489 data_form_values = xml_tools.XMLUIResult2DataFormResult(data) | |
463 client = self.host.getClient(profile) | 490 client = self.host.getClient(profile) |
464 # TODO: cancel, prev and next are not managed | 491 # TODO: cancel, prev and next are not managed |
465 # TODO: managed answerer errors | 492 # TODO: managed answerer errors |
466 # TODO: manage nodes with a non data form payload | 493 # TODO: manage nodes with a non data form payload |
467 if "session_id" not in data: | 494 if "session_id" not in data: |
468 # we just had the jid, we now request it for the available commands | 495 # we just had the jid, we now request it for the available commands |
469 session_id, session_data = self.requesting.newSession(profile=client.profile) | 496 session_id, session_data = self.requesting.newSession(profile=client.profile) |
470 entity = jid.JID(data[xml_tools.SAT_FORM_PREFIX + "jid"]) | 497 entity = jid.JID(data[xml_tools.SAT_FORM_PREFIX + "jid"]) |
471 session_data["jid"] = entity | 498 session_data["jid"] = entity |
472 d = self.list(client, entity) | 499 d = self.listUI(client, entity) |
473 | 500 |
474 def sendItems(xmlui): | 501 def sendItems(xmlui): |
475 xmlui.session_id = session_id # we need to keep track of the session | 502 xmlui.session_id = session_id # we need to keep track of the session |
476 return {"xmlui": xmlui} | 503 return {"xmlui": xmlui} |
477 | 504 |
491 try: | 518 try: |
492 session_data["node"] | 519 session_data["node"] |
493 # node has already been received | 520 # node has already been received |
494 except KeyError: | 521 except KeyError: |
495 # it's the first time we know the node, we save it in session data | 522 # it's the first time we know the node, we save it in session data |
496 session_data["node"] = data[xml_tools.SAT_FORM_PREFIX + "node"] | 523 session_data["node"] = data_form_values.pop("node") |
524 | |
525 # remote_id is the XEP_0050 sessionid used by answering command | |
526 # while session_id is our own session id used with the frontend | |
527 remote_id = session_data.get("remote_id") | |
497 | 528 |
498 # we request execute node's command | 529 # we request execute node's command |
499 iq_elt = compat.IQ(client.xmlstream, "set") | 530 d = self.do(client, entity, session_data["node"], action=XEP_0050.ACTION.EXECUTE, |
500 iq_elt["to"] = entity.full() | 531 session_id=remote_id, form_values=data_form_values) |
501 command_elt = iq_elt.addElement("command", NS_COMMANDS) | |
502 command_elt["node"] = session_data["node"] | |
503 command_elt["action"] = XEP_0050.ACTION.EXECUTE | |
504 try: | |
505 # remote_id is the XEP_0050 sessionid used by answering command | |
506 # while session_id is our own session id used with the frontend | |
507 command_elt["sessionid"] = session_data["remote_id"] | |
508 except KeyError: | |
509 pass | |
510 | |
511 command_elt.addChild( | |
512 xml_tools.XMLUIResultToElt(data) | |
513 ) # We add the XMLUI result to the command payload | |
514 d = iq_elt.send() | |
515 d.addCallback(self._commandsAnswer2XMLUI, session_id, session_data) | 532 d.addCallback(self._commandsAnswer2XMLUI, session_id, session_data) |
516 d.addCallback(lambda xmlui: {"xmlui": xmlui} if xmlui is not None else {}) | 533 d.addCallback(lambda xmlui: {"xmlui": xmlui} if xmlui is not None else {}) |
517 | 534 |
518 return d | 535 return d |
519 | 536 |
520 def _commandsMenu(self, menu_data, profile): | 537 def _commandsMenu(self, menu_data, profile): |
521 """ First XMLUI activated by menu: ask for target jid | 538 """First XMLUI activated by menu: ask for target jid |
539 | |
522 @param profile: %(doc_profile)s | 540 @param profile: %(doc_profile)s |
523 | |
524 """ | 541 """ |
525 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) | 542 form_ui = xml_tools.XMLUI("form", submit_id=self.__requesting_id) |
526 form_ui.addText(_("Please enter target jid"), "instructions") | 543 form_ui.addText(_("Please enter target jid"), "instructions") |
527 form_ui.changeContainer("pairs") | 544 form_ui.changeContainer("pairs") |
528 form_ui.addLabel("jid") | 545 form_ui.addLabel("jid") |
529 form_ui.addString("jid", value=self.host.getClient(profile).jid.host) | 546 form_ui.addString("jid", value=self.host.getClient(profile).jid.host) |
530 return {"xmlui": form_ui.toXml()} | 547 return {"xmlui": form_ui.toXml()} |
531 | 548 |
532 def _statusCallback(self, command_elt, session_data, action, node, profile): | 549 def _statusCallback(self, client, command_elt, session_data, action, node): |
533 """ Ad-hoc command used to change the "show" part of status """ | 550 """Ad-hoc command used to change the "show" part of status""" |
534 actions = session_data.setdefault("actions", []) | 551 actions = session_data.setdefault("actions", []) |
535 actions.append(action) | 552 actions.append(action) |
536 | 553 |
537 if len(actions) == 1: | 554 if len(actions) == 1: |
538 # it's our first request, we ask the desired new status | 555 # it's our first request, we ask the desired new status |
558 except (KeyError, StopIteration): | 575 except (KeyError, StopIteration): |
559 raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD) | 576 raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD) |
560 if show not in SHOWS: | 577 if show not in SHOWS: |
561 raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD) | 578 raise AdHocError(XEP_0050.ERROR.BAD_PAYLOAD) |
562 if show == "disconnect": | 579 if show == "disconnect": |
563 self.host.disconnect(profile) | 580 self.host.disconnect(client.profile) |
564 else: | 581 else: |
565 self.host.setPresence(show=show, profile_key=profile) | 582 self.host.setPresence(show=show, profile_key=client.profile) |
566 | 583 |
567 # job done, we can end the session | 584 # job done, we can end the session |
568 form = data_form.Form("form", title=_(u"Updated")) | 585 form = data_form.Form("form", title=_(u"Updated")) |
569 form.addField(data_form.Field("fixed", u"Status updated")) | 586 form.addField(data_form.Field("fixed", u"Status updated")) |
570 status = XEP_0050.STATUS.COMPLETED | 587 status = XEP_0050.STATUS.COMPLETED |
595 if service_jid is None: | 612 if service_jid is None: |
596 service_jid = jid.JID(client.jid.host) | 613 service_jid = jid.JID(client.jid.host) |
597 session_id, session_data = self.requesting.newSession(profile=client.profile) | 614 session_id, session_data = self.requesting.newSession(profile=client.profile) |
598 session_data["jid"] = service_jid | 615 session_data["jid"] = service_jid |
599 if node is None: | 616 if node is None: |
600 xmlui = yield self.list(client, service_jid) | 617 xmlui = yield self.listUI(client, service_jid) |
601 else: | 618 else: |
602 session_data["node"] = node | 619 session_data["node"] = node |
603 cb_data = yield self.requestingEntity( | 620 cb_data = yield self.requestingEntity( |
604 {"session_id": session_id}, client.profile | 621 {"session_id": session_id}, client.profile |
605 ) | 622 ) |
606 xmlui = cb_data["xmlui"] | 623 xmlui = cb_data["xmlui"] |
607 | 624 |
608 xmlui.session_id = session_id | 625 xmlui.session_id = session_id |
609 defer.returnValue(xmlui) | 626 defer.returnValue(xmlui) |
610 | 627 |
611 def _list(self, to_jid_s, profile_key): | 628 def list(self, client, to_jid): |
629 """Request available commands | |
630 | |
631 @param to_jid(jid.JID, None): the entity answering the commands | |
632 None to use profile's server | |
633 @return D(disco.DiscoItems): found commands | |
634 """ | |
635 d = self.host.getDiscoItems(client, to_jid, NS_COMMANDS) | |
636 return d | |
637 | |
638 def _listUI(self, to_jid_s, profile_key): | |
612 client = self.host.getClient(profile_key) | 639 client = self.host.getClient(profile_key) |
613 to_jid = jid.JID(to_jid_s) if to_jid_s else None | 640 to_jid = jid.JID(to_jid_s) if to_jid_s else None |
614 d = self.list(client, to_jid, no_instructions=True) | 641 d = self.listUI(client, to_jid, no_instructions=True) |
615 d.addCallback(lambda xmlui: xmlui.toXml()) | 642 d.addCallback(lambda xmlui: xmlui.toXml()) |
616 return d | 643 return d |
617 | 644 |
618 def list(self, client, to_jid, no_instructions=False): | 645 def listUI(self, client, to_jid, no_instructions=False): |
619 """Request available commands | 646 """Request available commands and generate XMLUI |
620 | 647 |
621 @param to_jid(jid.JID, None): the entity answering the commands | 648 @param to_jid(jid.JID, None): the entity answering the commands |
622 None to use profile's server | 649 None to use profile's server |
623 @param no_instructions(bool): if True, don't add instructions widget | 650 @param no_instructions(bool): if True, don't add instructions widget |
624 """ | 651 @return D(xml_tools.XMLUI): UI with the commands |
625 d = self.host.getDiscoItems(client, to_jid, NS_COMMANDS) | 652 """ |
653 d = self.list(client, to_jid) | |
626 d.addCallback(self._items2XMLUI, no_instructions) | 654 d.addCallback(self._items2XMLUI, no_instructions) |
627 return d | 655 return d |
628 | 656 |
629 def addAdHocCommand( | 657 def addAdHocCommand(self, client, callback, label, node=None, features=None, |
630 self, | 658 timeout=600, allowed_jids=None, allowed_groups=None, |
631 callback, | 659 allowed_magics=None, forbidden_jids=None, forbidden_groups=None, |
632 label, | 660 ): |
633 node=None, | |
634 features=None, | |
635 timeout=600, | |
636 allowed_jids=None, | |
637 allowed_groups=None, | |
638 allowed_magics=None, | |
639 forbidden_jids=None, | |
640 forbidden_groups=None, | |
641 profile_key=C.PROF_KEY_NONE, | |
642 ): | |
643 """Add an ad-hoc command for the current profile | 661 """Add an ad-hoc command for the current profile |
644 | 662 |
645 @param callback: method associated with this ad-hoc command which return the payload data (see AdHocCommand._sendAnswer), can return a deferred | 663 @param callback: method associated with this ad-hoc command which return the |
664 payload data (see AdHocCommand._sendAnswer), can return a | |
665 deferred | |
646 @param label: label associated with this command on the main menu | 666 @param label: label associated with this command on the main menu |
647 @param node: disco item node associated with this command. None to use autogenerated node | 667 @param node: disco item node associated with this command. None to use |
648 @param features: features associated with the payload (list of strings), usualy data form | 668 autogenerated node |
649 @param timeout: delay between two requests before canceling the session (in seconds) | 669 @param features: features associated with the payload (list of strings), usualy |
670 data form | |
671 @param timeout: delay between two requests before canceling the session (in | |
672 seconds) | |
650 @param allowed_jids: list of allowed entities | 673 @param allowed_jids: list of allowed entities |
651 @param allowed_groups: list of allowed roster groups | 674 @param allowed_groups: list of allowed roster groups |
652 @param allowed_magics: list of allowed magic keys, can be: | 675 @param allowed_magics: list of allowed magic keys, can be: |
653 @ALL@: allow everybody | 676 @ALL@: allow everybody |
654 @PROFILE_BAREJID@: allow only the jid of the profile | 677 @PROFILE_BAREJID@: allow only the jid of the profile |
655 @param forbidden_jids: black list of entities which can't access this command | 678 @param forbidden_jids: black list of entities which can't access this command |
656 @param forbidden_groups: black list of groups which can't access this command | 679 @param forbidden_groups: black list of groups which can't access this command |
657 @param profile_key: profile key associated with this command, @ALL@ means can be accessed with every profiles | |
658 @return: node of the added command, useful to remove the command later | 680 @return: node of the added command, useful to remove the command later |
659 """ | 681 """ |
660 # FIXME: "@ALL@" for profile_key seems useless and dangerous | 682 # FIXME: "@ALL@" for profile_key seems useless and dangerous |
661 | 683 |
662 if node is None: | 684 if node is None: |
674 if forbidden_jids is None: | 696 if forbidden_jids is None: |
675 forbidden_jids = [] | 697 forbidden_jids = [] |
676 if forbidden_groups is None: | 698 if forbidden_groups is None: |
677 forbidden_groups = [] | 699 forbidden_groups = [] |
678 | 700 |
679 for client in self.host.getClients(profile_key): | 701 # TODO: manage newly created/removed profiles |
680 # TODO: manage newly created/removed profiles | 702 _allowed_jids = ( |
681 _allowed_jids = ( | 703 (allowed_jids + [client.jid.userhostJID()]) |
682 (allowed_jids + [client.jid.userhostJID()]) | 704 if "@PROFILE_BAREJID@" in allowed_magics |
683 if "@PROFILE_BAREJID@" in allowed_magics | 705 else allowed_jids |
684 else allowed_jids | 706 ) |
685 ) | 707 ad_hoc_command = AdHocCommand( |
686 ad_hoc_command = AdHocCommand( | 708 callback, |
687 self, | 709 label, |
688 callback, | 710 node, |
689 label, | 711 features, |
690 node, | 712 timeout, |
691 features, | 713 _allowed_jids, |
692 timeout, | 714 allowed_groups, |
693 _allowed_jids, | 715 allowed_magics, |
694 allowed_groups, | 716 forbidden_jids, |
695 allowed_magics, | 717 forbidden_groups, |
696 forbidden_jids, | 718 ) |
697 forbidden_groups, | 719 ad_hoc_command.setHandlerParent(client) |
698 client, | 720 commands = client._XEP_0050_commands |
699 ) | 721 commands[node] = ad_hoc_command |
700 ad_hoc_command.setHandlerParent(client) | 722 |
701 profile_commands = self.answering.setdefault(client.profile, {}) | 723 def onCmdRequest(self, request, client): |
702 profile_commands[node] = ad_hoc_command | |
703 | |
704 def onCmdRequest(self, request, profile): | |
705 request.handled = True | 724 request.handled = True |
706 requestor = jid.JID(request["from"]) | 725 requestor = jid.JID(request["from"]) |
707 command_elt = request.elements(NS_COMMANDS, "command").next() | 726 command_elt = request.elements(NS_COMMANDS, "command").next() |
708 action = command_elt.getAttribute("action", self.ACTION.EXECUTE) | 727 action = command_elt.getAttribute("action", self.ACTION.EXECUTE) |
709 node = command_elt.getAttribute("node") | 728 node = command_elt.getAttribute("node") |
710 if not node: | 729 if not node: |
711 raise exceptions.DataError | 730 client.sendError(request, u"bad-request") |
731 return | |
712 sessionid = command_elt.getAttribute("sessionid") | 732 sessionid = command_elt.getAttribute("sessionid") |
733 commands = client._XEP_0050_commands | |
713 try: | 734 try: |
714 command = self.answering[profile][node] | 735 command = commands[node] |
715 except KeyError: | 736 except KeyError: |
716 raise exceptions.DataError | 737 client.sendError(request, u"item-not-found") |
738 return | |
717 command.onRequest(command_elt, requestor, action, sessionid) | 739 command.onRequest(command_elt, requestor, action, sessionid) |
718 | 740 |
719 | 741 |
720 class XEP_0050_handler(XMPPHandler): | 742 class XEP_0050_handler(XMPPHandler): |
721 implements(iwokkel.IDisco) | 743 implements(iwokkel.IDisco) |
722 | 744 |
723 def __init__(self, plugin_parent): | 745 def __init__(self, plugin_parent): |
724 self.plugin_parent = plugin_parent | 746 self.plugin_parent = plugin_parent |
725 | 747 |
748 @property | |
749 def client(self): | |
750 return self.parent | |
751 | |
726 def connectionInitialized(self): | 752 def connectionInitialized(self): |
727 self.xmlstream.addObserver( | 753 self.xmlstream.addObserver( |
728 CMD_REQUEST, self.plugin_parent.onCmdRequest, profile=self.parent.profile | 754 CMD_REQUEST, self.plugin_parent.onCmdRequest, client=self.parent |
729 ) | 755 ) |
730 | 756 |
731 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): | 757 def getDiscoInfo(self, requestor, target, nodeIdentifier=""): |
732 identities = [] | 758 identities = [] |
733 if nodeIdentifier == NS_COMMANDS and self.plugin_parent.answering.get( | 759 if nodeIdentifier == NS_COMMANDS and self.client._XEP_0050_commands: |
734 self.parent.profile | 760 # we only add the identity if we have registred commands |
735 ): # we only add the identity if we have registred commands | |
736 identities.append(ID_CMD_LIST) | 761 identities.append(ID_CMD_LIST) |
737 return [disco.DiscoFeature(NS_COMMANDS)] + identities | 762 return [disco.DiscoFeature(NS_COMMANDS)] + identities |
738 | 763 |
739 def getDiscoItems(self, requestor, target, nodeIdentifier=""): | 764 def getDiscoItems(self, requestor, target, nodeIdentifier=""): |
740 ret = [] | 765 ret = [] |
741 if nodeIdentifier == NS_COMMANDS: | 766 if nodeIdentifier == NS_COMMANDS: |
742 for command in self.plugin_parent.answering.get( | 767 commands = self.client._XEP_0050_commands |
743 self.parent.profile, {} | 768 for command in commands.values(): |
744 ).values(): | |
745 if command.isAuthorised(requestor): | 769 if command.isAuthorised(requestor): |
746 ret.append( | 770 ret.append( |
747 disco.DiscoItem(self.parent.jid, command.node, command.getName()) | 771 disco.DiscoItem(self.parent.jid, command.node, command.getName()) |
748 ) # TODO: manage name language | 772 ) # TODO: manage name language |
749 return ret | 773 return ret |