Edm0nd/edmond/plugin.py

111 lines
3.8 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, ns=None):
"""Get a value from the plugin runtime dict."""
if ns is None:
ns = self.name
return self.bot.values[ns].get(key)
def set_runtime_value(self, key, value):
"""Set a value in the plugin runtime dict."""
self.bot.values[self.name][key] = 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)
commands = self.config.get("commands", [])
if command and any(c == command.ident 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