Compare commits

...

10 commits

8 changed files with 187 additions and 44 deletions

View file

@ -65,6 +65,9 @@
"sentences": ["you're breathtaking"], "sentences": ["you're breathtaking"],
"calm_rate": 100 "calm_rate": 100
}, },
"doupsland": {
"commands": ["doupsland"]
},
"horoscope": { "horoscope": {
"commands": ["horoscope"], "commands": ["horoscope"],
"meditation": "/me looks at the stars", "meditation": "/me looks at the stars",
@ -144,6 +147,13 @@
"positive": ["I like it."], "positive": ["I like it."],
"negative": ["I don't like it."] "negative": ["I don't like it."]
}, },
"plus": {
"commands": ["plus"],
"aliases": {
"plus": "more"
},
"shortcut": "+"
},
"randomchoice": { "randomchoice": {
"commands": ["choose"], "commands": ["choose"],
"separator": "or", "separator": "or",

View file

@ -37,13 +37,13 @@ class Plugin:
plugins. It can also save data using the Bot's storage feature to be plugins. It can also save data using the Bot's storage feature to be
available after a restart. available after a restart.
Initalisation should be very fast, no network connections or anything. They Initialisation should be very fast, no network connections or anything.
are initialised before connecting to the server, so their `is_ready` flag They are initialised before connecting to the server, so their `is_ready`
is set at that point. The loading order is more or less random, so a plugin flag is set at that point. The loading order is more or less random, so a
cannot assume another has been loaded during initialisation. If it wants to plugin cannot assume another has been loaded during initialisation. If it
interact with another plugin, the earliest point to do that is in the wants to interact with another plugin, the earliest point to do that is in
on_welcome callback which is called after connecting to a server, and can the on_welcome callback which is called after connecting to a server, and
disable itself by setting its own `is_ready` flag to false. can disable itself by setting its own `is_ready` flag to false.
A plugin can access its config once the base `__init__` has been called. A plugin can access its config once the base `__init__` has been called.
The configuration is valid only is `is_ready` is True, else accessing its The configuration is valid only is `is_ready` is True, else accessing its

View file

@ -26,9 +26,12 @@ class CapturePlugin(Plugin):
return False return False
if self.current_thing is not None: if self.current_thing is not None:
message = event.arguments[0] message = event.arguments[0]
if message == self.config["capture_sentence"]: capture_sentence = self.config["capture_sentence"]
if message == capture_sentence:
self.capture(event.source.nick, event.target) self.capture(event.source.nick, event.target)
return True return True
else:
self.bot.log_d(f"Capture: “{message}” != “{capture_sentence}")
return False return False
if proc(self.config["rate"]): if proc(self.config["rate"]):

View file

@ -0,0 +1,64 @@
import random
from edmond.plugin import Plugin
P_TIME = [
"ce matin", "hier soir", "aujourd'hui", "tout à l'heure", "au réveil", "",
]
P_ACTION = [
"j'ai aperçu", "j'ai caressé", "j'ai nadenade", "j'ai passé du temps avec",
"je me suis arrêté vers", "je me suis promené avec", "j'ai salué",
"j'ai approché", "j'ai suivi", "je me suis assis devant", "j'ai regardé",
"j'ai parlé avec",
]
P_SUBJ = [
"le chat", "le chat calico", "le chat noir", "le chaton",
"le chat blanc", "le chat tigré", "le chat gris", "le chat avec le cœur",
"le chat errant", "le vieux chat", "le gros chat", "le petit chat",
"le chat doux", "le beau chat",
]
P_PLACE = [
"en ville", "au port de pêche", "sur l'île", "sous l'arbre",
"au monument de pierre", "sur la plage", "sur le chemin", "dans l'herbe",
"devant l'école", "dans la petite cour",
"qui miaulait", "qui dormait", "qui était devant la boutique",
"qui voulait manger", "qui demandait le nadenade",
"sur le quai",
]
P_COORD = [
"et", "et donc", "et puis", "après quoi",
]
P_ACTION2 = [
"il a miaulé", "il s'est endormi", "il m'a remercié",
"il est resté avec moi",
"il m'a suivi", "il a reclamé un nadenade", "il est monté sur le toît",
"il s'est roulé en boule",
]
def get_title():
return " ".join((
random.choice(P_TIME),
random.choice(P_ACTION),
random.choice(P_SUBJ),
random.choice(P_PLACE),
random.choice(P_COORD),
random.choice(P_ACTION2),
)).strip().capitalize()
class DoupslandPlugin(Plugin):
REQUIRED_CONFIGS = ["commands"]
def __init__(self, bot):
super().__init__(bot)
def on_pubmsg(self, event):
if not self.should_handle_command(event.arguments[0]):
return False
reply = get_title()
self.bot.say(event.target, reply)
return True

41
edmond/plugins/plus.py Normal file
View file

@ -0,0 +1,41 @@
from edmond.plugin import Plugin
class PlusPlugin(Plugin):
"""Plugin to store additional content from other plugins.
This plugin does not do much on its own, it lets other plugins register
additional content to compute on the fly when asked, e.g. the Wikipedia
plugin can store the Web page of the definition they just gave so users
wanting to know more about a definition can use the "plus" function to get
the URL to the Web page.
There can be only one handle registered at one time by target (so by
channel, user, etc). External plugins use the `add_handler` to set the
current handler for a target.
"""
REQUIRED_CONFIGS = ["commands"]
def __init__(self, bot):
super().__init__(bot)
self.handlers = {}
self.shortcut = self.config.get("shortcut")
def on_pubmsg(self, event):
if (
(self.shortcut and event.arguments[0].strip() == self.shortcut)
or self.should_handle_command(event.arguments[0], no_content=True)
):
self.process_handler(event)
return True
return False
def process_handler(self, event):
if handler := self.handlers.pop(event.target, None):
handler(event)
else:
self.signal_failure(event.target)
def add_handler(self, target: str, handler):
self.handlers[target] = handler

View file

@ -1,5 +1,7 @@
import json
import socket import socket
from edmond.bot import Bot
from edmond.plugin import Plugin from edmond.plugin import Plugin
@ -10,7 +12,7 @@ class ShrlokPlugin(Plugin):
plugins to share longer texts through a Web portal. plugins to share longer texts through a Web portal.
""" """
def __init__(self, bot): def __init__(self, bot: Bot):
super().__init__(bot) super().__init__(bot)
self.socket = "" self.socket = ""
self.file_root = "" self.file_root = ""
@ -29,22 +31,18 @@ class ShrlokPlugin(Plugin):
self.bot.log_d("No socket path specified, shrlok plugin disabled.") self.bot.log_d("No socket path specified, shrlok plugin disabled.")
self.is_ready = False self.is_ready = False
def post(self, content_type, content_data): def post(self, header: dict, data: bytes):
header = '{{"type":"{}"}}'.format(content_type).encode() + b"\0" encoded_header = json.dumps(header).encode()
try: try:
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
sock.connect(self.socket) sock.connect(self.socket)
data = header + content_data header_and_data = encoded_header + b"\0" + data
sock.sendall(str(len(data)).encode() + b"\0" + data) length = len(header_and_data)
packet = str(length).encode() + b"\0" + header_and_data
sock.sendall(packet)
response = sock.recv(4096) response = sock.recv(4096)
except OSError as exc: except OSError as exc:
self.bot.log_e(f"Can't post data: {exc}") self.bot.log_e(f"Can't post data: {exc}")
return None return None
url = response.decode().replace(self.file_root, self.url_root, 1) url = response.decode().replace(self.file_root, self.url_root, 1)
return url return url
def post_text(self, text):
self.post("txt", text.encode())
def post_html(self, html):
self.post("html", html.encode())

View file

@ -1,15 +1,20 @@
import random import random
import urllib.parse import urllib.parse
from typing import cast, Optional
import requests import requests
from edmond.plugin import Plugin from edmond.plugin import Plugin
from edmond.plugins.shrlok import ShrlokPlugin
BASE_URL = "https://taxref.mnhn.fr/api" BASE_URL = "https://taxref.mnhn.fr/api"
IMG_FETCH_HTML = """\ IMG_FETCH_HTML = """\
<!doctype html> <!doctype html>
<html> <html>
<head><meta charset="UTF-8"/></head> <head>
<meta charset="UTF-8"/>
<style>img {{ display: block; max-width: 95%; }}</style>
</head>
<body></body> <body></body>
<script> <script>
const urls = [{}]; const urls = [{}];
@ -52,7 +57,7 @@ class TaxrefPlugin(Plugin):
self.find_scientific_name(self.command.content, event.target) self.find_scientific_name(self.command.content, event.target)
return True return True
def search_by_name(self, name, target): def search_by_name(self, name: str, target: str) -> None:
"""Get species data from a scientific name. """Get species data from a scientific name.
Try to disambiguate the results by focusing on species only and their Try to disambiguate the results by focusing on species only and their
@ -116,7 +121,7 @@ class TaxrefPlugin(Plugin):
if images_reply := self.get_images_reply(item_to_use): if images_reply := self.get_images_reply(item_to_use):
self.bot.say(target, images_reply) self.bot.say(target, images_reply)
def get_ambiguous_reply(self, items): def get_ambiguous_reply(self, items) -> str:
"""Show a reply with potential species.""" """Show a reply with potential species."""
reply = self.config["ambiguous_reply"] reply = self.config["ambiguous_reply"]
append = "" append = ""
@ -128,7 +133,7 @@ class TaxrefPlugin(Plugin):
reply += append reply += append
return reply return reply
def get_images_reply(self, item): def get_images_reply(self, item) -> Optional[str]:
"""If there are media available, return one in a message. """If there are media available, return one in a message.
If shrlok is available, return a link to an HTML page shared by shrlok. If shrlok is available, return a link to an HTML page shared by shrlok.
@ -145,32 +150,40 @@ class TaxrefPlugin(Plugin):
""" """
m_url = item.get("_links", {}).get("media", {}).get("href") m_url = item.get("_links", {}).get("media", {}).get("href")
if not m_url: if not m_url:
self.bot.log_d("No media links.")
return None return None
response = requests.get(m_url) response = requests.get(m_url)
if response.status_code != 200: if (code := response.status_code) != 200:
self.bot.log_d(f"Failed to reach media link ({code}).")
return None return None
media_data = response.json() media_data = response.json()
items = media_data.get("_embedded", {}).get("media", []) items = media_data.get("_embedded", {}).get("media", [])
if not items: if not items:
self.bot.log_d("No media found in response.")
return None return None
def get_img_url(item): def get_img_url(item) -> Optional[str]:
return item.get("_links", {}).get("file", {}).get("href") return item.get("_links", {}).get("file", {}).get("href")
if shrlok := self.bot.get_plugin("shrlok"): if shrlok := cast(ShrlokPlugin, self.bot.get_plugin("shrlok")):
if len(items) > 10: if len(items) > 10:
items = random.sample(items, 10) items = random.sample(items, 10)
urls = map(get_img_url, items) urls = map(get_img_url, items)
urls_text = ",".join(map(lambda url: f'"{url}"', urls)) urls_text = ",".join(map(lambda url: f'"{url}"', urls))
html = IMG_FETCH_HTML.format(urls_text) html = IMG_FETCH_HTML.format(urls_text).encode()
link = shrlok.post_html(html) link = shrlok.post({"type": "raw", "ext": "html"}, html)
if not link:
self.bot.log_d("shrlok plugin returned an empty string.")
else: else:
link = get_img_url(random.choice(items)) link = get_img_url(random.choice(items))
if not link:
self.bot.log_d("No link found.")
if link: if link:
return "📷 " + link return "📷 " + link
return None
def find_scientific_name(self, name, target): def find_scientific_name(self, name: str, target: str):
"""Find a corresponding scientific name for a vernacular name.""" """Find a corresponding scientific name for a vernacular name."""
name = name.lower() name = name.lower()
enc_name = urllib.parse.quote(name) enc_name = urllib.parse.quote(name)
@ -195,7 +208,7 @@ class TaxrefPlugin(Plugin):
else: else:
# More than one result? For simplicity sake, use the shrlok plugin # More than one result? For simplicity sake, use the shrlok plugin
# if available or just show an ambiguous response. # if available or just show an ambiguous response.
if shrlok := self.bot.get_plugin("shrlok"): if shrlok := cast(ShrlokPlugin, self.bot.get_plugin("shrlok")):
text = ( text = (
"\n".join( "\n".join(
( (
@ -207,7 +220,7 @@ class TaxrefPlugin(Plugin):
) )
+ "\n" + "\n"
) )
reply = shrlok.post_text(text) reply = shrlok.post({"type": "txt"}, text.encode())
else: else:
reply = self.get_ambiguous_reply(items) reply = self.get_ambiguous_reply(items)

View file

@ -38,35 +38,49 @@ class WikipediaPlugin(Plugin):
return True return True
def tell_random_summary(self, event): def tell_random_summary(self, event):
summary = "" page = None
retries = self.NUM_RETRIES retries = self.NUM_RETRIES
while retries > 0: while retries > 0:
try: try:
summary = wikipedia.summary(wikipedia.random(), sentences=1) page = wikipedia.page(title=wikipedia.random())
break break
except: # The wikipedia package can raise a lot of different stuff. except Exception as exc:
pass # The wikipedia package can raise a lot of different stuff,
# so we sort of have to catch broadly.
self.bot.log_d(f"Wikipedia exception: {exc}")
retries -= 1 retries -= 1
if summary: if page:
self.bot.say(event.target, summary) if plus_plugin := self.bot.get_plugin("plus"):
def handler(plus_event):
self.bot.say(plus_event.target, page.url)
plus_plugin.add_handler(event.target, handler)
self.bot.say(event.target, page.summary)
def tell_definition(self, event): def tell_definition(self, event):
summary = "" page = None
reply = ""
retries = self.NUM_RETRIES retries = self.NUM_RETRIES
while retries > 0: while retries > 0:
try: try:
summary = wikipedia.summary(self.command.content, sentences=1) page = wikipedia.page(title=self.command.content)
break break
except wikipedia.exceptions.DisambiguationError: except wikipedia.exceptions.DisambiguationError:
summary = self.config["ambiguous_response"] reply = self.config["ambiguous_response"]
break break
except wikipedia.exceptions.PageError: except wikipedia.exceptions.PageError:
summary = self.config["empty_response"] reply = self.config["empty_response"]
break break
except: except:
summary = self.bot.config["error_message"] reply = self.bot.config["error_message"]
# Keep trying after a slight delay. # Keep trying after a slight delay.
time.sleep(1) time.sleep(1)
retries -= 1 retries -= 1
if summary: if page:
self.bot.say(event.target, summary) reply = page.summary.split(". ", maxsplit=1)[0]
if len(reply) > 200:
reply = reply[:196] + " […]"
if plus_plugin := self.bot.get_plugin("plus"):
def handler(plus_event):
self.bot.say(plus_event.target, page.url)
plus_plugin.add_handler(event.target, handler)
self.bot.say(event.target, reply)