From 4bed5b849446b1a8dcc77f351797c16a11d45b30 Mon Sep 17 00:00:00 2001 From: EXPLOSION <40616000+A5rocks@users.noreply.github.com> Date: Tue, 31 Dec 2019 14:16:30 +0900 Subject: [PATCH] Better Prefixes (#160) * Easily use multiple prefixes... Now onto programmatic fetching and conquering the world! * Updated a test and added some documentation. Now someone just needs to find a way to transition into multiple prefixes... * Allow bots to be run without access to the console. This is going to allow for custom prefix-getters :D * Fix flake8's innate hate of monkey patching. * Documentation! No idea how to make tests for this... * Python 2 compatibility is basically impossible. Implicit Relative Imports ruin the day here (`import logging`) and so why not ruin Python 2's day with type hinting? * And it's done! Python 2 **can** keep using disco, because `disco.util.runner` shouldn't be automatically imported. Changelog: + Added two keys to the config. `commands_prefix_getter` `commands_prefix` + Added documentation. + Added a new utility file, `disco.util.runner` * nekoka.tt code review (github.com/nekocatt) Some of the stuff edited (`disco.util.config.Config`) are not within the scope of this PR, but `disco.util.runner.create_bot` makes it easier to mess up. (also, if lines 414 to 417 are premature optimization, that's all me) * Undo the change to `disco.util.config.Config`. There is probably a way to do this, but that's topic of a different PR. * remove unused import * Python 2 Compatibility * Move `disco.util.runner.py` to another PR * b1nzy code review * fix tests * stop being dumb * Remove unintuitive behaviour. Previously, if you had `{@mention} {prefix}{command}`, `require_mention` was set to `True`, and `prefixes` were set to anything, then the command framework would look for a command named `{prefix}{command}`. However, this specific grouping of settings would not be chosen unless the bot creator wanted the command framework to look for `{command}`. tl;dr settings weren't being respected Co-authored-by: Andrei Zbikowski --- disco/bot/bot.py | 46 ++++++++++++++++++++++++++------ disco/util/config.py | 10 +++++++ docs/bot_tutorial/first_steps.md | 8 ++++++ tests/bot/bot.py | 13 +++++++-- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/disco/bot/bot.py b/disco/bot/bot.py index d354e29..2dc9646 100644 --- a/disco/bot/bot.py +++ b/disco/bot/bot.py @@ -45,7 +45,13 @@ class BotConfig(Config): command parsing. commands_prefix : str A string prefix that is required for a message to be considered for - command parsing. + command parsing. **DEPRECATED** + command_prefixes : list[string] + A list of string prefixes that are required for a message to be considered + for command parsing. + commands_prefix_getter : Optional[function] + A function which takes in a message object and returns an array of strings + (prefixes). commands_allow_edit : bool If true, the bot will re-parse an edited message if it was the last sent message in a channel, and did not previously trigger a command. This is @@ -74,6 +80,8 @@ class BotConfig(Config): http_port : int The port for the HTTP Flask server (if enabled). """ + deprecated = {'commands_prefix': 'command_prefixes'} + levels = {} plugins = [] plugin_config = {} @@ -87,7 +95,9 @@ class BotConfig(Config): 'role': True, 'user': True, } - commands_prefix = '' + commands_prefix = '' # now deprecated + command_prefixes = [] + commands_prefix_getter = None commands_allow_edit = True commands_level_getter = None commands_group_abbrev = True @@ -239,7 +249,7 @@ class Bot(LoggingClass): Computes all possible abbreviations for a command grouping. """ # For the first pass, we just want to compute each groups possible - # abbreviations that don't conflict with eachother. + # abbreviations that don't conflict with each other. possible = {} for group in groups: for index in range(1, len(group)): @@ -275,13 +285,20 @@ class Bot(LoggingClass): else: self.command_matches_re = None - def get_commands_for_message(self, require_mention, mention_rules, prefix, msg): + def get_commands_for_message(self, require_mention, mention_rules, prefixes, msg): """ Generator of all commands that a given message object triggers, based on the bots plugins and configuration. Parameters --------- + require_mention : bool + Checks if the message starts with a mention (and then ignores the prefix(es)) + mention_rules : dict(str, bool) + Whether `user`, `everyone`, and `role` mentions are allowed. Defaults to: + `{'user': True, 'everyone': False, 'role': False}` + prefixes : list[string] + A list of prefixes to check the message starts with. msg : :class:`disco.types.message.Message` The message object to parse and find matching commands for. @@ -290,6 +307,8 @@ class Bot(LoggingClass): tuple(:class:`disco.bot.command.Command`, `re.MatchObject`) All commands the message triggers. """ + # somebody better figure out what this yields... + content = msg.content if require_mention: @@ -326,10 +345,17 @@ class Bot(LoggingClass): content = content.lstrip() - if prefix and not content.startswith(prefix): - return [] + # Scan through the prefixes to find the first one that matches. + # This may lead to unexpected results, but said unexpectedness + # should be easy to avoid. An example of the unexpected results + # that may occur would be if one prefix was `!` and one was `!a`. + for prefix in prefixes: + if prefix and content.startswith(prefix): + content = content[len(prefix):] + break else: - content = content[len(prefix):] + if not require_mention: # don't want to prematurely return + return [] if not self.command_matches_re or not self.command_matches_re.match(content): return [] @@ -339,6 +365,7 @@ class Bot(LoggingClass): match = command.compiled_regex.match(content) if match: options.append((command, match)) + return sorted(options, key=lambda obj: obj[0].group is None) def get_level(self, actor): @@ -382,10 +409,13 @@ class Bot(LoggingClass): bool Whether any commands where successfully triggered by the message. """ + custom_message_prefixes = (self.config.commands_prefix_getter(msg) + if self.config.commands_prefix_getter else []) + commands = list(self.get_commands_for_message( self.config.commands_require_mention, self.config.commands_mention_rules, - self.config.commands_prefix, + custom_message_prefixes or self.config.command_prefixes, msg, )) diff --git a/disco/util/config.py b/disco/util/config.py index 30d2996..bed960c 100644 --- a/disco/util/config.py +++ b/disco/util/config.py @@ -10,6 +10,16 @@ class Config(object): k: getattr(self, k) for k in dir(self.__class__) }) + # issue `DeprecationWarning`s + if hasattr(self.__class__, 'deprecated') and obj: + for deprecated_key, replacement in self.__class__.deprecated.items(): + if deprecated_key in obj.keys(): + warning_text = '"{0}" is deprecated.'.format(deprecated_key) + warning_text += ('\nReplace "{0}" with "{1}".'.format(deprecated_key, replacement) + if replacement else '') + + raise DeprecationWarning(warning_text) + if obj: self.__dict__.update(obj) diff --git a/docs/bot_tutorial/first_steps.md b/docs/bot_tutorial/first_steps.md index 4bec43a..5ad77fe 100644 --- a/docs/bot_tutorial/first_steps.md +++ b/docs/bot_tutorial/first_steps.md @@ -34,6 +34,14 @@ Now let's setup the configuration file. To start off with we'll paste the follow } ``` +{% hint style='tip' %} +If you want to use a prefix (or even multiple), you add something this into the `"bot"` dictionary: +```json +"requires_mentions": false, +"command_prefixes": ["!", "?"] +``` +{% endhint %} + Now we're ready to write our plugin. Plugins are used to isolate the functionality of your bot into components. Plugins can be dynamically loaded, unloaded and reloaded at runtime. Lets start off by writing a plugin with a "ping" command; diff --git a/tests/bot/bot.py b/tests/bot/bot.py index 61149d5..3a8d7cb 100644 --- a/tests/bot/bot.py +++ b/tests/bot/bot.py @@ -85,10 +85,19 @@ class TestBot(TestCase): msg = Object() msg.content = '!test a' - commands = list(self.bot.get_commands_for_message(False, None, '!', msg)) + commands = list(self.bot.get_commands_for_message(False, None, ['!'], msg)) self.assertEqual(commands[0][0], self.bot._commands[1]) self.assertEqual(commands[1][0], self.bot._commands[0]) msg.content = '!test' - commands = list(self.bot.get_commands_for_message(False, None, '!', msg)) + commands = list(self.bot.get_commands_for_message(False, None, ['!'], msg)) + self.assertEqual(commands[0][0], self.bot._commands[0]) + + msg.content = '?test a' + commands = list(self.bot.get_commands_for_message(False, None, ['!', '?', ';'], msg)) + self.assertEqual(commands[0][0], self.bot._commands[1]) + self.assertEqual(commands[1][0], self.bot._commands[0]) + + msg.content = '?test' + commands = list(self.bot.get_commands_for_message(False, None, ['!', '?', ';'], msg)) self.assertEqual(commands[0][0], self.bot._commands[0])