diff --git a/.gitignore b/.gitignore index 0360844..8af5c69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ config.json +storage.json resources/* !resources/.gitkeep diff --git a/README.md b/README.md index 0c3455d..7caee13 100644 --- a/README.md +++ b/README.md @@ -43,3 +43,4 @@ Missing features - [ ] Youtube: parsing for title, requests for channel or video - [ ] Command aliases - [ ] Question aliases +- [ ] Various macros diff --git a/config.json.example b/config.json.example index 84045b1..384ace7 100644 --- a/config.json.example +++ b/config.json.example @@ -5,6 +5,7 @@ "alternative_nicks": ["edmon", "edmond"], "channels": ["#idi0crates"], "speak_delay": 0.5, + "storage_file": "storage.json", "resources_dir": "resources", "error_message": "An error occured, sorry!", "plugins": { @@ -37,6 +38,10 @@ "pissed": "Pissed off..." } }, + "notes": { + "commands": ["note down"], + "content_regex": "for (P:\\S+) (P:.+)" + }, "random": { "commands": ["choose"], "separator": "or" diff --git a/edmond/bot.py b/edmond/bot.py index 6504617..eeff7e5 100644 --- a/edmond/bot.py +++ b/edmond/bot.py @@ -1,4 +1,5 @@ import importlib +import json import os import time from pathlib import Path @@ -17,6 +18,7 @@ class Bot(irc.client.SimpleIRCClient, Logger): self.logger = logger self.plugins = [] self.values = {} + self.storage = self._get_storage() @property def nick(self): @@ -26,6 +28,28 @@ class Bot(irc.client.SimpleIRCClient, Logger): def names(self): return (self.config["nick"], *self.config["alternative_nicks"]) + def _get_storage(self): + """Load data from storage.""" + try: + with open(self.config["storage_file"], "rt") as storage_file: + storage = json.load(storage_file) + self.log_d("Loaded storage file.") + return storage + except (OSError, json.decoder.JSONDecodeError) as exc: + self.log_e(f"Could not load storage file: {exc}") + self.log_w("If it's not the first time Edm0nd is run, you may lose" + " data when closing the program.") + return {} + + def _save_storage(self): + """Save storage data to disk.""" + try: + with open(self.config["storage_file"], "wt") as storage_file: + json.dump(self.storage, storage_file, indent=2, sort_keys=True) + self.log_d("Saved storage file.") + except (OSError, json.decoder.JSONEncodeError) as exc: + self.log_e(f"Could not save storage file: {exc}") + def on_welcome(self, connection, event): self.log_i(f"Connected to server {event.source}.") self.run_plugin_callbacks(event) @@ -63,6 +87,7 @@ class Bot(irc.client.SimpleIRCClient, Logger): self.start() except KeyboardInterrupt: self.log_i("Stopping Edmond.") + self._save_storage() def load_plugins(self): """Load all installed plugins.""" @@ -97,6 +122,7 @@ class Bot(irc.client.SimpleIRCClient, Logger): self.connection.privmsg(target, message) def run_plugin_callbacks(self, event): + """Run appropriate callbacks for each plugin.""" etype = event.type for plugin in filter(lambda p: p.is_ready, self.plugins): callbacks = plugin.callbacks diff --git a/edmond/plugin.py b/edmond/plugin.py index 3f9e94a..4ca824f 100644 --- a/edmond/plugin.py +++ b/edmond/plugin.py @@ -47,7 +47,11 @@ class Plugin: return not missing def get_runtime_value(self, key, 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 + 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) @@ -56,6 +60,26 @@ class Plugin: """Set a value in the plugin runtime dict.""" self.bot.values[self.name][key] = value + def get_storage_value(self, key): + """Get a value from the plugin persistent storage.""" + return self.bot.storage.get(self.name, {}).get(key) + + 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 should_answer_question(self, message): """Store Question in object and return True if it should answer it.""" words = message.split() @@ -74,7 +98,10 @@ class Plugin: """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): + 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 diff --git a/edmond/plugins/notes.py b/edmond/plugins/notes.py new file mode 100644 index 0000000..bd1bcc9 --- /dev/null +++ b/edmond/plugins/notes.py @@ -0,0 +1,42 @@ +import re + +from edmond.plugin import Plugin + + +class NotesPlugin(Plugin): + + REQUIRED_CONFIGS = ["commands", "content_regex"] + + def __init__(self, bot): + super().__init__(bot) + self._content_re = None + + @property + def content_re(self): + if self._content_re is None: + self._content_re = re.compile(self.config["content_regex"]) + return self._content_re + + def on_pubmsg(self, event): + if not self.should_handle_command(event.arguments[0], no_content=True): + return False + + # "note down" command. + command0 = self.config["commands"][0] + if self.command.ident.startswith(command0): + content = self.command.ident[len(command0):].strip() + match = self.content_re.match(content) + if not match: + return False + groups = match.groupdict() + if any(k not in groups for k in ("target", "note")): + return False + target = groups["target"] + message = groups["note"] + self.bot.log_d(f"Noting for {target}: {message}") + note = { + "sender": event.source.nick, + "dest": target, + "message": message + } + self.append_storage_list_value("notes", note) diff --git a/edmond/plugins/random.py b/edmond/plugins/random.py index 2462ebe..2f8b47e 100644 --- a/edmond/plugins/random.py +++ b/edmond/plugins/random.py @@ -13,14 +13,10 @@ class RandomPlugin(Plugin): def on_pubmsg(self, event): if not self.should_handle_command(event.arguments[0]): return False - if self.command.ident == "choose": - self.pick_random(event.target) - - def pick_random(self, target): separator = self.config["separator"] choices = self.command.content.split(f" {separator} ") self.bot.log_d(f"Choices: {choices}") if len(choices): choice = random.choice(choices) if choice: - self.bot.say(target, choice) + self.bot.say(event.target, choice)