plugin: answer to privmsg mostly like pubmsg

This commit is contained in:
dece 2022-07-07 19:06:59 +02:00
parent a8b1b811c5
commit 8020de4edf
2 changed files with 48 additions and 30 deletions

View file

@ -95,7 +95,7 @@ class Bot(irc.client.SimpleIRCClient, Logger):
self.run_plugin_callbacks(event) self.run_plugin_callbacks(event)
def on_privmsg(self, connection, 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 nick = NickMask(event.source).nick
target = event.target target = event.target
message = event.arguments[0] message = event.arguments[0]

View file

@ -1,6 +1,8 @@
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from irc.client import NickMask
class Plugin: class Plugin:
"""Base class for the bot plugins. """Base class for the bot plugins.
@ -17,15 +19,15 @@ class Plugin:
available after a restart. available after a restart.
Initalisation should be very fast, no network connections or anything. They Initalisation should be very fast, no network connections or anything. They
are initialised before connecting to the server, so their `is_ready` flag is are initialised before connecting to the server, so their `is_ready` flag
set at that point. The loading order is more or less random, so a plugin 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 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 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 on_welcome callback which is called after connecting to a server, and can
disable itself by setting its own `is_ready` flag to false. 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 A plugin can access its config once the base `__init__` has been called.
configuration is valid only is `is_ready` is True, else accessing its The configuration is valid only is `is_ready` is True, else accessing its
content is undefined behaviour. content is undefined behaviour.
Plugins can have priorities and calling their callbacks will respect it. Plugins can have priorities and calling their callbacks will respect it.
@ -57,19 +59,19 @@ class Plugin:
} }
def __get_config(self): 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"] plugins_configs = self.bot.config["plugins"]
config = plugins_configs["common"].copy() config = plugins_configs["common"].copy()
config.update(plugins_configs.get(self.name, {})) config.update(plugins_configs.get(self.name, {}))
# Load resources as config values. # Load resources as config values.
resources_dir = self.bot.config["resources_dir"] res_dir = self.bot.config["resources_dir"]
for key, resource_path in config.get("resources", {}).items(): for key, res_path in config.get("resources", {}).items():
resource_path = Path(resources_dir) / resource_path res_path = Path(res_dir) / res_path
try: try:
with open(resource_path, "rt") as resource_file: with open(res_path, "rt") as res_file:
config[key] = [l.strip() for l in resource_file.readlines()] config[key] = [l.strip() for l in res_file.readlines()]
except OSError: 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 return config
def __check_config(self): def __check_config(self):
@ -77,15 +79,15 @@ class Plugin:
missing = False missing = False
for key in self.REQUIRED_CONFIGS: for key in self.REQUIRED_CONFIGS:
if key not in self.config: 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 missing = True
return not missing return not missing
def get_runtime_value(self, key, default=None, ns=None): def get_runtime_value(self, key, default=None, ns=None):
"""Get a value from the plugin runtime dict. """Get a value from the plugin runtime dict.
This will get the value from the plugin namespace, but it is possible to This will get the value from the plugin namespace, but it is possible
get runtime values from other plugins using their name as `ns`. to get runtime values from other plugins using their name as `ns`.
""" """
if ns is None: if ns is None:
ns = self.name ns = self.name
@ -98,8 +100,8 @@ class Plugin:
def get_storage_value(self, key, default=None, ns=None): def get_storage_value(self, key, default=None, ns=None):
"""Get a value from the plugin persistent storage. """Get a value from the plugin persistent storage.
This will get the value from the plugin namespace, but it is possible to This will get the value from the plugin namespace, but it is possible
get storage values from other plugins using their name as `ns`. to get storage values from other plugins using their name as `ns`.
""" """
if ns is None: if ns is None:
ns = self.name ns = self.name
@ -124,7 +126,10 @@ class Plugin:
def remove_storage_list_value(self, key, value): def remove_storage_list_value(self, key, value):
"""Remove a value from a persistent storage list.""" """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) self.bot.storage[self.name][key].remove(value)
def should_read_message(self, message): 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 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 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 parsing of the message. This is rather for plugins reacting to a
addressed to the bot that are neither commands or questions. message addressed to the bot that are neither commands or questions.
""" """
first_word_and_rest = message.split(maxsplit=1) first_word_and_rest = message.split(maxsplit=1)
num_parts = len(first_word_and_rest) num_parts = len(first_word_and_rest)
@ -158,11 +163,11 @@ class Plugin:
def should_answer_question(self, message): def should_answer_question(self, message):
"""Store Question in object and return True if I should answer it. """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 To answer a question, the message must start with one of the bot's
and optionally end with one or more question marks (they are discarded). names and optionally end with one or more question marks (they are
The bot checks that answering conditions are respected, and they cannot discarded). The bot checks that answering conditions are respected, and
be bypassed as with commands. A Question created is checked from the they cannot be bypassed as with commands. A Question created is checked
plugin's registered questions and stored in the instance. from the plugin's registered questions and stored in the instance.
""" """
words = message.split() words = message.split()
# Is the message addressed to me? # 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 If no_content is True, the command does not parse command contents and
put all the command message (without suffix) to the command identifier. put all the command message (without suffix) to the command identifier.
This is useful for commands that have multiple words in their identifier This is useful for commands that have multiple words in their
and do not have any content, or when the content parsing should be done identifier and do not have any content, or when the content parsing
by the plugin itself. should be done by the plugin itself.
If exclude_conditions is a collection of plugin namespaces, the If exclude_conditions is a collection of plugin namespaces, the
conditions of these plugins are not tested. A basic scenario is the conditions of these plugins are not tested. A basic scenario is the
@ -250,7 +255,7 @@ class Plugin:
return False return False
def __parse_command(self, message, no_content=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 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 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): def __save_command(self, command):
"""Save command in instance for further processing by the plugin.""" """Save command in instance for further processing by the plugin."""
self.command = command 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): def respects_handling_conditions(self, exclude_conditions=None):
"""Check if handling conditions are valid. """Check if handling conditions are valid.
@ -297,6 +302,19 @@ class Plugin:
"""Signal a plugin failure to target.""" """Signal a plugin failure to target."""
self.bot.say(target, self.bot.config["error_message"]) 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 @dataclass
class Question: class Question: