From dee6096af2db135f8312c156fd566b820bfcc69f Mon Sep 17 00:00:00 2001 From: dece Date: Fri, 9 Oct 2020 10:40:37 +0200 Subject: [PATCH] plugins: add base system with horoscope as demo --- Pipfile | 1 + Pipfile.lock | 40 ++++++++++++++++++++++++++++++++- README.md | 26 +++++++++++++++++++++ config.json.example | 10 ++++++++- edmond/bot.py | 44 ++++++++++++++++++++++++++++++++++++ edmond/plugin.py | 45 +++++++++++++++++++++++++++++++++++++ edmond/plugins/horoscope.py | 21 +++++++++++++++++ edmond/plugins/opinion.py | 8 +++++++ edmond/utils.py | 7 ++++++ 9 files changed, 200 insertions(+), 2 deletions(-) create mode 100644 README.md create mode 100644 edmond/plugin.py create mode 100644 edmond/plugins/horoscope.py create mode 100644 edmond/plugins/opinion.py create mode 100644 edmond/utils.py diff --git a/Pipfile b/Pipfile index 995c3aa..bf58ead 100644 --- a/Pipfile +++ b/Pipfile @@ -7,6 +7,7 @@ verify_ssl = true [packages] irc = "~=19.0.1" +requests = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 718fccb..fb2a633 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "489adc12bb1fac949a63215586220274f8f10299d4d171976ac988bc32b4afab" + "sha256": "024a36b2b9e61302df7560d7af32ef1c9802327f23381e327cacb345adf56eb7" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,28 @@ ] }, "default": { + "certifi": { + "hashes": [ + "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", + "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" + ], + "version": "==2020.6.20" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6", + "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.10" + }, "importlib-metadata": { "hashes": [ "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da", @@ -95,6 +117,14 @@ ], "version": "==2020.1" }, + "requests": { + "hashes": [ + "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", + "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" + ], + "index": "pypi", + "version": "==2.24.0" + }, "six": { "hashes": [ "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", @@ -111,6 +141,14 @@ "markers": "python_version >= '3.6'", "version": "==4.0.0" }, + "urllib3": { + "hashes": [ + "sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", + "sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", + "version": "==1.25.10" + }, "zipp": { "hashes": [ "sha256:64ad89efee774d1897a58607895d80789c59778ea02185dd846ac38394a8642b", diff --git a/README.md b/README.md new file mode 100644 index 0000000..11ef66d --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +Edm0nd +====== + +New version of the infamous IRC bot. + + + +Missing features +---------------- + +- [ ] Actions (/me) +- [ ] Beers +- [ ] Mood +- [ ] Random: dice, choice, etc +- [ ] Notes +- [ ] Handle compliments +- [ ] Handle +- [ ] Horoscope +- [ ] "Journee mondiale" +- [ ] Mug +- [ ] Music +- [ ] Opinions +- [ ] Translate +- [ ] Wikipedia: find definition, get random page +- [ ] Wolframalpha +- [ ] Youtube: parsing for title, requests for channel or video diff --git a/config.json.example b/config.json.example index b9b6e36..523f023 100644 --- a/config.json.example +++ b/config.json.example @@ -2,5 +2,13 @@ "host": "irc.freenode.net", "port": 6667, "nick": "edm0nd", - "channels": ["#idi0crates"] + "alternative_nicks": ["edmon", "edmond"], + "channels": ["#idi0crates"], + "speak_delay": 0.5, + "plugins": { + "horoscope": { + "url": "http://zwergf.elynx.fr/bots/horobot/", + "delay": 2 + } + } } diff --git a/edmond/bot.py b/edmond/bot.py index 602ee7d..4ad4919 100644 --- a/edmond/bot.py +++ b/edmond/bot.py @@ -1,3 +1,8 @@ +import importlib +import os +import time +from pathlib import Path + import irc.client from irc.client import NickMask @@ -10,11 +15,16 @@ class Bot(irc.client.SimpleIRCClient, Logger): super().__init__() self.config = config self.logger = logger + self.plugins = [] @property def nick(self): return self.config["nick"] + @property + def names(self): + return (self.config["nick"], *self.config["alternative_nicks"]) + def on_welcome(self, connection, event): self.log_i(f"Connected to server {event.source}.") for channel in self.config["channels"]: @@ -22,26 +32,60 @@ class Bot(irc.client.SimpleIRCClient, Logger): def on_join(self, connection, event): self.log_i(f"Joined {event.target}.") + self.run_plugin_callbacks(event) def on_part(self, connection, event): self.log_i(f"Left {event.target} (args: {event.arguments[0]}).") + self.run_plugin_callbacks(event) def on_pubmsg(self, connection, event): channel = event.target nick = NickMask(event.source).nick message = event.arguments[0] self.log_d(f"Message in {channel} from {nick}: {message}") + self.run_plugin_callbacks(event) def on_privmsg(self, connection, event): nick = NickMask(event.source).nick target = event.target message = event.arguments[0] self.log_d(f"Private message from {nick} to {target}: {message}") + self.run_plugin_callbacks(event) def run(self): + """Connect the bot to server, join channels and start responding.""" self.log_i("Starting Edmond.") + self.load_plugins() self.connect(self.config["host"], self.config["port"], self.nick) try: self.start() except KeyboardInterrupt: self.log_i("Stopping Edmond.") + + def load_plugins(self): + """Load all installed plugins.""" + plugin_files = os.listdir(Path(__file__).parent / "plugins") + plugin_names = map( + lambda f: os.path.splitext(f)[0], + filter(lambda f: f.endswith(".py"), plugin_files) + ) + for plugin_name in plugin_names: + module = importlib.import_module(f"edmond.plugins.{plugin_name}") + class_name = plugin_name.capitalize() + "Plugin" + plugin_class = getattr(module, class_name) + self.plugins.append(plugin_class(self)) + self.log_d(f"Loaded {class_name}.") + + def say(self, target, message): + """Send message to target after a slight delay.""" + time.sleep(self.config["speak_delay"]) + self.log_d(f"Sending to {target}: {message}") + self.connection.privmsg(target, message) + + def run_plugin_callbacks(self, event): + etype = event.type + for plugin in self.plugins: + callbacks = plugin.callbacks + if etype not in callbacks: + continue + callbacks[etype](event) diff --git a/edmond/plugin.py b/edmond/plugin.py new file mode 100644 index 0000000..731b6e6 --- /dev/null +++ b/edmond/plugin.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass + + +class Plugin: + + def __init__(self, bot): + self.bot = bot + self.config = self.get_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.""" + name = self.__class__.__name__.lower()[:-6] # Remove Plugin suffix. + plugins_configs = self.bot.config["plugins"] + return plugins_configs.get(name, {}) + + def should_handle_command(self, message): + """Store Command in object and return True if it should handle it. """ + command = self.parse_command(message) + if command and any(c == command.ident for c in self.COMMANDS): + self.command = command + return True + return False + + def parse_command(self, message): + """Return a command ID if this message is a command.""" + words = message.split() + if words[0].lower() in self.bot.names and words[-1] == "please": + ident = words[1] + content = " ".join(words[2:-1]) + return Command(ident, content) + + +@dataclass +class Command: + ident: str + content: str diff --git a/edmond/plugins/horoscope.py b/edmond/plugins/horoscope.py new file mode 100644 index 0000000..42a9e7f --- /dev/null +++ b/edmond/plugins/horoscope.py @@ -0,0 +1,21 @@ +import time + +from edmond.plugin import Plugin +from edmond.utils import http_get + + +class HoroscopePlugin(Plugin): + + COMMANDS = ["horoscope"] + + def __init__(self, bot): + super().__init__(bot) + + def on_pubmsg(self, event): + if not self.should_handle_command(event.arguments[0]): + return False + self.bot.say(event.target, "/me looks at the stars") + time.sleep(self.config["delay"]) + text = http_get(self.config["url"]) + if text: + self.bot.say(event.target, text) diff --git a/edmond/plugins/opinion.py b/edmond/plugins/opinion.py new file mode 100644 index 0000000..f9ae1e5 --- /dev/null +++ b/edmond/plugins/opinion.py @@ -0,0 +1,8 @@ +from edmond.plugin import Plugin + + +class OpinionPlugin(Plugin): + + def __init__(self, bot): + super().__init__(bot) + diff --git a/edmond/utils.py b/edmond/utils.py new file mode 100644 index 0000000..07a29b6 --- /dev/null +++ b/edmond/utils.py @@ -0,0 +1,7 @@ +import requests + + +def http_get(url): + response = requests.get(url) + if response.status_code == 200: + return response.text