plugins: add base system with horoscope as demo

This commit is contained in:
dece 2020-10-09 10:40:37 +02:00
parent 3d78c1b149
commit dee6096af2
9 changed files with 200 additions and 2 deletions

View file

@ -7,6 +7,7 @@ verify_ssl = true
[packages] [packages]
irc = "~=19.0.1" irc = "~=19.0.1"
requests = "*"
[requires] [requires]
python_version = "3.7" python_version = "3.7"

40
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "489adc12bb1fac949a63215586220274f8f10299d4d171976ac988bc32b4afab" "sha256": "024a36b2b9e61302df7560d7af32ef1c9802327f23381e327cacb345adf56eb7"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -16,6 +16,28 @@
] ]
}, },
"default": { "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": { "importlib-metadata": {
"hashes": [ "hashes": [
"sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da", "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da",
@ -95,6 +117,14 @@
], ],
"version": "==2020.1" "version": "==2020.1"
}, },
"requests": {
"hashes": [
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
],
"index": "pypi",
"version": "==2.24.0"
},
"six": { "six": {
"hashes": [ "hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
@ -111,6 +141,14 @@
"markers": "python_version >= '3.6'", "markers": "python_version >= '3.6'",
"version": "==4.0.0" "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": { "zipp": {
"hashes": [ "hashes": [
"sha256:64ad89efee774d1897a58607895d80789c59778ea02185dd846ac38394a8642b", "sha256:64ad89efee774d1897a58607895d80789c59778ea02185dd846ac38394a8642b",

26
README.md Normal file
View file

@ -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

View file

@ -2,5 +2,13 @@
"host": "irc.freenode.net", "host": "irc.freenode.net",
"port": 6667, "port": 6667,
"nick": "edm0nd", "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
}
}
} }

View file

@ -1,3 +1,8 @@
import importlib
import os
import time
from pathlib import Path
import irc.client import irc.client
from irc.client import NickMask from irc.client import NickMask
@ -10,11 +15,16 @@ class Bot(irc.client.SimpleIRCClient, Logger):
super().__init__() super().__init__()
self.config = config self.config = config
self.logger = logger self.logger = logger
self.plugins = []
@property @property
def nick(self): def nick(self):
return self.config["nick"] return self.config["nick"]
@property
def names(self):
return (self.config["nick"], *self.config["alternative_nicks"])
def on_welcome(self, connection, event): def on_welcome(self, connection, event):
self.log_i(f"Connected to server {event.source}.") self.log_i(f"Connected to server {event.source}.")
for channel in self.config["channels"]: for channel in self.config["channels"]:
@ -22,26 +32,60 @@ class Bot(irc.client.SimpleIRCClient, Logger):
def on_join(self, connection, event): def on_join(self, connection, event):
self.log_i(f"Joined {event.target}.") self.log_i(f"Joined {event.target}.")
self.run_plugin_callbacks(event)
def on_part(self, connection, event): def on_part(self, connection, event):
self.log_i(f"Left {event.target} (args: {event.arguments[0]}).") self.log_i(f"Left {event.target} (args: {event.arguments[0]}).")
self.run_plugin_callbacks(event)
def on_pubmsg(self, connection, event): def on_pubmsg(self, connection, event):
channel = event.target channel = event.target
nick = NickMask(event.source).nick nick = NickMask(event.source).nick
message = event.arguments[0] message = event.arguments[0]
self.log_d(f"Message in {channel} from {nick}: {message}") self.log_d(f"Message in {channel} from {nick}: {message}")
self.run_plugin_callbacks(event)
def on_privmsg(self, connection, event): def on_privmsg(self, connection, event):
nick = NickMask(event.source).nick nick = NickMask(event.source).nick
target = event.target target = event.target
message = event.arguments[0] message = event.arguments[0]
self.log_d(f"Private message from {nick} to {target}: {message}") self.log_d(f"Private message from {nick} to {target}: {message}")
self.run_plugin_callbacks(event)
def run(self): def run(self):
"""Connect the bot to server, join channels and start responding."""
self.log_i("Starting Edmond.") self.log_i("Starting Edmond.")
self.load_plugins()
self.connect(self.config["host"], self.config["port"], self.nick) self.connect(self.config["host"], self.config["port"], self.nick)
try: try:
self.start() self.start()
except KeyboardInterrupt: except KeyboardInterrupt:
self.log_i("Stopping Edmond.") 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)

45
edmond/plugin.py Normal file
View file

@ -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

View file

@ -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)

View file

@ -0,0 +1,8 @@
from edmond.plugin import Plugin
class OpinionPlugin(Plugin):
def __init__(self, bot):
super().__init__(bot)

7
edmond/utils.py Normal file
View file

@ -0,0 +1,7 @@
import requests
def http_get(url):
response = requests.get(url)
if response.status_code == 200:
return response.text