diff --git a/sat/plugins/plugin_xep_0050.py b/sat/plugins/plugin_xep_0050.py index da1b6d32..f4cfb50c 100644 --- a/sat/plugins/plugin_xep_0050.py +++ b/sat/plugins/plugin_xep_0050.py @@ -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 sat.tools.common import data_format -from zope.interface import implementer -try: - from twisted.words.protocols.xmlstream import XMPPHandler -except ImportError: - from wokkel.subprotocols import XMPPHandler +log = getLogger(__name__) -from collections import namedtuple - -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_LIST = disco.DiscoIdentity("automation", "command-list") 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 @@ class AdHocCommand(XMPPHandler): 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 @@ class AdHocCommand(XMPPHandler): 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 @@ class XEP_0050(object): 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 @@ class XEP_0050(object): 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,26 +411,12 @@ class XEP_0050(object): } return ["%s%s" % (lvl_map[lvl], msg) for lvl, msg in notes] - def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): - """Convert command answer to an ui for frontend - - @param iq_elt: command result - @param session_id: id of the session used with the frontend - @param profile_key: %(doc_profile_key)s - """ + def parseCommandAnswer(self, iq_elt): command_elt = self.getCommandElt(iq_elt) - status = command_elt.getAttribute("status", XEP_0050.STATUS.EXECUTING) - 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] - if status == XEP_0050.STATUS.COMPLETED: - session_id = None - else: - return None - remote_session_id = command_elt.getAttribute("sessionid") - if remote_session_id: - session_data["remote_id"] = remote_session_id - notes = [] + 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( ( @@ -432,6 +424,30 @@ class XEP_0050(object): str(note_elt), ) ) + + return command_elt, data + + + def _commandsAnswer2XMLUI(self, iq_elt, session_id, session_data): + """Convert command answer to an ui for frontend + + @param iq_elt: command result + @param session_id: id of the session used with the frontend + @param profile_key: %(doc_profile_key)s + """ + 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] + if status == XEP_0050.STATUS.COMPLETED: + session_id = None + else: + return None + remote_session_id = answer_data["session_id"] + if remote_session_id: + session_data["remote_id"] = remote_session_id + 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 @@ class XEP_0050(object): @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 @@ class XEP_0050(object): 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,