plugin: answer to privmsg mostly like pubmsg

master
dece 2 years ago
parent a8b1b811c5
commit 8020de4edf

@ -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]

@ -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:

Loading…
Cancel
Save