Mercurial > libervia-backend
comparison sat/plugins/plugin_xep_0050.py @ 3302:9d61ceeaa847
plugin XEP-0050: some modernisation + adHocSequence:
improved code by reordering imports, adding some type hints, using standard dict instead
of OrderedDict, etc.
New sequence method/adHocSequence bridge method allow to easily send a sequence of data to
a well known ad-hoc node.
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 19 Jun 2020 15:35:45 +0200 |
parents | 559a625a236b |
children | 53b229761c9d |
comparison
equal
deleted
inserted
replaced
3301:9d1c0feba048 | 3302:9d61ceeaa847 |
---|---|
15 # GNU Affero General Public License for more details. | 15 # GNU Affero General Public License for more details. |
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 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/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 | |
21 from collections import namedtuple | |
22 from uuid import uuid4 | |
23 from typing import List, Optional | |
24 | |
25 from zope.interface import implementer | |
26 from twisted.words.protocols.jabber import jid | |
27 from twisted.words.protocols import jabber | |
28 from twisted.words.protocols.jabber.xmlstream import XMPPHandler | |
29 from twisted.words.xish import domish | |
30 from twisted.internet import defer | |
31 from wokkel import disco, iwokkel, data_form | |
20 from sat.core.i18n import _, D_ | 32 from sat.core.i18n import _, D_ |
21 from sat.core.constants import Const as C | 33 from sat.core.constants import Const as C |
22 from sat.core.log import getLogger | 34 from sat.core.log import getLogger |
23 | 35 from sat.core.xmpp import SatXMPPEntity |
24 log = getLogger(__name__) | |
25 from twisted.words.protocols.jabber import jid | |
26 from twisted.words.protocols import jabber | |
27 from twisted.words.xish import domish | |
28 from twisted.internet import defer | |
29 from wokkel import disco, iwokkel, data_form | |
30 from sat.core import exceptions | 36 from sat.core import exceptions |
31 from sat.memory.memory import Sessions | 37 from sat.memory.memory import Sessions |
32 from uuid import uuid4 | |
33 from sat.tools import xml_tools | 38 from sat.tools import xml_tools |
34 | 39 from sat.tools.common import data_format |
35 from zope.interface import implementer | 40 |
36 | 41 |
37 try: | 42 log = getLogger(__name__) |
38 from twisted.words.protocols.xmlstream import XMPPHandler | 43 |
39 except ImportError: | |
40 from wokkel.subprotocols import XMPPHandler | |
41 | |
42 from collections import namedtuple | |
43 | |
44 try: | |
45 from collections import OrderedDict # only available from python 2.7 | |
46 except ImportError: | |
47 from ordereddict import OrderedDict | |
48 | 44 |
49 IQ_SET = '/iq[@type="set"]' | 45 IQ_SET = '/iq[@type="set"]' |
50 NS_COMMANDS = "http://jabber.org/protocol/commands" | 46 NS_COMMANDS = "http://jabber.org/protocol/commands" |
51 ID_CMD_LIST = disco.DiscoIdentity("automation", "command-list") | 47 ID_CMD_LIST = disco.DiscoIdentity("automation", "command-list") |
52 ID_CMD_NODE = disco.DiscoIdentity("automation", "command-node") | 48 ID_CMD_NODE = disco.DiscoIdentity("automation", "command-node") |
53 CMD_REQUEST = IQ_SET + '/command[@xmlns="' + NS_COMMANDS + '"]' | 49 CMD_REQUEST = IQ_SET + '/command[@xmlns="' + NS_COMMANDS + '"]' |
54 | 50 |
55 SHOWS = OrderedDict( | 51 SHOWS = { |
56 [ | 52 "default": _("Online"), |
57 ("default", _("Online")), | 53 "away": _("Away"), |
58 ("away", _("Away")), | 54 "chat": _("Free for chat"), |
59 ("chat", _("Free for chat")), | 55 "dnd": _("Do not disturb"), |
60 ("dnd", _("Do not disturb")), | 56 "xa": _("Left"), |
61 ("xa", _("Left")), | 57 "disconnect": _("Disconnect"), |
62 ("disconnect", _("Disconnect")), | 58 } |
63 ] | |
64 ) | |
65 | 59 |
66 PLUGIN_INFO = { | 60 PLUGIN_INFO = { |
67 C.PI_NAME: "Ad-Hoc Commands", | 61 C.PI_NAME: "Ad-Hoc Commands", |
68 C.PI_IMPORT_NAME: "XEP-0050", | 62 C.PI_IMPORT_NAME: "XEP-0050", |
69 C.PI_MODES: C.PLUG_MODE_BOTH, | 63 C.PI_MODES: C.PLUG_MODE_BOTH, |
206 if cmd_condition: | 200 if cmd_condition: |
207 error_elt = next(iq_elt.elements(None, "error")) | 201 error_elt = next(iq_elt.elements(None, "error")) |
208 error_elt.addElement(cmd_condition, NS_COMMANDS) | 202 error_elt.addElement(cmd_condition, NS_COMMANDS) |
209 self.client.send(iq_elt) | 203 self.client.send(iq_elt) |
210 del self.sessions[session_id] | 204 del self.sessions[session_id] |
205 | |
206 def _requestEb(self, failure_, request, session_id): | |
207 if failure_.check(AdHocError): | |
208 error_constant = failure.value.callback_error | |
209 else: | |
210 log.error(f"unexpected error while handling request: {failure_}") | |
211 error_constant = XEP_0050.ERROR.INTERNAL | |
212 | |
213 self._sendError(error_constant, session_id, request) | |
211 | 214 |
212 def onRequest(self, command_elt, requestor, action, session_id): | 215 def onRequest(self, command_elt, requestor, action, session_id): |
213 if not self.isAuthorised(requestor): | 216 if not self.isAuthorised(requestor): |
214 return self._sendError( | 217 return self._sendError( |
215 XEP_0050.ERROR.FORBIDDEN, session_id, command_elt.parent | 218 XEP_0050.ERROR.FORBIDDEN, session_id, command_elt.parent |
238 session_data, | 241 session_data, |
239 action, | 242 action, |
240 self.node, | 243 self.node, |
241 ) | 244 ) |
242 d.addCallback(self._sendAnswer, session_id, command_elt.parent) | 245 d.addCallback(self._sendAnswer, session_id, command_elt.parent) |
243 d.addErrback( | 246 d.addErrback(self._requestEb, command_elt.parent, session_id) |
244 lambda failure, request: self._sendError( | |
245 failure.value.callback_error, session_id, request | |
246 ), | |
247 command_elt.parent, | |
248 ) | |
249 | 247 |
250 | 248 |
251 class XEP_0050(object): | 249 class XEP_0050(object): |
252 STATUS = namedtuple("Status", ("EXECUTING", "COMPLETED", "CANCELED"))( | 250 STATUS = namedtuple("Status", ("EXECUTING", "COMPLETED", "CANCELED"))( |
253 "executing", "completed", "canceled" | 251 "executing", "completed", "canceled" |
301 in_sign="ss", | 299 in_sign="ss", |
302 out_sign="s", | 300 out_sign="s", |
303 method=self._listUI, | 301 method=self._listUI, |
304 async_=True, | 302 async_=True, |
305 ) | 303 ) |
304 host.bridge.addMethod( | |
305 "adHocSequence", | |
306 ".plugin", | |
307 in_sign="ssss", | |
308 out_sign="s", | |
309 method=self._sequence, | |
310 async_=True, | |
311 ) | |
306 self.__requesting_id = host.registerCallback( | 312 self.__requesting_id = host.registerCallback( |
307 self._requestingEntity, with_data=True | 313 self._requestingEntity, with_data=True |
308 ) | 314 ) |
309 host.importMenu( | 315 host.importMenu( |
310 (D_("Service"), D_("Commands")), | 316 (D_("Service"), D_("Commands")), |
332 @param action(unicode): one of XEP_0050.ACTION | 338 @param action(unicode): one of XEP_0050.ACTION |
333 @param session_id(unicode, None): id of the ad-hoc session | 339 @param session_id(unicode, None): id of the ad-hoc session |
334 None if no session is involved | 340 None if no session is involved |
335 @param form_values(dict, None): values to use to create command form | 341 @param form_values(dict, None): values to use to create command form |
336 values will be passed to data_form.Form.makeFields | 342 values will be passed to data_form.Form.makeFields |
337 @return | 343 @return: iq result element |
338 """ | 344 """ |
339 iq_elt = client.IQ(timeout=timeout) | 345 iq_elt = client.IQ(timeout=timeout) |
340 iq_elt["to"] = entity.full() | 346 iq_elt["to"] = entity.full() |
341 command_elt = iq_elt.addElement("command", NS_COMMANDS) | 347 command_elt = iq_elt.addElement("command", NS_COMMANDS) |
342 command_elt["node"] = node | 348 command_elt["node"] = node |
403 C.XMLUI_DATA_LVL_WARNING: "%s: " % _("WARNING"), | 409 C.XMLUI_DATA_LVL_WARNING: "%s: " % _("WARNING"), |
404 C.XMLUI_DATA_LVL_ERROR: "%s: " % _("ERROR"), | 410 C.XMLUI_DATA_LVL_ERROR: "%s: " % _("ERROR"), |
405 } | 411 } |
406 return ["%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes] | 412 return ["%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes] |
407 | 413 |
414 def parseCommandAnswer(self, iq_elt): | |
415 command_elt = self.getCommandElt(iq_elt) | |
416 data = {} | |
417 data["status"] = command_elt.getAttribute("status", XEP_0050.STATUS.EXECUTING) | |
418 data["session_id"] = command_elt.getAttribute("sessionid") | |
419 data["notes"] = notes = [] | |
420 for note_elt in command_elt.elements(NS_COMMANDS, "note"): | |
421 notes.append( | |
422 ( | |
423 self._getDataLvl(note_elt.getAttribute("type", "info")), | |
424 str(note_elt), | |
425 ) | |
426 ) | |
427 | |
428 return command_elt, data | |
429 | |
430 | |
408 def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): | 431 def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): |
409 """Convert command answer to an ui for frontend | 432 """Convert command answer to an ui for frontend |
410 | 433 |
411 @param iq_elt: command result | 434 @param iq_elt: command result |
412 @param session_id: id of the session used with the frontend | 435 @param session_id: id of the session used with the frontend |
413 @param profile_key: %(doc_profile_key)s | 436 @param profile_key: %(doc_profile_key)s |
414 """ | 437 """ |
415 command_elt = self.getCommandElt(iq_elt) | 438 command_elt, answer_data = self.parseCommandAnswer(iq_elt) |
416 status = command_elt.getAttribute("status", XEP_0050.STATUS.EXECUTING) | 439 status = answer_data["status"] |
417 if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]: | 440 if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]: |
418 # the command session is finished, we purge our session | 441 # the command session is finished, we purge our session |
419 del self.requesting[session_id] | 442 del self.requesting[session_id] |
420 if status == XEP_0050.STATUS.COMPLETED: | 443 if status == XEP_0050.STATUS.COMPLETED: |
421 session_id = None | 444 session_id = None |
422 else: | 445 else: |
423 return None | 446 return None |
424 remote_session_id = command_elt.getAttribute("sessionid") | 447 remote_session_id = answer_data["session_id"] |
425 if remote_session_id: | 448 if remote_session_id: |
426 session_data["remote_id"] = remote_session_id | 449 session_data["remote_id"] = remote_session_id |
427 notes = [] | 450 notes = answer_data["notes"] |
428 for note_elt in command_elt.elements(NS_COMMANDS, "note"): | |
429 notes.append( | |
430 ( | |
431 self._getDataLvl(note_elt.getAttribute("type", "info")), | |
432 str(note_elt), | |
433 ) | |
434 ) | |
435 for data_elt in command_elt.elements(data_form.NS_X_DATA, "x"): | 451 for data_elt in command_elt.elements(data_form.NS_X_DATA, "x"): |
436 if data_elt["type"] in ("form", "result"): | 452 if data_elt["type"] in ("form", "result"): |
437 break | 453 break |
438 else: | 454 else: |
439 # no matching data element found | 455 # no matching data element found |
606 d.addCallback(lambda xmlui: xmlui.toXml()) | 622 d.addCallback(lambda xmlui: xmlui.toXml()) |
607 return d | 623 return d |
608 | 624 |
609 @defer.inlineCallbacks | 625 @defer.inlineCallbacks |
610 def run(self, client, service_jid=None, node=None): | 626 def run(self, client, service_jid=None, node=None): |
611 """run an ad-hoc command | 627 """Run an ad-hoc command |
612 | 628 |
613 @param service_jid(jid.JID, None): jid of the ad-hoc service | 629 @param service_jid(jid.JID, None): jid of the ad-hoc service |
614 None to use profile's server | 630 None to use profile's server |
615 @param node(unicode, None): node of the ad-hoc commnad | 631 @param node(unicode, None): node of the ad-hoc commnad |
616 None to get initial list | 632 None to get initial list |
658 @return D(xml_tools.XMLUI): UI with the commands | 674 @return D(xml_tools.XMLUI): UI with the commands |
659 """ | 675 """ |
660 d = self.list(client, to_jid) | 676 d = self.list(client, to_jid) |
661 d.addCallback(self._items2XMLUI, no_instructions) | 677 d.addCallback(self._items2XMLUI, no_instructions) |
662 return d | 678 return d |
679 | |
680 def _sequence(self, sequence, node, service_jid_s="", profile_key=C.PROF_KEY_NONE): | |
681 sequence = data_format.deserialise(sequence, type_check=list) | |
682 client = self.host.getClient(profile_key) | |
683 service_jid = jid.JID(service_jid_s) if service_jid_s else None | |
684 d = defer.ensureDeferred(self.sequence(client, sequence, node, service_jid)) | |
685 d.addCallback(lambda data: data_format.serialise(data)) | |
686 return d | |
687 | |
688 async def sequence( | |
689 self, | |
690 client: SatXMPPEntity, | |
691 sequence: List[dict], | |
692 node: str, | |
693 service_jid: Optional[jid.JID] = None, | |
694 ) -> dict: | |
695 """Send a series of data to an ad-hoc service | |
696 | |
697 @param sequence: list of | |
698 @param node: node of the ad-hoc commnad | |
699 @param service_jid: jid of the ad-hoc service | |
700 None to use profile's server | |
701 @return: data received in final answer | |
702 """ | |
703 if service_jid is None: | |
704 service_jid = jid.JID(client.jid.host) | |
705 | |
706 session_id = None | |
707 | |
708 for data_to_send in sequence: | |
709 iq_result_elt = await self.do( | |
710 client, | |
711 service_jid, | |
712 node, | |
713 session_id=session_id, | |
714 form_values=data_to_send, | |
715 ) | |
716 __, answer_data = self.parseCommandAnswer(iq_result_elt) | |
717 session_id = answer_data.pop("session_id") | |
718 | |
719 return answer_data | |
663 | 720 |
664 def addAdHocCommand(self, client, callback, label, node=None, features=None, | 721 def addAdHocCommand(self, client, callback, label, node=None, features=None, |
665 timeout=600, allowed_jids=None, allowed_groups=None, | 722 timeout=600, allowed_jids=None, allowed_groups=None, |
666 allowed_magics=None, forbidden_jids=None, forbidden_groups=None, | 723 allowed_magics=None, forbidden_jids=None, forbidden_groups=None, |
667 ): | 724 ): |