# HG changeset patch # User Goffi # Date 1592573745 -7200 # Node ID 9d61ceeaa847fc19644a7f682f3cacbb6a109794 # Parent 9d1c0feba0485fec15a9f442c8909b7bcbdde9df 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. diff -r 9d1c0feba048 -r 9d61ceeaa847 sat/plugins/plugin_xep_0050.py --- a/sat/plugins/plugin_xep_0050.py Fri Jun 19 14:56:45 2020 +0200 +++ b/sat/plugins/plugin_xep_0050.py Fri Jun 19 15:35:45 2020 +0200 @@ -17,34 +17,30 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from sat.core.i18n import _, D_ -from sat.core.constants import Const as C -from sat.core.log import getLogger -log = getLogger(__name__) +from collections import namedtuple +from uuid import uuid4 +from typing import List, Optional + +from zope.interface import implementer from twisted.words.protocols.jabber import jid from twisted.words.protocols import jabber +from twisted.words.protocols.jabber.xmlstream import XMPPHandler from twisted.words.xish import domish from twisted.internet import defer from wokkel import disco, iwokkel, data_form +from sat.core.i18n import _, D_ +from sat.core.constants import Const as C +from sat.core.log import getLogger +from sat.core.xmpp import SatXMPPEntity from sat.core import exceptions from sat.memory.memory import Sessions -from uuid import uuid4 from sat.tools import xml_tools - -from zope.interface import implementer +from sat.tools.common import data_format -try: - from twisted.words.protocols.xmlstream import XMPPHandler -except ImportError: - from wokkel.subprotocols import XMPPHandler -from collections import namedtuple +log = getLogger(__name__) -try: - from collections import OrderedDict # only available from python 2.7 -except ImportError: - from ordereddict import OrderedDict IQ_SET = '/iq[@type="set"]' NS_COMMANDS = "http://jabber.org/protocol/commands" @@ -52,16 +48,14 @@ ID_CMD_NODE = disco.DiscoIdentity("automation", "command-node") CMD_REQUEST = IQ_SET + '/command[@xmlns="' + NS_COMMANDS + '"]' -SHOWS = OrderedDict( - [ - ("default", _("Online")), - ("away", _("Away")), - ("chat", _("Free for chat")), - ("dnd", _("Do not disturb")), - ("xa", _("Left")), - ("disconnect", _("Disconnect")), - ] -) +SHOWS = { + "default": _("Online"), + "away": _("Away"), + "chat": _("Free for chat"), + "dnd": _("Do not disturb"), + "xa": _("Left"), + "disconnect": _("Disconnect"), +} PLUGIN_INFO = { C.PI_NAME: "Ad-Hoc Commands", @@ -209,6 +203,15 @@ self.client.send(iq_elt) del self.sessions[session_id] + def _requestEb(self, failure_, request, session_id): + if failure_.check(AdHocError): + error_constant = failure.value.callback_error + else: + log.error(f"unexpected error while handling request: {failure_}") + error_constant = XEP_0050.ERROR.INTERNAL + + self._sendError(error_constant, session_id, request) + def onRequest(self, command_elt, requestor, action, session_id): if not self.isAuthorised(requestor): return self._sendError( @@ -240,12 +243,7 @@ self.node, ) d.addCallback(self._sendAnswer, session_id, command_elt.parent) - d.addErrback( - lambda failure, request: self._sendError( - failure.value.callback_error, session_id, request - ), - command_elt.parent, - ) + d.addErrback(self._requestEb, command_elt.parent, session_id) class XEP_0050(object): @@ -303,6 +301,14 @@ method=self._listUI, async_=True, ) + host.bridge.addMethod( + "adHocSequence", + ".plugin", + in_sign="ssss", + out_sign="s", + method=self._sequence, + async_=True, + ) self.__requesting_id = host.registerCallback( self._requestingEntity, with_data=True ) @@ -334,7 +340,7 @@ None if no session is involved @param form_values(dict, None): values to use to create command form values will be passed to data_form.Form.makeFields - @return + @return: iq result element """ iq_elt = client.IQ(timeout=timeout) iq_elt["to"] = entity.full() @@ -405,6 +411,23 @@ } return ["%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes] + def parseCommandAnswer(self, iq_elt): + command_elt = self.getCommandElt(iq_elt) + data = {} + data["status"] = command_elt.getAttribute("status", XEP_0050.STATUS.EXECUTING) + data["session_id"] = command_elt.getAttribute("sessionid") + data["notes"] = notes = [] + for note_elt in command_elt.elements(NS_COMMANDS, "note"): + notes.append( + ( + self._getDataLvl(note_elt.getAttribute("type", "info")), + str(note_elt), + ) + ) + + return command_elt, data + + def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): """Convert command answer to an ui for frontend @@ -412,8 +435,8 @@ @param session_id: id of the session used with the frontend @param profile_key: %(doc_profile_key)s """ - command_elt = self.getCommandElt(iq_elt) - status = command_elt.getAttribute("status", XEP_0050.STATUS.EXECUTING) + command_elt, answer_data = self.parseCommandAnswer(iq_elt) + status = answer_data["status"] if status in [XEP_0050.STATUS.COMPLETED, XEP_0050.STATUS.CANCELED]: # the command session is finished, we purge our session del self.requesting[session_id] @@ -421,17 +444,10 @@ session_id = None else: return None - remote_session_id = command_elt.getAttribute("sessionid") + remote_session_id = answer_data["session_id"] if remote_session_id: session_data["remote_id"] = remote_session_id - notes = [] - for note_elt in command_elt.elements(NS_COMMANDS, "note"): - notes.append( - ( - self._getDataLvl(note_elt.getAttribute("type", "info")), - str(note_elt), - ) - ) + notes = answer_data["notes"] for data_elt in command_elt.elements(data_form.NS_X_DATA, "x"): if data_elt["type"] in ("form", "result"): break @@ -608,7 +624,7 @@ @defer.inlineCallbacks def run(self, client, service_jid=None, node=None): - """run an ad-hoc command + """Run an ad-hoc command @param service_jid(jid.JID, None): jid of the ad-hoc service None to use profile's server @@ -661,6 +677,47 @@ d.addCallback(self._items2XMLUI, no_instructions) return d + def _sequence(self, sequence, node, service_jid_s="", profile_key=C.PROF_KEY_NONE): + sequence = data_format.deserialise(sequence, type_check=list) + client = self.host.getClient(profile_key) + service_jid = jid.JID(service_jid_s) if service_jid_s else None + d = defer.ensureDeferred(self.sequence(client, sequence, node, service_jid)) + d.addCallback(lambda data: data_format.serialise(data)) + return d + + async def sequence( + self, + client: SatXMPPEntity, + sequence: List[dict], + node: str, + service_jid: Optional[jid.JID] = None, + ) -> dict: + """Send a series of data to an ad-hoc service + + @param sequence: list of + @param node: node of the ad-hoc commnad + @param service_jid: jid of the ad-hoc service + None to use profile's server + @return: data received in final answer + """ + if service_jid is None: + service_jid = jid.JID(client.jid.host) + + session_id = None + + for data_to_send in sequence: + iq_result_elt = await self.do( + client, + service_jid, + node, + session_id=session_id, + form_values=data_to_send, + ) + __, answer_data = self.parseCommandAnswer(iq_result_elt) + session_id = answer_data.pop("session_id") + + return answer_data + def addAdHocCommand(self, client, callback, label, node=None, features=None, timeout=600, allowed_jids=None, allowed_groups=None, allowed_magics=None, forbidden_jids=None, forbidden_groups=None,