144 lines
5.2 KiB
Python
144 lines
5.2 KiB
Python
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
|
|
class Plugin:
|
|
|
|
REQUIRED_CONFIGS = []
|
|
|
|
def __init__(self, bot):
|
|
self.bot = bot
|
|
self.name = self.__class__.__name__.lower()[:-6] # Remove "Plugin".
|
|
self.config = self._get_config()
|
|
self.is_ready = self._check_config()
|
|
|
|
@property
|
|
def callbacks(self):
|
|
"""List of callback types available for this plugin."""
|
|
return {
|
|
cb[3:]: getattr(self, cb)
|
|
for cb in dir(self)
|
|
if cb.startswith("on_") and callable(getattr(self, cb))
|
|
}
|
|
|
|
def _get_config(self):
|
|
"""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
|
|
try:
|
|
with open(resource_path, "rt") as resource_file:
|
|
config[key] = [l.strip() for l in resource_file.readlines()]
|
|
except OSError:
|
|
self.bot.log_e(f"Could not load resource at {resource_path}.")
|
|
return config
|
|
|
|
def _check_config(self):
|
|
"""Return True if the plugin config is properly setup."""
|
|
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.")
|
|
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`.
|
|
"""
|
|
if ns is None:
|
|
ns = self.name
|
|
return self.bot.values[ns].get(key, default)
|
|
|
|
def set_runtime_value(self, key, value):
|
|
"""Set a value in the plugin runtime dict."""
|
|
self.bot.values[self.name][key] = value
|
|
|
|
def get_storage_value(self, key, default=None):
|
|
"""Get a value from the plugin persistent storage."""
|
|
return self.bot.storage.get(self.name, {}).get(key, default)
|
|
|
|
def set_storage_value(self, key, value):
|
|
"""Set a value in the plugin persistent storage."""
|
|
if self.name not in self.bot.storage:
|
|
self.bot.storage[self.name] = {key: value}
|
|
else:
|
|
self.bot.storage[self.name][key] = value
|
|
|
|
def append_storage_list_value(self, key, value):
|
|
"""Append a value to a list in the plugin persistent storage."""
|
|
if self.name not in self.bot.storage:
|
|
self.bot.storage[self.name] = {key: [value]}
|
|
elif key not in self.bot.storage[self.name]:
|
|
self.bot.storage[self.name][key] = [value]
|
|
else:
|
|
self.bot.storage[self.name][key].append(value)
|
|
|
|
def remove_storage_list_value(self, key, value):
|
|
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_answer_question(self, message):
|
|
"""Store Question in object and return True if it should answer it."""
|
|
words = message.split()
|
|
if words[0].lower() not in self.bot.names:
|
|
return False
|
|
question = message[len(words[0]):].strip()
|
|
|
|
for q in self.config.get("questions", []):
|
|
if question.startswith(q):
|
|
self.question = Question(q, question[len(q):].strip())
|
|
self.bot.log_d(f"Answering question from plugin {self.name}.")
|
|
return True
|
|
return False
|
|
|
|
def should_handle_command(self, message, no_content=False):
|
|
"""Store Command in object and return True if it should handle it."""
|
|
command = self.parse_command(message, no_content=no_content)
|
|
if not command:
|
|
return False
|
|
commands = self.config.get("commands", [])
|
|
if (
|
|
any(command.ident == c for c in commands) or
|
|
(no_content and any(command.ident.startswith(c) for c in commands))
|
|
):
|
|
self.command = command
|
|
self.bot.log_d(f"Processing command from plugin {self.name}.")
|
|
return True
|
|
return False
|
|
|
|
def parse_command(self, message, no_content=False):
|
|
"""Return a command ID if this message is a command."""
|
|
words = message.split()
|
|
command_suffix = self.config["command_suffix"]
|
|
if words[0].lower() in self.bot.names and words[-1] == command_suffix:
|
|
if no_content:
|
|
ident = " ".join(words[1:-1])
|
|
content = ""
|
|
else:
|
|
ident = words[1]
|
|
content = " ".join(words[2:-1])
|
|
return Command(ident, content)
|
|
|
|
def signal_failure(self, target):
|
|
"""Signal a plugin failure to target."""
|
|
self.bot.say(target, self.bot.config["error_message"])
|
|
|
|
|
|
@dataclass
|
|
class Question:
|
|
preambule: str
|
|
content: str
|
|
|
|
|
|
@dataclass
|
|
class Command:
|
|
ident: str
|
|
content: str
|