Mercurial > libervia-backend
comparison sat/plugins/plugin_misc_text_commands.py @ 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 |
parents | 0318802dfe28 |
children | be6d91572633 |
comparison
equal
deleted
inserted
replaced
3215:bfa1bde97f48 | 3216:8418e0c83ed7 |
---|---|
15 # GNU Affero General Public License for more details. | 15 # GNU Affero General Public License for more details. |
16 | 16 |
17 # You should have received a copy of the GNU Affero General Public License | 17 # You should have received a copy of the GNU Affero General Public License |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 18 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
19 | 19 |
20 from twisted.words.protocols.jabber import jid | |
21 from twisted.internet import defer | |
22 from twisted.python import failure | |
20 from sat.core.i18n import _ | 23 from sat.core.i18n import _ |
21 from sat.core.constants import Const as C | 24 from sat.core.constants import Const as C |
22 from sat.core import exceptions | 25 from sat.core import exceptions |
23 from twisted.words.protocols.jabber import jid | |
24 from twisted.internet import defer | |
25 from sat.core.log import getLogger | 26 from sat.core.log import getLogger |
27 from sat.tools import utils | |
28 | |
26 | 29 |
27 log = getLogger(__name__) | 30 log = getLogger(__name__) |
28 from twisted.python import failure | |
29 from collections import OrderedDict | |
30 | 31 |
31 PLUGIN_INFO = { | 32 PLUGIN_INFO = { |
32 C.PI_NAME: "Text commands", | 33 C.PI_NAME: "Text commands", |
33 C.PI_IMPORT_NAME: C.TEXT_CMDS, | 34 C.PI_IMPORT_NAME: C.TEXT_CMDS, |
34 C.PI_TYPE: "Misc", | 35 C.PI_TYPE: "Misc", |
79 - @command keyword can be used, | 80 - @command keyword can be used, |
80 see http://wiki.goffi.org/wiki/Coding_style/en for documentation | 81 see http://wiki.goffi.org/wiki/Coding_style/en for documentation |
81 @return (dict): dictionary with parsed data where key can be: | 82 @return (dict): dictionary with parsed data where key can be: |
82 - "doc_short_help" (default: ""): the untranslated short documentation | 83 - "doc_short_help" (default: ""): the untranslated short documentation |
83 - "type" (default "all"): the command type as specified in documentation | 84 - "type" (default "all"): the command type as specified in documentation |
84 - "args" (default: ""): the arguments available, using syntax specified in documentation. | 85 - "args" (default: ""): the arguments available, using syntax specified in |
86 documentation. | |
85 - "doc_arg_[name]": the doc of [name] argument | 87 - "doc_arg_[name]": the doc of [name] argument |
86 """ | 88 """ |
87 data = OrderedDict([("doc_short_help", ""), ("type", "all"), ("args", "")]) | 89 data = { |
90 "doc_short_help": "", | |
91 "type": "all", | |
92 "args": "", | |
93 } | |
88 docstring = cmd.__doc__ | 94 docstring = cmd.__doc__ |
89 if docstring is None: | 95 if docstring is None: |
90 log.warning("No docstring found for command {}".format(cmd_name)) | 96 log.warning("No docstring found for command {}".format(cmd_name)) |
91 docstring = "" | 97 docstring = "" |
92 | 98 |
167 _( | 173 _( |
168 "Conflict for command [{old_name}], renaming it to [{new_name}]" | 174 "Conflict for command [{old_name}], renaming it to [{new_name}]" |
169 ).format(old_name=cmd_name, new_name=new_name) | 175 ).format(old_name=cmd_name, new_name=new_name) |
170 ) | 176 ) |
171 cmd_name = new_name | 177 cmd_name = new_name |
172 self._commands[cmd_name] = cmd_data = OrderedDict( | 178 self._commands[cmd_name] = cmd_data = {"callback": cmd} |
173 {"callback": cmd} | |
174 ) # We use an Ordered dict to keep documenation order | |
175 cmd_data.update(self._parseDocString(cmd, cmd_name)) | 179 cmd_data.update(self._parseDocString(cmd, cmd_name)) |
176 log.info(_("Registered text command [%s]") % cmd_name) | 180 log.info(_("Registered text command [%s]") % cmd_name) |
177 | 181 |
178 def addWhoIsCb(self, callback, priority=0): | 182 def addWhoIsCb(self, callback, priority=0): |
179 """Add a callback which give information to the /whois command | 183 """Add a callback which give information to the /whois command |
231 except IndexError: | 235 except IndexError: |
232 return mess_data | 236 return mess_data |
233 | 237 |
234 # we have a command | 238 # we have a command |
235 d = None | 239 d = None |
236 command = msg[1:].partition(" ")[0].lower() | 240 command = msg[1:].partition(" ")[0].lower().strip() |
237 if command.isalpha(): | 241 if not command.isidentifier(): |
238 # looks like an actual command, we try to call the corresponding method | 242 self.feedBack( |
239 def retHandling(ret): | 243 client, |
240 """ Handle command return value: | 244 _("Invalid command /%s. ") % command + self.HELP_SUGGESTION, |
241 if ret is True, normally send message (possibly modified by command) | 245 mess_data, |
242 else, abord message sending | 246 ) |
243 """ | 247 raise failure.Failure(exceptions.CancelError()) |
244 if ret: | 248 |
245 return mess_data | 249 # looks like an actual command, we try to call the corresponding method |
246 else: | 250 def retHandling(ret): |
247 log.debug("text command detected ({})".format(command)) | 251 """ Handle command return value: |
248 raise failure.Failure(exceptions.CancelError()) | 252 if ret is True, normally send message (possibly modified by command) |
249 | 253 else, abord message sending |
250 def genericErrback(failure): | 254 """ |
251 try: | 255 if ret: |
252 msg = "with condition {}".format(failure.value.condition) | 256 return mess_data |
253 except AttributeError: | 257 else: |
254 msg = "with error {}".format(failure.value) | 258 log.debug("text command detected ({})".format(command)) |
255 self.feedBack(client, "Command failed {}".format(msg), mess_data) | 259 raise failure.Failure(exceptions.CancelError()) |
256 return False | 260 |
257 | 261 def genericErrback(failure): |
258 mess_data["unparsed"] = msg[ | |
259 1 + len(command) : | |
260 ] # part not yet parsed of the message | |
261 try: | 262 try: |
262 cmd_data = self._commands[command] | 263 msg = "with condition {}".format(failure.value.condition) |
263 except KeyError: | 264 except AttributeError: |
265 msg = "with error {}".format(failure.value) | |
266 self.feedBack(client, "Command failed {}".format(msg), mess_data) | |
267 return False | |
268 | |
269 mess_data["unparsed"] = msg[ | |
270 1 + len(command) : | |
271 ] # part not yet parsed of the message | |
272 try: | |
273 cmd_data = self._commands[command] | |
274 except KeyError: | |
275 self.feedBack( | |
276 client, | |
277 _("Unknown command /%s. ") % command + self.HELP_SUGGESTION, | |
278 mess_data, | |
279 ) | |
280 log.debug("text command help message") | |
281 raise failure.Failure(exceptions.CancelError()) | |
282 else: | |
283 if not self._contextValid(mess_data, cmd_data): | |
284 # The command is not launched in the right context, we throw a message with help instructions | |
285 context_txt = ( | |
286 _("group discussions") | |
287 if cmd_data["type"] == "group" | |
288 else _("one to one discussions") | |
289 ) | |
290 feedback = _("/{command} command only applies in {context}.").format( | |
291 command=command, context=context_txt | |
292 ) | |
264 self.feedBack( | 293 self.feedBack( |
265 client, | 294 client, "{} {}".format(feedback, self.HELP_SUGGESTION), mess_data |
266 _("Unknown command /%s. ") % command + self.HELP_SUGGESTION, | 295 ) |
267 mess_data, | 296 log.debug("text command invalid message") |
268 ) | |
269 log.debug("text command help message") | |
270 raise failure.Failure(exceptions.CancelError()) | 297 raise failure.Failure(exceptions.CancelError()) |
271 else: | 298 else: |
272 if not self._contextValid(mess_data, cmd_data): | 299 d = utils.asDeferred(cmd_data["callback"], client, mess_data) |
273 # The command is not launched in the right context, we throw a message with help instructions | 300 d.addErrback(genericErrback) |
274 context_txt = ( | 301 d.addCallback(retHandling) |
275 _("group discussions") | 302 |
276 if cmd_data["type"] == "group" | 303 return d |
277 else _("one to one discussions") | |
278 ) | |
279 feedback = _("/{command} command only applies in {context}.").format( | |
280 command=command, context=context_txt | |
281 ) | |
282 self.feedBack( | |
283 client, "{} {}".format(feedback, self.HELP_SUGGESTION), mess_data | |
284 ) | |
285 log.debug("text command invalid message") | |
286 raise failure.Failure(exceptions.CancelError()) | |
287 else: | |
288 d = defer.maybeDeferred(cmd_data["callback"], client, mess_data) | |
289 d.addErrback(genericErrback) | |
290 d.addCallback(retHandling) | |
291 | |
292 return ( | |
293 d or mess_data | |
294 ) # if a command is detected, we should have a deferred, else we send the message normally | |
295 | 304 |
296 def _contextValid(self, mess_data, cmd_data): | 305 def _contextValid(self, mess_data, cmd_data): |
297 """Tell if a command can be used in the given context | 306 """Tell if a command can be used in the given context |
298 | 307 |
299 @param mess_data(dict): message data as given in sendMessage trigger | 308 @param mess_data(dict): message data as given in sendMessage trigger |