diff --git a/edmond/bot.py b/edmond/bot.py index 020ab0b..924f214 100644 --- a/edmond/bot.py +++ b/edmond/bot.py @@ -95,7 +95,7 @@ class Bot(irc.client.SimpleIRCClient, Logger): self.run_plugin_callbacks(event) def on_privmsg(self, connection, event): - """Handle a message received privately.""" + """Handle a message received privately, usually like a channel msg.""" nick = NickMask(event.source).nick target = event.target message = event.arguments[0] diff --git a/edmond/plugin.py b/edmond/plugin.py index 82760dc..95128bb 100644 --- a/edmond/plugin.py +++ b/edmond/plugin.py @@ -1,6 +1,8 @@ from dataclasses import dataclass from pathlib import Path +from irc.client import NickMask + class Plugin: """Base class for the bot plugins. @@ -17,15 +19,15 @@ class Plugin: available after a restart. Initalisation should be very fast, no network connections or anything. They - are initialised before connecting to the server, so their `is_ready` flag is - set at that point. The loading order is more or less random, so a plugin + are initialised before connecting to the server, so their `is_ready` flag + is set at that point. The loading order is more or less random, so a plugin cannot assume another has been loaded during initialisation. If it wants to interact with another plugin, the earliest point to do that is in the on_welcome callback which is called after connecting to a server, and can disable itself by setting its own `is_ready` flag to false. - A plugin can access its config once the base `__init__` has been called. The - configuration is valid only is `is_ready` is True, else accessing its + A plugin can access its config once the base `__init__` has been called. + The configuration is valid only is `is_ready` is True, else accessing its content is undefined behaviour. Plugins can have priorities and calling their callbacks will respect it. @@ -57,19 +59,19 @@ class Plugin: } def __get_config(self): - """Return the plugin section from the bot config, plus common values.""" + """Return the plugin section from the bot config plus common values.""" plugins_configs = self.bot.config["plugins"] config = plugins_configs["common"].copy() config.update(plugins_configs.get(self.name, {})) # Load resources as config values. - resources_dir = self.bot.config["resources_dir"] - for key, resource_path in config.get("resources", {}).items(): - resource_path = Path(resources_dir) / resource_path + res_dir = self.bot.config["resources_dir"] + for key, res_path in config.get("resources", {}).items(): + res_path = Path(res_dir) / res_path try: - with open(resource_path, "rt") as resource_file: - config[key] = [l.strip() for l in resource_file.readlines()] + with open(res_path, "rt") as res_file: + config[key] = [l.strip() for l in res_file.readlines()] except OSError: - self.bot.log_e(f"Could not load resource at {resource_path}.") + self.bot.log_e(f"Could not load resource at {res_path}.") return config def __check_config(self): @@ -77,15 +79,15 @@ class Plugin: missing = False for key in self.REQUIRED_CONFIGS: if key not in self.config: - self.bot.log_e(f"Missing '{key}' in {self.name} configuration.") + self.bot.log_e(f"Missing '{key}' in {self.name} config.") missing = True return not missing def get_runtime_value(self, key, default=None, ns=None): """Get a value from the plugin runtime dict. - This will get the value from the plugin namespace, but it is possible to - get runtime values from other plugins using their name as `ns`. + This will get the value from the plugin namespace, but it is possible + to get runtime values from other plugins using their name as `ns`. """ if ns is None: ns = self.name @@ -98,8 +100,8 @@ class Plugin: def get_storage_value(self, key, default=None, ns=None): """Get a value from the plugin persistent storage. - This will get the value from the plugin namespace, but it is possible to - get storage values from other plugins using their name as `ns`. + This will get the value from the plugin namespace, but it is possible + to get storage values from other plugins using their name as `ns`. """ if ns is None: ns = self.name @@ -124,7 +126,10 @@ class Plugin: def remove_storage_list_value(self, key, value): """Remove a value from a persistent storage list.""" - if self.name in self.bot.storage and key in self.bot.storage[self.name]: + if ( + self.name in self.bot.storage + and key in self.bot.storage[self.name] + ): self.bot.storage[self.name][key].remove(value) def should_read_message(self, message): @@ -136,8 +141,8 @@ class Plugin: Note that you do not need to use this method for command/question plugins, the next methods already check for this and do more precise - parsing of the message. This is rather for plugins reacting to a message - addressed to the bot that are neither commands or questions. + parsing of the message. This is rather for plugins reacting to a + message addressed to the bot that are neither commands or questions. """ first_word_and_rest = message.split(maxsplit=1) num_parts = len(first_word_and_rest) @@ -158,11 +163,11 @@ class Plugin: def should_answer_question(self, message): """Store Question in object and return True if I should answer it. - To answer a question, the message must start with one of the bot's names - and optionally end with one or more question marks (they are discarded). - The bot checks that answering conditions are respected, and they cannot - be bypassed as with commands. A Question created is checked from the - plugin's registered questions and stored in the instance. + To answer a question, the message must start with one of the bot's + names and optionally end with one or more question marks (they are + discarded). The bot checks that answering conditions are respected, and + they cannot be bypassed as with commands. A Question created is checked + from the plugin's registered questions and stored in the instance. """ words = message.split() # Is the message addressed to me? @@ -200,9 +205,9 @@ class Plugin: If no_content is True, the command does not parse command contents and put all the command message (without suffix) to the command identifier. - This is useful for commands that have multiple words in their identifier - and do not have any content, or when the content parsing should be done - by the plugin itself. + This is useful for commands that have multiple words in their + identifier and do not have any content, or when the content parsing + should be done by the plugin itself. If exclude_conditions is a collection of plugin namespaces, the conditions of these plugins are not tested. A basic scenario is the @@ -250,7 +255,7 @@ class Plugin: return False def __parse_command(self, message, no_content=False): - """Return a parsed Command if this message is a command, None otherwise. + """Return a parsed Command if this message is a command, else None. The command raw field is always set. The ident and content fields are empty when no_content is True. The match field is never set by this @@ -273,7 +278,7 @@ class Plugin: def __save_command(self, command): """Save command in instance for further processing by the plugin.""" self.command = command - self.bot.log_i(f"Processing command from plugin {self.name}: {command}") + self.bot.log_i(f"Processing command from p. {self.name}: {command}") def respects_handling_conditions(self, exclude_conditions=None): """Check if handling conditions are valid. @@ -297,6 +302,19 @@ class Plugin: """Signal a plugin failure to target.""" self.bot.say(target, self.bot.config["error_message"]) + def on_privmsg(self, event): + """Default behaviour on privmsg is to answer like a pubmsg. + + Because most plugins assume messages are sent publicly, they send their + replies to `event.target` so we replace it with the event source nick. + If it causes an issue with a plugin requiring the original target even + on private message, override this method. + """ + if (on_pubmsg := getattr(self, "on_pubmsg", None)): + event.target = NickMask(event.source).nick + return on_pubmsg(event) + return False + @dataclass class Question: