# HG changeset patch # User Goffi # Date 1584118208 -3600 # Node ID 8418e0c83ed7974d76537bbafbb40f8c14a9e123 # Parent bfa1bde97f480df6f4cc6fcaa61af58c7a366bd0 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 diff -r bfa1bde97f48 -r 8418e0c83ed7 sat/plugins/plugin_misc_text_commands.py --- 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 . +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