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
This commit is contained in:
Goffi 2020-03-13 17:50:08 +01:00
commit f7fb2dd238

View file

@ -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 @@ class TextCommands(object):
@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 @@ class TextCommands(object):
).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 @@ class TextCommands(object):
# 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())
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
# 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())
mess_data["unparsed"] = msg[
1 + len(command) :
] # part not yet parsed of the message
def genericErrback(failure):
try:
cmd_data = self._commands[command]
except KeyError:
self.feedBack(
client,
_("Unknown command /%s. ") % command + self.HELP_SUGGESTION,
mess_data,
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:
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")
)
log.debug("text command help message")
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:
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