Compare commits
6 commits
895b0dec47
...
4177730164
Author | SHA1 | Date | |
---|---|---|---|
dece | 4177730164 | ||
dece | 448ee8477e | ||
dece | 0978b43d2c | ||
dece | 3831987d9d | ||
dece | 24bf8d02d0 | ||
dece | bbacc2fbef |
|
@ -9,7 +9,7 @@ ADD edmond /app/edmond
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN pip install requests irc
|
RUN pip install requests irc
|
||||||
RUN pip install -r edmond/plugins/requirements.txt
|
RUN pip install -r plugins-requirements.txt
|
||||||
|
|
||||||
RUN groupadd --gid ${GID} edmond
|
RUN groupadd --gid ${GID} edmond
|
||||||
RUN useradd --uid ${UID} --gid edmond \
|
RUN useradd --uid ${UID} --gid edmond \
|
||||||
|
|
1
Pipfile
1
Pipfile
|
@ -8,7 +8,6 @@ python-lsp-server = "*"
|
||||||
rope = "*"
|
rope = "*"
|
||||||
pyflakes = "*"
|
pyflakes = "*"
|
||||||
mccabe = "*"
|
mccabe = "*"
|
||||||
pycodestyle = "*"
|
|
||||||
black = "*"
|
black = "*"
|
||||||
python-lsp-black = "*"
|
python-lsp-black = "*"
|
||||||
mypy = "*"
|
mypy = "*"
|
||||||
|
|
83
Pipfile.lock
generated
83
Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "7efa513c27dbd60153dad1bbd870d6ee19d085a5daaf7b4a356f98caab790fd3"
|
"sha256": "0c289dec5d2be30e6e49a067f62cefe5f00942be835168cbe8d735a271412b46"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {},
|
"requires": {},
|
||||||
|
@ -14,6 +14,13 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
|
"autocommand": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:85d03044c2a1fc1c7844ac41545045927aecde0cbaf8ea28b88e0cd8588ce5d3",
|
||||||
|
"sha256:fed420e9d02745821a782971b583c6970259ee0b229be2a0a401e1467a4f170f"
|
||||||
|
],
|
||||||
|
"version": "==2.2.1"
|
||||||
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
|
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
|
||||||
|
@ -35,6 +42,13 @@
|
||||||
],
|
],
|
||||||
"version": "==3.3"
|
"version": "==3.3"
|
||||||
},
|
},
|
||||||
|
"inflect": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:0bc1516ec2725e2d8221707a612245093cb6f1cea209cfd8cbd4fc5e96fa6365",
|
||||||
|
"sha256:e3b85d65a296843268f35f4136283ad7c012a129375db1529d49b4b01ecb400b"
|
||||||
|
],
|
||||||
|
"version": "==6.0.0"
|
||||||
|
},
|
||||||
"irc": {
|
"irc": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5114aee1247ff634abed80f3349d75ccab35616a4b76be2a25add7ef189db0e5",
|
"sha256:5114aee1247ff634abed80f3349d75ccab35616a4b76be2a25add7ef189db0e5",
|
||||||
|
@ -87,10 +101,10 @@
|
||||||
},
|
},
|
||||||
"jaraco.text": {
|
"jaraco.text": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:450957c3f8fb9a553d9d3e60738733ab1c5cc27b36a463342adb937e9a70ab3e",
|
"sha256:4b2dc3678994de601e3e5b6f554eeb8445651b7ab984893c5e9ac44564033077",
|
||||||
"sha256:e4418d632425d741b8f9128cdf8fd9c0c878dc1450a7430dbd4bf6296eeac915"
|
"sha256:810bc4aee7329e30bec8c6c6bab1b6ef149359ca4b98928e42178345ed7d7bd8"
|
||||||
],
|
],
|
||||||
"version": "==3.8.1"
|
"version": "==3.9.0"
|
||||||
},
|
},
|
||||||
"more-itertools": {
|
"more-itertools": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -99,12 +113,52 @@
|
||||||
],
|
],
|
||||||
"version": "==8.14.0"
|
"version": "==8.14.0"
|
||||||
},
|
},
|
||||||
|
"pydantic": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1061c6ee6204f4f5a27133126854948e3b3d51fcc16ead2e5d04378c199b2f44",
|
||||||
|
"sha256:19b5686387ea0d1ea52ecc4cffb71abb21702c5e5b2ac626fd4dbaa0834aa49d",
|
||||||
|
"sha256:2bd446bdb7755c3a94e56d7bdfd3ee92396070efa8ef3a34fab9579fe6aa1d84",
|
||||||
|
"sha256:328558c9f2eed77bd8fffad3cef39dbbe3edc7044517f4625a769d45d4cf7555",
|
||||||
|
"sha256:32e0b4fb13ad4db4058a7c3c80e2569adbd810c25e6ca3bbd8b2a9cc2cc871d7",
|
||||||
|
"sha256:3ee0d69b2a5b341fc7927e92cae7ddcfd95e624dfc4870b32a85568bd65e6131",
|
||||||
|
"sha256:4aafd4e55e8ad5bd1b19572ea2df546ccace7945853832bb99422a79c70ce9b8",
|
||||||
|
"sha256:4b3946f87e5cef3ba2e7bd3a4eb5a20385fe36521d6cc1ebf3c08a6697c6cfb3",
|
||||||
|
"sha256:4de71c718c9756d679420c69f216776c2e977459f77e8f679a4a961dc7304a56",
|
||||||
|
"sha256:5565a49effe38d51882cb7bac18bda013cdb34d80ac336428e8908f0b72499b0",
|
||||||
|
"sha256:5803ad846cdd1ed0d97eb00292b870c29c1f03732a010e66908ff48a762f20e4",
|
||||||
|
"sha256:5da164119602212a3fe7e3bc08911a89db4710ae51444b4224c2382fd09ad453",
|
||||||
|
"sha256:615661bfc37e82ac677543704437ff737418e4ea04bef9cf11c6d27346606044",
|
||||||
|
"sha256:78a4d6bdfd116a559aeec9a4cfe77dda62acc6233f8b56a716edad2651023e5e",
|
||||||
|
"sha256:7d0f183b305629765910eaad707800d2f47c6ac5bcfb8c6397abdc30b69eeb15",
|
||||||
|
"sha256:7ead3cd020d526f75b4188e0a8d71c0dbbe1b4b6b5dc0ea775a93aca16256aeb",
|
||||||
|
"sha256:84d76ecc908d917f4684b354a39fd885d69dd0491be175f3465fe4b59811c001",
|
||||||
|
"sha256:8cb0bc509bfb71305d7a59d00163d5f9fc4530f0881ea32c74ff4f74c85f3d3d",
|
||||||
|
"sha256:91089b2e281713f3893cd01d8e576771cd5bfdfbff5d0ed95969f47ef6d676c3",
|
||||||
|
"sha256:9c9e04a6cdb7a363d7cb3ccf0efea51e0abb48e180c0d31dca8d247967d85c6e",
|
||||||
|
"sha256:a8c5360a0297a713b4123608a7909e6869e1b56d0e96eb0d792c27585d40757f",
|
||||||
|
"sha256:afacf6d2a41ed91fc631bade88b1d319c51ab5418870802cedb590b709c5ae3c",
|
||||||
|
"sha256:b34ba24f3e2d0b39b43f0ca62008f7ba962cff51efa56e64ee25c4af6eed987b",
|
||||||
|
"sha256:bd67cb2c2d9602ad159389c29e4ca964b86fa2f35c2faef54c3eb28b4efd36c8",
|
||||||
|
"sha256:c0f5e142ef8217019e3eef6ae1b6b55f09a7a15972958d44fbd228214cede567",
|
||||||
|
"sha256:cdb4272678db803ddf94caa4f94f8672e9a46bae4a44f167095e4d06fec12979",
|
||||||
|
"sha256:d70916235d478404a3fa8c997b003b5f33aeac4686ac1baa767234a0f8ac2326",
|
||||||
|
"sha256:d8ce3fb0841763a89322ea0432f1f59a2d3feae07a63ea2c958b2315e1ae8adb",
|
||||||
|
"sha256:e0b214e57623a535936005797567231a12d0da0c29711eb3514bc2b3cd008d0f",
|
||||||
|
"sha256:e631c70c9280e3129f071635b81207cad85e6c08e253539467e4ead0e5b219aa",
|
||||||
|
"sha256:e78578f0c7481c850d1c969aca9a65405887003484d24f6110458fb02cca7747",
|
||||||
|
"sha256:f0ca86b525264daa5f6b192f216a0d1e860b7383e3da1c65a1908f9c02f42801",
|
||||||
|
"sha256:f1a68f4f65a9ee64b6ccccb5bf7e17db07caebd2730109cb8a95863cfa9c4e55",
|
||||||
|
"sha256:fafe841be1103f340a24977f61dee76172e4ae5f647ab9e7fd1e1fca51524f08",
|
||||||
|
"sha256:ff68fc85355532ea77559ede81f35fff79a6a5543477e168ab3a381887caea76"
|
||||||
|
],
|
||||||
|
"version": "==1.9.2"
|
||||||
|
},
|
||||||
"pytz": {
|
"pytz": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7",
|
"sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197",
|
||||||
"sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"
|
"sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"
|
||||||
],
|
],
|
||||||
"version": "==2022.1"
|
"version": "==2022.2.1"
|
||||||
},
|
},
|
||||||
"requests": {
|
"requests": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
@ -121,6 +175,13 @@
|
||||||
],
|
],
|
||||||
"version": "==5.0.2"
|
"version": "==5.0.2"
|
||||||
},
|
},
|
||||||
|
"typing-extensions": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02",
|
||||||
|
"sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"
|
||||||
|
],
|
||||||
|
"version": "==4.3.0"
|
||||||
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc",
|
"sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc",
|
||||||
|
@ -252,14 +313,6 @@
|
||||||
],
|
],
|
||||||
"version": "==1.0.0"
|
"version": "==1.0.0"
|
||||||
},
|
},
|
||||||
"pycodestyle": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785",
|
|
||||||
"sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==2.9.1"
|
|
||||||
},
|
|
||||||
"pyflakes": {
|
"pyflakes": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2",
|
"sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2",
|
||||||
|
|
|
@ -4,46 +4,48 @@ import os
|
||||||
import time
|
import time
|
||||||
import signal
|
import signal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterable, Optional
|
||||||
|
|
||||||
import irc.client # type: ignore
|
import irc.client
|
||||||
from irc.client import NickMask
|
from irc.client import Connection, Event, NickMask
|
||||||
|
|
||||||
from edmond.log import Logger
|
from edmond.log import Logger
|
||||||
|
from edmond.plugin import Plugin
|
||||||
|
|
||||||
|
|
||||||
class Bot(irc.client.SimpleIRCClient, Logger):
|
class Bot(irc.client.SimpleIRCClient, Logger):
|
||||||
|
|
||||||
CHANNELS_RUNTIME_KEY = "_channels"
|
CHANNELS_RUNTIME_KEY = "_channels"
|
||||||
|
|
||||||
def __init__(self, config, logger):
|
def __init__(self, config: dict, logger):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.config = config
|
self.config: dict = config
|
||||||
self.logger = logger
|
self.logger = logger
|
||||||
self.plugins = []
|
self.plugins: list[Plugin] = []
|
||||||
self.values = {}
|
self.values: dict[str, Any] = {}
|
||||||
self.storage = self.__get_storage()
|
self.storage: dict[str, Any] = self.get_storage()
|
||||||
self.done = False
|
self.done: bool = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nick(self):
|
def nick(self) -> str:
|
||||||
"""Nickname validated by the server, or the configured nick."""
|
"""Nickname validated by the server, or the configured nick."""
|
||||||
if self.connection.is_connected():
|
if self.connection.is_connected():
|
||||||
return self.connection.get_nickname()
|
return self.connection.get_nickname()
|
||||||
return self.config["nick"]
|
return self.config["nick"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def names(self):
|
def names(self) -> Iterable[str]:
|
||||||
"""Collection of names the bot should identify with."""
|
"""Collection of names the bot should identify with."""
|
||||||
return (self.nick, *self.config["alternative_nicks"])
|
return (self.nick, *self.config["alternative_nicks"])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def channels(self):
|
def channels(self) -> list[str]:
|
||||||
"""List of joined channels."""
|
"""List of joined channels."""
|
||||||
if self.CHANNELS_RUNTIME_KEY not in self.values:
|
if self.CHANNELS_RUNTIME_KEY not in self.values:
|
||||||
self.values[self.CHANNELS_RUNTIME_KEY] = []
|
self.values[self.CHANNELS_RUNTIME_KEY] = []
|
||||||
return self.values[self.CHANNELS_RUNTIME_KEY]
|
return self.values[self.CHANNELS_RUNTIME_KEY]
|
||||||
|
|
||||||
def __get_storage(self):
|
def get_storage(self) -> dict:
|
||||||
"""Load data from storage."""
|
"""Load data from storage."""
|
||||||
try:
|
try:
|
||||||
with open(self.config["storage_file"], "rt") as storage_file:
|
with open(self.config["storage_file"], "rt") as storage_file:
|
||||||
|
@ -58,7 +60,7 @@ class Bot(irc.client.SimpleIRCClient, Logger):
|
||||||
)
|
)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def __save_storage(self):
|
def save_storage(self) -> None:
|
||||||
"""Save storage data to disk."""
|
"""Save storage data to disk."""
|
||||||
try:
|
try:
|
||||||
with open(self.config["storage_file"], "wt") as storage_file:
|
with open(self.config["storage_file"], "wt") as storage_file:
|
||||||
|
@ -67,28 +69,28 @@ class Bot(irc.client.SimpleIRCClient, Logger):
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
self.log_e(f"Could not save storage file: {exc}")
|
self.log_e(f"Could not save storage file: {exc}")
|
||||||
|
|
||||||
def on_welcome(self, connection, event):
|
def on_welcome(self, connection: Connection, event: Event):
|
||||||
"""Handle a successful connection to a server."""
|
"""Handle a successful connection to a server."""
|
||||||
self.log_i(f"Connected to server {event.source}.")
|
self.log_i(f"Connected to server {event.source}.")
|
||||||
self.run_plugin_callbacks(event)
|
self.run_plugin_callbacks(event)
|
||||||
for channel in self.config["channels"]:
|
for channel in self.config["channels"]:
|
||||||
connection.join(channel)
|
connection.join(channel)
|
||||||
|
|
||||||
def on_join(self, connection, event):
|
def on_join(self, connection: Connection, event: Event):
|
||||||
"""Handle someone, possibly the bot, joining a channel."""
|
"""Handle someone, possibly the bot, joining a channel."""
|
||||||
if event.source.nick == self.nick:
|
if event.source.nick == self.nick:
|
||||||
self.log_i(f"Joined {event.target}.")
|
self.log_i(f"Joined {event.target}.")
|
||||||
self.channels.append(event.target)
|
self.channels.append(event.target)
|
||||||
self.run_plugin_callbacks(event)
|
self.run_plugin_callbacks(event)
|
||||||
|
|
||||||
def on_part(self, connection, event):
|
def on_part(self, connection: Connection, event: Event):
|
||||||
"""Handle someone, possibly the bot, leaving a channel."""
|
"""Handle someone, possibly the bot, leaving a channel."""
|
||||||
if event.source.nick == self.nick:
|
if event.source.nick == self.nick:
|
||||||
self.log_i(f"Left {event.target} (args: {event.arguments[0]}).")
|
self.log_i(f"Left {event.target} (args: {event.arguments[0]}).")
|
||||||
self.channels.remove(event.target)
|
self.channels.remove(event.target)
|
||||||
self.run_plugin_callbacks(event)
|
self.run_plugin_callbacks(event)
|
||||||
|
|
||||||
def on_pubmsg(self, connection, event):
|
def on_pubmsg(self, connection: Connection, event: Event):
|
||||||
"""Handle a message received in a channel."""
|
"""Handle a message received in a channel."""
|
||||||
channel = event.target
|
channel = event.target
|
||||||
nick = NickMask(event.source).nick
|
nick = NickMask(event.source).nick
|
||||||
|
@ -96,7 +98,7 @@ class Bot(irc.client.SimpleIRCClient, Logger):
|
||||||
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)
|
self.run_plugin_callbacks(event)
|
||||||
|
|
||||||
def on_privmsg(self, connection, event):
|
def on_privmsg(self, connection: Connection, event: Event):
|
||||||
"""Handle a message received privately, usually like a channel msg."""
|
"""Handle a message received privately, usually like a channel msg."""
|
||||||
nick = NickMask(event.source).nick
|
nick = NickMask(event.source).nick
|
||||||
target = event.target
|
target = event.target
|
||||||
|
@ -104,7 +106,7 @@ class Bot(irc.client.SimpleIRCClient, Logger):
|
||||||
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)
|
self.run_plugin_callbacks(event)
|
||||||
|
|
||||||
def on_ping(self, connection, event):
|
def on_ping(self, connection: Connection, event: Event):
|
||||||
"""Handle a ping; can be used as a random event timer."""
|
"""Handle a ping; can be used as a random event timer."""
|
||||||
self.log_d(f"Received ping from {event.target}.")
|
self.log_d(f"Received ping from {event.target}.")
|
||||||
self.run_plugin_callbacks(event)
|
self.run_plugin_callbacks(event)
|
||||||
|
@ -131,7 +133,10 @@ class Bot(irc.client.SimpleIRCClient, Logger):
|
||||||
plugin_files = os.listdir(Path(__file__).parent / "plugins")
|
plugin_files = os.listdir(Path(__file__).parent / "plugins")
|
||||||
plugin_names = map(
|
plugin_names = map(
|
||||||
lambda f: os.path.splitext(f)[0],
|
lambda f: os.path.splitext(f)[0],
|
||||||
filter(lambda f: f.endswith(".py"), plugin_files),
|
filter(
|
||||||
|
lambda f: f.endswith(".py") and f != "__init__.py",
|
||||||
|
plugin_files,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
for plugin_name in plugin_names:
|
for plugin_name in plugin_names:
|
||||||
module = importlib.import_module(f"edmond.plugins.{plugin_name}")
|
module = importlib.import_module(f"edmond.plugins.{plugin_name}")
|
||||||
|
@ -149,11 +154,12 @@ class Bot(irc.client.SimpleIRCClient, Logger):
|
||||||
self.values[plugin_name] = {}
|
self.values[plugin_name] = {}
|
||||||
self.log_d(f"Loaded {class_name}.")
|
self.log_d(f"Loaded {class_name}.")
|
||||||
|
|
||||||
def get_plugin(self, name):
|
def get_plugin(self, name: str) -> Optional[Plugin]:
|
||||||
"""Get a loaded plugin by its name (e.g. 'mood'), or None."""
|
"""Get a loaded plugin by its name (e.g. 'mood'), or None."""
|
||||||
return next(filter(lambda p: p.name == name, self.plugins), None)
|
matching_plugins = filter(lambda plugin: plugin.name == name, self.plugins)
|
||||||
|
return next(matching_plugins, None)
|
||||||
|
|
||||||
def say(self, target, message):
|
def say(self, target: str, message: str) -> None:
|
||||||
"""Send message to target after a slight delay."""
|
"""Send message to target after a slight delay."""
|
||||||
message = message.replace("\n", " ").replace("\r", " ")
|
message = message.replace("\n", " ").replace("\r", " ")
|
||||||
time.sleep(self.config["speak_delay"])
|
time.sleep(self.config["speak_delay"])
|
||||||
|
@ -166,11 +172,11 @@ class Bot(irc.client.SimpleIRCClient, Logger):
|
||||||
except irc.client.MessageTooLong:
|
except irc.client.MessageTooLong:
|
||||||
self.log_e("Could not send, message is too long.")
|
self.log_e("Could not send, message is too long.")
|
||||||
|
|
||||||
def run_plugin_callbacks(self, event):
|
def run_plugin_callbacks(self, event: Event) -> None:
|
||||||
"""Run appropriate callbacks for each plugin."""
|
"""Run appropriate callbacks for each plugin."""
|
||||||
etype = event.type
|
etype = event.type
|
||||||
plugins = filter(lambda p: p.is_ready, self.plugins)
|
ready_plugins = filter(lambda p: p.is_ready, self.plugins)
|
||||||
plugins = sorted(plugins, key=lambda p: p.priority, reverse=True)
|
plugins = sorted(ready_plugins, key=lambda p: p.priority, reverse=True)
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
callbacks = plugin.callbacks
|
callbacks = plugin.callbacks
|
||||||
if etype not in callbacks:
|
if etype not in callbacks:
|
||||||
|
@ -183,7 +189,7 @@ class Bot(irc.client.SimpleIRCClient, Logger):
|
||||||
self.cleanup()
|
self.cleanup()
|
||||||
exit("Exiting after received SIGTERM.")
|
exit("Exiting after received SIGTERM.")
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self) -> None:
|
||||||
"""Save the storage file and close the connection. Run only once."""
|
"""Save the storage file and close the connection. Run only once."""
|
||||||
if self.done:
|
if self.done:
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path, logger=None):
|
def load_config(config_path: str, logger=None) -> dict:
|
||||||
try:
|
try:
|
||||||
with open(config_path, "rt") as config_file:
|
with open(config_path, "rt") as config_file:
|
||||||
return json.load(config_file)
|
return json.load(config_file)
|
||||||
|
|
110
edmond/plugin.py
110
edmond/plugin.py
|
@ -1,7 +1,26 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
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:
|
class Plugin:
|
||||||
|
@ -40,14 +59,17 @@ class Plugin:
|
||||||
- -10: lowest, e.g. for random reactions usually with a very low rate
|
- -10: lowest, e.g. for random reactions usually with a very low rate
|
||||||
"""
|
"""
|
||||||
|
|
||||||
REQUIRED_CONFIGS = []
|
REQUIRED_CONFIGS: list[str] = []
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
from edmond.bot import Bot
|
||||||
self.name = self.__class__.__name__.lower()[:-6] # Remove "Plugin".
|
|
||||||
self.priority = 0
|
self.bot: Bot = bot
|
||||||
self.config = self.__get_config()
|
# self.name is the plugin name, lowercased, without the Plugin suffix.
|
||||||
self.is_ready = self.__check_config()
|
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
|
@property
|
||||||
def callbacks(self):
|
def callbacks(self):
|
||||||
|
@ -58,7 +80,7 @@ class Plugin:
|
||||||
if cb.startswith("on_") and callable(getattr(self, cb))
|
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."""
|
"""Return the plugin section from the bot config plus common values."""
|
||||||
plugins_configs = self.bot.config["plugins"]
|
plugins_configs = self.bot.config["plugins"]
|
||||||
config = plugins_configs["common"].copy()
|
config = plugins_configs["common"].copy()
|
||||||
|
@ -69,12 +91,14 @@ class Plugin:
|
||||||
res_path = Path(res_dir) / res_path
|
res_path = Path(res_dir) / res_path
|
||||||
try:
|
try:
|
||||||
with open(res_path, "rt") as res_file:
|
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:
|
except OSError:
|
||||||
self.bot.log_e(f"Could not load resource at {res_path}.")
|
self.bot.log_e(f"Could not load resource at {res_path}.")
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def __check_config(self):
|
def __check_config(self) -> bool:
|
||||||
"""Return True if the plugin config is properly setup."""
|
"""Return True if the plugin config is properly setup."""
|
||||||
missing = False
|
missing = False
|
||||||
for key in self.REQUIRED_CONFIGS:
|
for key in self.REQUIRED_CONFIGS:
|
||||||
|
@ -83,7 +107,9 @@ class Plugin:
|
||||||
missing = True
|
missing = True
|
||||||
return not missing
|
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.
|
"""Get a value from the plugin runtime dict.
|
||||||
|
|
||||||
This will get the value from the plugin namespace, but it is possible
|
This will get the value from the plugin namespace, but it is possible
|
||||||
|
@ -93,11 +119,11 @@ class Plugin:
|
||||||
ns = self.name
|
ns = self.name
|
||||||
return self.bot.values[ns].get(key, default)
|
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."""
|
"""Set a value in the plugin runtime dict."""
|
||||||
self.bot.values[self.name][key] = value
|
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.
|
"""Get a value from the plugin persistent storage.
|
||||||
|
|
||||||
This will get the value from the plugin namespace, but it is possible
|
This will get the value from the plugin namespace, but it is possible
|
||||||
|
@ -107,15 +133,16 @@ class Plugin:
|
||||||
ns = self.name
|
ns = self.name
|
||||||
return self.bot.storage.get(ns, {}).get(key, default)
|
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."""
|
"""Set a value in the plugin persistent storage."""
|
||||||
name = ns or self.name
|
name = ns or self.name
|
||||||
if name not in self.bot.storage:
|
if name not in self.bot.storage:
|
||||||
self.bot.storage[name] = {key: value}
|
self.bot.storage[name] = {key: value}
|
||||||
else:
|
else:
|
||||||
self.bot.storage[name][key] = value
|
self.bot.storage[name][key] = value
|
||||||
|
self.bot.save_storage()
|
||||||
|
|
||||||
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."""
|
"""Append a value to a list in the plugin persistent storage."""
|
||||||
if self.name not in self.bot.storage:
|
if self.name not in self.bot.storage:
|
||||||
self.bot.storage[self.name] = {key: [value]}
|
self.bot.storage[self.name] = {key: [value]}
|
||||||
|
@ -123,16 +150,18 @@ class Plugin:
|
||||||
self.bot.storage[self.name][key] = [value]
|
self.bot.storage[self.name][key] = [value]
|
||||||
else:
|
else:
|
||||||
self.bot.storage[self.name][key].append(value)
|
self.bot.storage[self.name][key].append(value)
|
||||||
|
self.bot.save_storage()
|
||||||
|
|
||||||
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."""
|
"""Remove a value from a persistent storage list."""
|
||||||
if (
|
if (
|
||||||
self.name in self.bot.storage
|
self.name in self.bot.storage
|
||||||
and key in self.bot.storage[self.name]
|
and key in self.bot.storage[self.name]
|
||||||
):
|
):
|
||||||
self.bot.storage[self.name][key].remove(value)
|
self.bot.storage[self.name][key].remove(value)
|
||||||
|
self.bot.save_storage()
|
||||||
|
|
||||||
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.
|
"""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
|
If the message starts with one of the bot's names, the rest of the
|
||||||
|
@ -159,8 +188,9 @@ class Plugin:
|
||||||
content = first_word_and_rest[1].strip()
|
content = first_word_and_rest[1].strip()
|
||||||
self.bot.log_i(f"Reading message from {self.name}: {content}")
|
self.bot.log_i(f"Reading message from {self.name}: {content}")
|
||||||
return 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.
|
"""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
|
To answer a question, the message must start with one of the bot's
|
||||||
|
@ -188,19 +218,18 @@ class Plugin:
|
||||||
return True
|
return True
|
||||||
return False
|
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 = question[len(matched) :].strip()
|
||||||
content = content.rstrip("?").rstrip()
|
content = content.rstrip("?").rstrip()
|
||||||
question = Question(preamble, content)
|
self.question = Question(preamble, content)
|
||||||
self.question = question
|
self.bot.log_i(f"Answering from plugin {self.name}: {self.question}")
|
||||||
self.bot.log_i(f"Answering from plugin {self.name}: {question}")
|
|
||||||
|
|
||||||
def should_handle_command(
|
def should_handle_command(
|
||||||
self,
|
self,
|
||||||
message,
|
message: str,
|
||||||
no_content=False,
|
no_content: bool = False,
|
||||||
exclude_conditions=None,
|
exclude_conditions: Optional[dict] = None,
|
||||||
):
|
) -> bool:
|
||||||
"""Store Command in object and return True if I should handle it.
|
"""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
|
If no_content is True, the command does not parse command contents and
|
||||||
|
@ -254,7 +283,9 @@ class Plugin:
|
||||||
return True
|
return True
|
||||||
return False
|
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.
|
"""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
|
The command raw field is always set. The ident and content fields are
|
||||||
|
@ -274,13 +305,16 @@ class Plugin:
|
||||||
ident = words[1]
|
ident = words[1]
|
||||||
content = " ".join(words[2:-1])
|
content = " ".join(words[2:-1])
|
||||||
return Command(ident, content, raw)
|
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."""
|
"""Save command in instance for further processing by the plugin."""
|
||||||
self.command = command
|
self.command = command
|
||||||
self.bot.log_i(f"Processing command from p. {self.name}: {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.
|
"""Check if handling conditions are valid.
|
||||||
|
|
||||||
Handling conditions can be specified by each plugin to create states in
|
Handling conditions can be specified by each plugin to create states in
|
||||||
|
@ -314,21 +348,3 @@ class Plugin:
|
||||||
event.target = NickMask(event.source).nick
|
event.target = NickMask(event.source).nick
|
||||||
return on_pubmsg(event)
|
return on_pubmsg(event)
|
||||||
return False
|
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 = ""
|
|
||||||
|
|
0
edmond/plugins/__init__.py
Normal file
0
edmond/plugins/__init__.py
Normal file
|
@ -40,35 +40,44 @@ class YoutubePlugin(Plugin):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def handle_commands(self, target):
|
def handle_commands(self, target):
|
||||||
if self.command.ident == self.config["commands"][0]:
|
if self.command.ident != self.config["commands"][0]:
|
||||||
try:
|
return
|
||||||
search_response = (
|
try:
|
||||||
self.youtube.search()
|
search_response = (
|
||||||
.list(
|
self.youtube.search()
|
||||||
q=self.command.content,
|
.list(
|
||||||
part="id,snippet",
|
q=self.command.content,
|
||||||
maxResults=1,
|
part="id,snippet",
|
||||||
)
|
maxResults=1,
|
||||||
.execute()
|
|
||||||
)
|
)
|
||||||
except GoogleApiError:
|
.execute()
|
||||||
self.signal_failure(target)
|
)
|
||||||
return
|
except GoogleApiError:
|
||||||
|
self.signal_failure(target)
|
||||||
|
return
|
||||||
|
|
||||||
link = ""
|
link = ""
|
||||||
for result in search_response.get("items", []):
|
icon = ""
|
||||||
if result["id"]["kind"] == "youtube#video":
|
title = ""
|
||||||
video_id = result["id"]["videoId"]
|
self.bot.log_i(str(search_response))
|
||||||
link = self.VIDEO_URL_FMT.format(video_id)
|
for result in search_response.get("items", []):
|
||||||
elif result["id"]["kind"] == "youtube#channel":
|
kind = result["id"]["kind"]
|
||||||
channel_id = result["id"]["channelId"]
|
if kind == "youtube#video":
|
||||||
link = self.CHANNEL_URL_FMT.format(channel_id)
|
video_id = result["id"]["videoId"]
|
||||||
elif result["id"]["kind"] == "youtube#playlist":
|
link = self.VIDEO_URL_FMT.format(video_id)
|
||||||
playlist_id = result["id"]["playlistId"]
|
icon = "📼"
|
||||||
link = self.PLAYLIST_URL_FMT.format(playlist_id)
|
elif kind == "youtube#channel":
|
||||||
if link:
|
channel_id = result["id"]["channelId"]
|
||||||
break
|
link = self.CHANNEL_URL_FMT.format(channel_id)
|
||||||
else:
|
icon = "📺"
|
||||||
self.signal_failure(target)
|
elif kind == "youtube#playlist":
|
||||||
return
|
playlist_id = result["id"]["playlistId"]
|
||||||
self.bot.say(target, link)
|
link = self.PLAYLIST_URL_FMT.format(playlist_id)
|
||||||
|
icon = "➕"
|
||||||
|
if link:
|
||||||
|
title = result["snippet"]["title"]
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.signal_failure(target)
|
||||||
|
return
|
||||||
|
self.bot.say(target, f"{icon} {link} {title}")
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import random
|
import random
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
def http_get(url):
|
def http_get(url: str) -> Optional[str]:
|
||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.text
|
return response.text
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def proc(proba_percentage):
|
def proc(proba_percentage: int) -> bool:
|
||||||
return random.random() < (proba_percentage / 100.0)
|
return random.random() < (proba_percentage / 100.0)
|
||||||
|
|
Loading…
Reference in a new issue