changeset 3216:8418e0c83ed7

plugin text commands: handles coroutines + better command parsing: - async coroutine can now be used for text commands (in addition to Deferred and regular callbacks) - isidentifier is now used to detect a command, allowing to use "_" in command names - if message starts with a single `/`, it is never sent (previously it could be sent is some corner cases) - don't use OrderedDict anymore as dict now keep insertion order by default
author Goffi <goffi@goffi.org>
date Fri, 13 Mar 2020 17:50:08 +0100 (2020-03-13)
parents bfa1bde97f48
children 0469c53ed5dd
files sat/plugins/plugin_misc_text_commands.py
diffstat 1 files changed, 71 insertions(+), 62 deletions(-) [+]
line wrap: on
line diff
--- a/sat/plugins/plugin_misc_text_commands.py	Fri Mar 13 17:46:27 2020 +0100
+++ b/sat/plugins/plugin_misc_text_commands.py	Fri Mar 13 17:50:08 2020 +0100
@@ -17,16 +17,17 @@
 # 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 twisted.words.protocols.jabber import jid
+from twisted.internet import defer
+from twisted.python import failure
 from sat.core.i18n import _
 from sat.core.constants import Const as C
 from sat.core import exceptions
-from twisted.words.protocols.jabber import jid
-from twisted.internet import defer
 from sat.core.log import getLogger
+from sat.tools import utils
+
 
 log = getLogger(__name__)
-from twisted.python import failure
-from collections import OrderedDict
 
 PLUGIN_INFO = {
     C.PI_NAME: "Text commands",
@@ -81,10 +82,15 @@
         @return (dict): dictionary with parsed data where key can be:
             - "doc_short_help" (default: ""): the untranslated short documentation
             - "type" (default "all"): the command type as specified in documentation
-            - "args" (default: ""): the arguments available, using syntax specified in documentation.
+            - "args" (default: ""): the arguments available, using syntax specified in
+                documentation.
             - "doc_arg_[name]": the doc of [name] argument
         """
-        data = OrderedDict([("doc_short_help", ""), ("type", "all"), ("args", "")])
+        data = {
+            "doc_short_help": "",
+            "type": "all",
+            "args": "",
+        }
         docstring = cmd.__doc__
         if docstring is None:
             log.warning("No docstring found for command {}".format(cmd_name))
@@ -169,9 +175,7 @@
                         ).format(old_name=cmd_name, new_name=new_name)
                     )
                     cmd_name = new_name
-                self._commands[cmd_name] = cmd_data = OrderedDict(
-                    {"callback": cmd}
-                )  # We use an Ordered dict to keep documenation order
+                self._commands[cmd_name] = cmd_data = {"callback": cmd}
                 cmd_data.update(self._parseDocString(cmd, cmd_name))
                 log.info(_("Registered text command [%s]") % cmd_name)
 
@@ -233,65 +237,70 @@
 
         # we have a command
         d = None
-        command = msg[1:].partition(" ")[0].lower()
-        if command.isalpha():
-            # looks like an actual command, we try to call the corresponding method
-            def retHandling(ret):
-                """ Handle command return value:
-                if ret is True, normally send message (possibly modified by command)
-                else, abord message sending
-                """
-                if ret:
-                    return mess_data
-                else:
-                    log.debug("text command detected ({})".format(command))
-                    raise failure.Failure(exceptions.CancelError())
+        command = msg[1:].partition(" ")[0].lower().strip()
+        if not command.isidentifier():
+            self.feedBack(
+                client,
+                _("Invalid command /%s. ") % command + self.HELP_SUGGESTION,
+                mess_data,
+            )
+            raise failure.Failure(exceptions.CancelError())
+
+        # looks like an actual command, we try to call the corresponding method
+        def retHandling(ret):
+            """ Handle command return value:
+            if ret is True, normally send message (possibly modified by command)
+            else, abord message sending
+            """
+            if ret:
+                return mess_data
+            else:
+                log.debug("text command detected ({})".format(command))
+                raise failure.Failure(exceptions.CancelError())
+
+        def genericErrback(failure):
+            try:
+                msg = "with condition {}".format(failure.value.condition)
+            except AttributeError:
+                msg = "with error {}".format(failure.value)
+            self.feedBack(client, "Command failed {}".format(msg), mess_data)
+            return False
 
-            def genericErrback(failure):
-                try:
-                    msg = "with condition {}".format(failure.value.condition)
-                except AttributeError:
-                    msg = "with error {}".format(failure.value)
-                self.feedBack(client, "Command failed {}".format(msg), mess_data)
-                return False
-
-            mess_data["unparsed"] = msg[
-                1 + len(command) :
-            ]  # part not yet parsed of the message
-            try:
-                cmd_data = self._commands[command]
-            except KeyError:
+        mess_data["unparsed"] = msg[
+            1 + len(command) :
+        ]  # part not yet parsed of the message
+        try:
+            cmd_data = self._commands[command]
+        except KeyError:
+            self.feedBack(
+                client,
+                _("Unknown command /%s. ") % command + self.HELP_SUGGESTION,
+                mess_data,
+            )
+            log.debug("text command help message")
+            raise failure.Failure(exceptions.CancelError())
+        else:
+            if not self._contextValid(mess_data, cmd_data):
+                # The command is not launched in the right context, we throw a message with help instructions
+                context_txt = (
+                    _("group discussions")
+                    if cmd_data["type"] == "group"
+                    else _("one to one discussions")
+                )
+                feedback = _("/{command} command only applies in {context}.").format(
+                    command=command, context=context_txt
+                )
                 self.feedBack(
-                    client,
-                    _("Unknown command /%s. ") % command + self.HELP_SUGGESTION,
-                    mess_data,
+                    client, "{} {}".format(feedback, self.HELP_SUGGESTION), mess_data
                 )
-                log.debug("text command help message")
+                log.debug("text command invalid message")
                 raise failure.Failure(exceptions.CancelError())
             else:
-                if not self._contextValid(mess_data, cmd_data):
-                    # The command is not launched in the right context, we throw a message with help instructions
-                    context_txt = (
-                        _("group discussions")
-                        if cmd_data["type"] == "group"
-                        else _("one to one discussions")
-                    )
-                    feedback = _("/{command} command only applies in {context}.").format(
-                        command=command, context=context_txt
-                    )
-                    self.feedBack(
-                        client, "{} {}".format(feedback, self.HELP_SUGGESTION), mess_data
-                    )
-                    log.debug("text command invalid message")
-                    raise failure.Failure(exceptions.CancelError())
-                else:
-                    d = defer.maybeDeferred(cmd_data["callback"], client, mess_data)
-                    d.addErrback(genericErrback)
-                    d.addCallback(retHandling)
+                d = utils.asDeferred(cmd_data["callback"], client, mess_data)
+                d.addErrback(genericErrback)
+                d.addCallback(retHandling)
 
-        return (
-            d or mess_data
-        )  # if a command is detected, we should have a deferred, else we send the message normally
+        return d
 
     def _contextValid(self, mess_data, cmd_data):
         """Tell if a command can be used in the given context