plugins: add base system with horoscope as demo
This commit is contained in:
parent
3d78c1b149
commit
dee6096af2
1
Pipfile
1
Pipfile
|
@ -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
40
Pipfile.lock
generated
|
@ -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
26
README.md
Normal 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
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
45
edmond/plugin.py
Normal 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
|
21
edmond/plugins/horoscope.py
Normal file
21
edmond/plugins/horoscope.py
Normal 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)
|
8
edmond/plugins/opinion.py
Normal file
8
edmond/plugins/opinion.py
Normal 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
7
edmond/utils.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
def http_get(url):
|
||||||
|
response = requests.get(url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.text
|
Loading…
Reference in a new issue