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 ):