changeset 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 9d1c0feba048
children f17379123571
files sat/plugins/plugin_xep_0050.py
diffstat 1 files changed, 102 insertions(+), 45 deletions(-) [+]
line wrap: on
line diff
--- 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 <http://www.gnu.org/licenses/>.
 
-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,