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