comparison sat/plugins/plugin_xep_0050.py @ 3028:ab2696e34d29

Python 3 port: /!\ this is a huge commit /!\ starting from this commit, SàT is needs Python 3.6+ /!\ SàT maybe be instable or some feature may not work anymore, this will improve with time This patch port backend, bridge and frontends to Python 3. Roughly this has been done this way: - 2to3 tools has been applied (with python 3.7) - all references to python2 have been replaced with python3 (notably shebangs) - fixed files not handled by 2to3 (notably the shell script) - several manual fixes - fixed issues reported by Python 3 that where not handled in Python 2 - replaced "async" with "async_" when needed (it's a reserved word from Python 3.7) - replaced zope's "implements" with @implementer decorator - temporary hack to handle data pickled in database, as str or bytes may be returned, to be checked later - fixed hash comparison for password - removed some code which is not needed anymore with Python 3 - deactivated some code which needs to be checked (notably certificate validation) - tested with jp, fixed reported issues until some basic commands worked - ported Primitivus (after porting dependencies like urwid satext) - more manual fixes
author Goffi <goffi@goffi.org>
date Tue, 13 Aug 2019 19:08:41 +0200
parents a8c2d8b3453f
children 9d0df638c8b4
comparison
equal deleted inserted replaced
3027:ff5bcb12ae60 3028:ab2696e34d29
1 #!/usr/bin/env python2 1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*- 2 # -*- coding: utf-8 -*-
3 3
4 # SAT plugin for Ad-Hoc Commands (XEP-0050) 4 # SAT plugin for Ad-Hoc Commands (XEP-0050)
5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org) 5 # Copyright (C) 2009-2019 Jérôme Poisson (goffi@goffi.org)
6 6
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
35 from zope.interface import implements 35 from zope.interface import implementer
36 36
37 try: 37 try:
38 from twisted.words.protocols.xmlstream import XMPPHandler 38 from twisted.words.protocols.xmlstream import XMPPHandler
39 except ImportError: 39 except ImportError:
40 from wokkel.subprotocols import XMPPHandler 40 from wokkel.subprotocols import XMPPHandler
69 C.PI_MODES: C.PLUG_MODE_BOTH, 69 C.PI_MODES: C.PLUG_MODE_BOTH,
70 C.PI_TYPE: "XEP", 70 C.PI_TYPE: "XEP",
71 C.PI_PROTOCOLS: ["XEP-0050"], 71 C.PI_PROTOCOLS: ["XEP-0050"],
72 C.PI_MAIN: "XEP_0050", 72 C.PI_MAIN: "XEP_0050",
73 C.PI_HANDLER: "yes", 73 C.PI_HANDLER: "yes",
74 C.PI_DESCRIPTION: _(u"""Implementation of Ad-Hoc Commands"""), 74 C.PI_DESCRIPTION: _("""Implementation of Ad-Hoc Commands"""),
75 } 75 }
76 76
77 77
78 class AdHocError(Exception): 78 class AdHocError(Exception):
79 def __init__(self, error_const): 79 def __init__(self, error_const):
82 """ 82 """
83 assert error_const in XEP_0050.ERROR 83 assert error_const in XEP_0050.ERROR
84 self.callback_error = error_const 84 self.callback_error = error_const
85 85
86 86
87 @implementer(iwokkel.IDisco)
87 class AdHocCommand(XMPPHandler): 88 class AdHocCommand(XMPPHandler):
88 implements(iwokkel.IDisco)
89 89
90 def __init__(self, callback, label, node, features, timeout, 90 def __init__(self, callback, label, node, features, timeout,
91 allowed_jids, allowed_groups, allowed_magics, forbidden_jids, 91 allowed_jids, allowed_groups, allowed_magics, forbidden_jids,
92 forbidden_groups): 92 forbidden_groups):
93 XMPPHandler.__init__(self) 93 XMPPHandler.__init__(self)
128 allowed = set(self.allowed_jids) 128 allowed = set(self.allowed_jids)
129 for group in self.allowed_groups: 129 for group in self.allowed_groups:
130 try: 130 try:
131 allowed.update(self.client.roster.getJidsFromGroup(group)) 131 allowed.update(self.client.roster.getJidsFromGroup(group))
132 except exceptions.UnknownGroupError: 132 except exceptions.UnknownGroupError:
133 log.warning(_(u"The groups [{group}] is unknown for profile [{profile}])") 133 log.warning(_("The groups [{group}] is unknown for profile [{profile}])")
134 .format(group=group, profile=self.client.profile)) 134 .format(group=group, profile=self.client.profile))
135 if requestor.userhostJID() in allowed: 135 if requestor.userhostJID() in allowed:
136 return True 136 return True
137 return False 137 return False
138 138
202 @param request: original request (domish.Element) 202 @param request: original request (domish.Element)
203 """ 203 """
204 xmpp_condition, cmd_condition = error_constant 204 xmpp_condition, cmd_condition = error_constant
205 iq_elt = jabber.error.StanzaError(xmpp_condition).toResponse(request) 205 iq_elt = jabber.error.StanzaError(xmpp_condition).toResponse(request)
206 if cmd_condition: 206 if cmd_condition:
207 error_elt = iq_elt.elements(None, "error").next() 207 error_elt = next(iq_elt.elements(None, "error"))
208 error_elt.addElement(cmd_condition, NS_COMMANDS) 208 error_elt.addElement(cmd_condition, NS_COMMANDS)
209 self.client.send(iq_elt) 209 self.client.send(iq_elt)
210 del self.sessions[session_id] 210 del self.sessions[session_id]
211 211
212 def onRequest(self, command_elt, requestor, action, session_id): 212 def onRequest(self, command_elt, requestor, action, session_id):
291 "adHocRun", 291 "adHocRun",
292 ".plugin", 292 ".plugin",
293 in_sign="sss", 293 in_sign="sss",
294 out_sign="s", 294 out_sign="s",
295 method=self._run, 295 method=self._run,
296 async=True, 296 async_=True,
297 ) 297 )
298 host.bridge.addMethod( 298 host.bridge.addMethod(
299 "adHocList", 299 "adHocList",
300 ".plugin", 300 ".plugin",
301 in_sign="ss", 301 in_sign="ss",
302 out_sign="s", 302 out_sign="s",
303 method=self._listUI, 303 method=self._listUI,
304 async=True, 304 async_=True,
305 ) 305 )
306 self.__requesting_id = host.registerCallback( 306 self.__requesting_id = host.registerCallback(
307 self._requestingEntity, with_data=True 307 self._requestingEntity, with_data=True
308 ) 308 )
309 host.importMenu( 309 host.importMenu(
310 (D_("Service"), D_("Commands")), 310 (D_("Service"), D_("Commands")),
311 self._commandsMenu, 311 self._commandsMenu,
312 security_limit=2, 312 security_limit=2,
313 help_string=D_("Execute ad-hoc commands"), 313 help_string=D_("Execute ad-hoc commands"),
314 ) 314 )
315 host.registerNamespace(u'commands', NS_COMMANDS) 315 host.registerNamespace('commands', NS_COMMANDS)
316 316
317 def getHandler(self, client): 317 def getHandler(self, client):
318 return XEP_0050_handler(self) 318 return XEP_0050_handler(self)
319 319
320 def profileConnected(self, client): 320 def profileConnected(self, client):
352 d = iq_elt.send() 352 d = iq_elt.send()
353 return d 353 return d
354 354
355 def getCommandElt(self, iq_elt): 355 def getCommandElt(self, iq_elt):
356 try: 356 try:
357 return iq_elt.elements(NS_COMMANDS, "command").next() 357 return next(iq_elt.elements(NS_COMMANDS, "command"))
358 except StopIteration: 358 except StopIteration:
359 raise exceptions.NotFound(_(u"Missing command element")) 359 raise exceptions.NotFound(_("Missing command element"))
360 360
361 def adHocError(self, error_type): 361 def adHocError(self, error_type):
362 """Shortcut to raise an AdHocError 362 """Shortcut to raise an AdHocError
363 363
364 @param error_type(unicode): one of XEP_0050.ERROR 364 @param error_type(unicode): one of XEP_0050.ERROR
387 return C.XMLUI_DATA_LVL_ERROR 387 return C.XMLUI_DATA_LVL_ERROR
388 elif type_ == "warn": 388 elif type_ == "warn":
389 return C.XMLUI_DATA_LVL_WARNING 389 return C.XMLUI_DATA_LVL_WARNING
390 else: 390 else:
391 if type_ != "info": 391 if type_ != "info":
392 log.warning(_(u"Invalid note type [%s], using info") % type_) 392 log.warning(_("Invalid note type [%s], using info") % type_)
393 return C.XMLUI_DATA_LVL_INFO 393 return C.XMLUI_DATA_LVL_INFO
394 394
395 def _mergeNotes(self, notes): 395 def _mergeNotes(self, notes):
396 """Merge notes with level prefix (e.g. "ERROR: the message") 396 """Merge notes with level prefix (e.g. "ERROR: the message")
397 397
401 lvl_map = { 401 lvl_map = {
402 C.XMLUI_DATA_LVL_INFO: "", 402 C.XMLUI_DATA_LVL_INFO: "",
403 C.XMLUI_DATA_LVL_WARNING: "%s: " % _("WARNING"), 403 C.XMLUI_DATA_LVL_WARNING: "%s: " % _("WARNING"),
404 C.XMLUI_DATA_LVL_ERROR: "%s: " % _("ERROR"), 404 C.XMLUI_DATA_LVL_ERROR: "%s: " % _("ERROR"),
405 } 405 }
406 return [u"%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes] 406 return ["%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes]
407 407
408 def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): 408 def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data):
409 """Convert command answer to an ui for frontend 409 """Convert command answer to an ui for frontend
410 410
411 @param iq_elt: command result 411 @param iq_elt: command result
427 notes = [] 427 notes = []
428 for note_elt in command_elt.elements(NS_COMMANDS, "note"): 428 for note_elt in command_elt.elements(NS_COMMANDS, "note"):
429 notes.append( 429 notes.append(
430 ( 430 (
431 self._getDataLvl(note_elt.getAttribute("type", "info")), 431 self._getDataLvl(note_elt.getAttribute("type", "info")),
432 unicode(note_elt), 432 str(note_elt),
433 ) 433 )
434 ) 434 )
435 for data_elt in command_elt.elements(data_form.NS_X_DATA, "x"): 435 for data_elt in command_elt.elements(data_form.NS_X_DATA, "x"):
436 if data_elt["type"] in ("form", "result"): 436 if data_elt["type"] in ("form", "result"):
437 break 437 break
459 dlg_level = notes[0][0] if len(notes) == 1 else C.XMLUI_DATA_LVL_INFO 459 dlg_level = notes[0][0] if len(notes) == 1 else C.XMLUI_DATA_LVL_INFO
460 return xml_tools.XMLUI( 460 return xml_tools.XMLUI(
461 C.XMLUI_DIALOG, 461 C.XMLUI_DIALOG,
462 dialog_opt={ 462 dialog_opt={
463 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE, 463 C.XMLUI_DATA_TYPE: C.XMLUI_DIALOG_NOTE,
464 C.XMLUI_DATA_MESS: u"\n".join(self._mergeNotes(notes)), 464 C.XMLUI_DATA_MESS: "\n".join(self._mergeNotes(notes)),
465 C.XMLUI_DATA_LVL: dlg_level, 465 C.XMLUI_DATA_LVL: dlg_level,
466 }, 466 },
467 session_id=session_id, 467 session_id=session_id,
468 ) 468 )
469 469
563 if len(actions) == 1: 563 if len(actions) == 1:
564 # it's our first request, we ask the desired new status 564 # it's our first request, we ask the desired new status
565 status = XEP_0050.STATUS.EXECUTING 565 status = XEP_0050.STATUS.EXECUTING
566 form = data_form.Form("form", title=_("status selection")) 566 form = data_form.Form("form", title=_("status selection"))
567 show_options = [ 567 show_options = [
568 data_form.Option(name, label) for name, label in SHOWS.items() 568 data_form.Option(name, label) for name, label in list(SHOWS.items())
569 ] 569 ]
570 field = data_form.Field( 570 field = data_form.Field(
571 "list-single", "show", options=show_options, required=True 571 "list-single", "show", options=show_options, required=True
572 ) 572 )
573 form.addField(field) 573 form.addField(field)
576 note = None 576 note = None
577 577
578 elif len(actions) == 2: 578 elif len(actions) == 2:
579 # we should have the answer here 579 # we should have the answer here
580 try: 580 try:
581 x_elt = command_elt.elements(data_form.NS_X_DATA, "x").next() 581 x_elt = next(command_elt.elements(data_form.NS_X_DATA, "x"))
582 answer_form = data_form.Form.fromElement(x_elt) 582 answer_form = data_form.Form.fromElement(x_elt)
583 show = answer_form["show"] 583 show = answer_form["show"]
584 except (KeyError, StopIteration): 584 except (KeyError, StopIteration):
585 self.adHocError(XEP_0050.ERROR.BAD_PAYLOAD) 585 self.adHocError(XEP_0050.ERROR.BAD_PAYLOAD)
586 if show not in SHOWS: 586 if show not in SHOWS:
591 self.host.setPresence(show=show, profile_key=client.profile) 591 self.host.setPresence(show=show, profile_key=client.profile)
592 592
593 # job done, we can end the session 593 # job done, we can end the session
594 status = XEP_0050.STATUS.COMPLETED 594 status = XEP_0050.STATUS.COMPLETED
595 payload = None 595 payload = None
596 note = (self.NOTE.INFO, _(u"Status updated")) 596 note = (self.NOTE.INFO, _("Status updated"))
597 else: 597 else:
598 self.adHocError(XEP_0050.ERROR.INTERNAL) 598 self.adHocError(XEP_0050.ERROR.INTERNAL)
599 599
600 return (payload, status, None, note) 600 return (payload, status, None, note)
601 601
728 commands[node] = ad_hoc_command 728 commands[node] = ad_hoc_command
729 729
730 def onCmdRequest(self, request, client): 730 def onCmdRequest(self, request, client):
731 request.handled = True 731 request.handled = True
732 requestor = jid.JID(request["from"]) 732 requestor = jid.JID(request["from"])
733 command_elt = request.elements(NS_COMMANDS, "command").next() 733 command_elt = next(request.elements(NS_COMMANDS, "command"))
734 action = command_elt.getAttribute("action", self.ACTION.EXECUTE) 734 action = command_elt.getAttribute("action", self.ACTION.EXECUTE)
735 node = command_elt.getAttribute("node") 735 node = command_elt.getAttribute("node")
736 if not node: 736 if not node:
737 client.sendError(request, u"bad-request") 737 client.sendError(request, "bad-request")
738 return 738 return
739 sessionid = command_elt.getAttribute("sessionid") 739 sessionid = command_elt.getAttribute("sessionid")
740 commands = client._XEP_0050_commands 740 commands = client._XEP_0050_commands
741 try: 741 try:
742 command = commands[node] 742 command = commands[node]
743 except KeyError: 743 except KeyError:
744 client.sendError(request, u"item-not-found") 744 client.sendError(request, "item-not-found")
745 return 745 return
746 command.onRequest(command_elt, requestor, action, sessionid) 746 command.onRequest(command_elt, requestor, action, sessionid)
747 747
748 748
749 @implementer(iwokkel.IDisco)
749 class XEP_0050_handler(XMPPHandler): 750 class XEP_0050_handler(XMPPHandler):
750 implements(iwokkel.IDisco)
751 751
752 def __init__(self, plugin_parent): 752 def __init__(self, plugin_parent):
753 self.plugin_parent = plugin_parent 753 self.plugin_parent = plugin_parent
754 754
755 @property 755 @property
770 770
771 def getDiscoItems(self, requestor, target, nodeIdentifier=""): 771 def getDiscoItems(self, requestor, target, nodeIdentifier=""):
772 ret = [] 772 ret = []
773 if nodeIdentifier == NS_COMMANDS: 773 if nodeIdentifier == NS_COMMANDS:
774 commands = self.client._XEP_0050_commands 774 commands = self.client._XEP_0050_commands
775 for command in commands.values(): 775 for command in list(commands.values()):
776 if command.isAuthorised(requestor): 776 if command.isAuthorised(requestor):
777 ret.append( 777 ret.append(
778 disco.DiscoItem(self.parent.jid, command.node, command.getName()) 778 disco.DiscoItem(self.parent.jid, command.node, command.getName())
779 ) # TODO: manage name language 779 ) # TODO: manage name language
780 return ret 780 return ret