|
|
|
@ -1,7 +1,26 @@
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any, Optional
|
|
|
|
|
|
|
|
|
|
from irc.client import NickMask
|
|
|
|
|
from irc.client import NickMask # type: ignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class Question:
|
|
|
|
|
preamble: str
|
|
|
|
|
content: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class Command:
|
|
|
|
|
# Identifier as set in config. Set even when an alias has been used.
|
|
|
|
|
ident: str
|
|
|
|
|
# Content of the command when it has been parsed, empty str otherwise.
|
|
|
|
|
content: str
|
|
|
|
|
# Raw command content (minus name and suffix), always set.
|
|
|
|
|
raw: str
|
|
|
|
|
# Identifier matched, possibly an alias. Set only when matched.
|
|
|
|
|
matched: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Plugin:
|
|
|
|
@ -40,14 +59,16 @@ class Plugin:
|
|
|
|
|
- -10: lowest, e.g. for random reactions usually with a very low rate
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
REQUIRED_CONFIGS = []
|
|
|
|
|
REQUIRED_CONFIGS: list[str] = []
|
|
|
|
|
|
|
|
|
|
def __init__(self, bot):
|
|
|
|
|
self.bot = bot
|
|
|
|
|
self.name = self.__class__.__name__.lower()[:-6] # Remove "Plugin".
|
|
|
|
|
self.priority = 0
|
|
|
|
|
self.config = self.__get_config()
|
|
|
|
|
self.is_ready = self.__check_config()
|
|
|
|
|
from edmond.bot import Bot
|
|
|
|
|
self.bot: Bot = bot
|
|
|
|
|
# self.name is the plugin name, lowercased, without the Plugin suffix.
|
|
|
|
|
self.name: str = self.__class__.__name__.lower()[:-6]
|
|
|
|
|
self.priority: int = 0
|
|
|
|
|
self.config: dict = self.__get_config()
|
|
|
|
|
self.is_ready: bool = self.__check_config()
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def callbacks(self):
|
|
|
|
@ -58,7 +79,7 @@ class Plugin:
|
|
|
|
|
if cb.startswith("on_") and callable(getattr(self, cb))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
def __get_config(self):
|
|
|
|
|
def __get_config(self) -> dict:
|
|
|
|
|
"""Return the plugin section from the bot config plus common values."""
|
|
|
|
|
plugins_configs = self.bot.config["plugins"]
|
|
|
|
|
config = plugins_configs["common"].copy()
|
|
|
|
@ -69,12 +90,14 @@ class Plugin:
|
|
|
|
|
res_path = Path(res_dir) / res_path
|
|
|
|
|
try:
|
|
|
|
|
with open(res_path, "rt") as res_file:
|
|
|
|
|
config[key] = [l.strip() for l in res_file.readlines()]
|
|
|
|
|
config[key] = [
|
|
|
|
|
line.strip() for line in res_file.readlines()
|
|
|
|
|
]
|
|
|
|
|
except OSError:
|
|
|
|
|
self.bot.log_e(f"Could not load resource at {res_path}.")
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
def __check_config(self):
|
|
|
|
|
def __check_config(self) -> bool:
|
|
|
|
|
"""Return True if the plugin config is properly setup."""
|
|
|
|
|
missing = False
|
|
|
|
|
for key in self.REQUIRED_CONFIGS:
|
|
|
|
@ -83,7 +106,9 @@ class Plugin:
|
|
|
|
|
missing = True
|
|
|
|
|
return not missing
|
|
|
|
|
|
|
|
|
|
def get_runtime_value(self, key, default=None, ns=None):
|
|
|
|
|
def get_runtime_value(
|
|
|
|
|
self, key: str, default: Any = None, ns: str = None
|
|
|
|
|
) -> Any:
|
|
|
|
|
"""Get a value from the plugin runtime dict.
|
|
|
|
|
|
|
|
|
|
This will get the value from the plugin namespace, but it is possible
|
|
|
|
@ -93,11 +118,11 @@ class Plugin:
|
|
|
|
|
ns = self.name
|
|
|
|
|
return self.bot.values[ns].get(key, default)
|
|
|
|
|
|
|
|
|
|
def set_runtime_value(self, key, value):
|
|
|
|
|
def set_runtime_value(self, key: str, value: Any) -> Any:
|
|
|
|
|
"""Set a value in the plugin runtime dict."""
|
|
|
|
|
self.bot.values[self.name][key] = value
|
|
|
|
|
|
|
|
|
|
def get_storage_value(self, key, default=None, ns=None):
|
|
|
|
|
def get_storage_value(self, key: str, default=None, ns: str = None) -> Any:
|
|
|
|
|
"""Get a value from the plugin persistent storage.
|
|
|
|
|
|
|
|
|
|
This will get the value from the plugin namespace, but it is possible
|
|
|
|
@ -107,7 +132,7 @@ class Plugin:
|
|
|
|
|
ns = self.name
|
|
|
|
|
return self.bot.storage.get(ns, {}).get(key, default)
|
|
|
|
|
|
|
|
|
|
def set_storage_value(self, key, value, ns=None):
|
|
|
|
|
def set_storage_value(self, key: str, value: Any, ns: str = None) -> None:
|
|
|
|
|
"""Set a value in the plugin persistent storage."""
|
|
|
|
|
name = ns or self.name
|
|
|
|
|
if name not in self.bot.storage:
|
|
|
|
@ -115,7 +140,7 @@ class Plugin:
|
|
|
|
|
else:
|
|
|
|
|
self.bot.storage[name][key] = value
|
|
|
|
|
|
|
|
|
|
def append_storage_list_value(self, key, value):
|
|
|
|
|
def append_storage_list_value(self, key: str, value: Any) -> None:
|
|
|
|
|
"""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]}
|
|
|
|
@ -124,7 +149,7 @@ class Plugin:
|
|
|
|
|
else:
|
|
|
|
|
self.bot.storage[self.name][key].append(value)
|
|
|
|
|
|
|
|
|
|
def remove_storage_list_value(self, key, value):
|
|
|
|
|
def remove_storage_list_value(self, key: str, value: Any) -> None:
|
|
|
|
|
"""Remove a value from a persistent storage list."""
|
|
|
|
|
if (
|
|
|
|
|
self.name in self.bot.storage
|
|
|
|
@ -132,7 +157,7 @@ class Plugin:
|
|
|
|
|
):
|
|
|
|
|
self.bot.storage[self.name][key].remove(value)
|
|
|
|
|
|
|
|
|
|
def should_read_message(self, message):
|
|
|
|
|
def should_read_message(self, message: str) -> Optional[str]:
|
|
|
|
|
"""Return a message content if it has been addressed to me, else None.
|
|
|
|
|
|
|
|
|
|
If the message starts with one of the bot's names, the rest of the
|
|
|
|
@ -159,8 +184,9 @@ class Plugin:
|
|
|
|
|
content = first_word_and_rest[1].strip()
|
|
|
|
|
self.bot.log_i(f"Reading message from {self.name}: {content}")
|
|
|
|
|
return content
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def should_answer_question(self, message):
|
|
|
|
|
def should_answer_question(self, message: str) -> bool:
|
|
|
|
|
"""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
|
|
|
|
@ -188,19 +214,18 @@ class Plugin:
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def __save_question(self, question, matched, preamble):
|
|
|
|
|
def __save_question(self, question: str, matched: str, preamble: str):
|
|
|
|
|
content = question[len(matched) :].strip()
|
|
|
|
|
content = content.rstrip("?").rstrip()
|
|
|
|
|
question = Question(preamble, content)
|
|
|
|
|
self.question = question
|
|
|
|
|
self.bot.log_i(f"Answering from plugin {self.name}: {question}")
|
|
|
|
|
self.question = Question(preamble, content)
|
|
|
|
|
self.bot.log_i(f"Answering from plugin {self.name}: {self.question}")
|
|
|
|
|
|
|
|
|
|
def should_handle_command(
|
|
|
|
|
self,
|
|
|
|
|
message,
|
|
|
|
|
no_content=False,
|
|
|
|
|
exclude_conditions=None,
|
|
|
|
|
):
|
|
|
|
|
message: str,
|
|
|
|
|
no_content: bool = False,
|
|
|
|
|
exclude_conditions: Optional[dict] = None,
|
|
|
|
|
) -> bool:
|
|
|
|
|
"""Store Command in object and return True if I should handle it.
|
|
|
|
|
|
|
|
|
|
If no_content is True, the command does not parse command contents and
|
|
|
|
@ -254,7 +279,9 @@ class Plugin:
|
|
|
|
|
return True
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
def __parse_command(self, message, no_content=False):
|
|
|
|
|
def __parse_command(
|
|
|
|
|
self, message: str, no_content: bool = False
|
|
|
|
|
) -> Optional[Command]:
|
|
|
|
|
"""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
|
|
|
|
@ -274,13 +301,14 @@ class Plugin:
|
|
|
|
|
ident = words[1]
|
|
|
|
|
content = " ".join(words[2:-1])
|
|
|
|
|
return Command(ident, content, raw)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def __save_command(self, command):
|
|
|
|
|
def __save_command(self, command: Command):
|
|
|
|
|
"""Save command in instance for further processing by the plugin."""
|
|
|
|
|
self.command = 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: Optional[dict] = None):
|
|
|
|
|
"""Check if handling conditions are valid.
|
|
|
|
|
|
|
|
|
|
Handling conditions can be specified by each plugin to create states in
|
|
|
|
@ -314,21 +342,3 @@ class Plugin:
|
|
|
|
|
event.target = NickMask(event.source).nick
|
|
|
|
|
return on_pubmsg(event)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class Question:
|
|
|
|
|
preamble: str
|
|
|
|
|
content: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class Command:
|
|
|
|
|
# Identifier as set in config. Set even when an alias has been used.
|
|
|
|
|
ident: str
|
|
|
|
|
# Content of the command when it has been parsed, empty str otherwise.
|
|
|
|
|
content: str
|
|
|
|
|
# Raw command content (minus name and suffix), always set.
|
|
|
|
|
raw: str
|
|
|
|
|
# Identifier matched, possibly an alias. Set only when matched.
|
|
|
|
|
matched: str = ""
|
|
|
|
|