Compare commits

..

13 commits

Author SHA1 Message Date
dece a7d3a18ea7 playlist_of_the_day: add plugin
Simply support adding lines from other plugins, the playlist can't be
posted yet.
2022-09-09 19:08:46 +02:00
dece e601a77f72 plugin: allow plugins to skip write of storage v.
Plugins may want to write several values at once and skip the first
writes to only save the values at the last call.
2022-09-09 19:05:56 +02:00
dece 255cdcaac2 plugin: clean the runtime/storage API 2022-09-09 17:54:18 +02:00
dece 8d9ccd8dc9 wikipedia: fix crash 2022-09-04 15:27:13 +02:00
dece e8a18c6a90 plus: add shortcut 2022-09-04 15:26:52 +02:00
dece 0f31807909 shrlok: update and clean plugin (and taxref) 2022-09-03 20:43:56 +02:00
dece 801471efb2 taxref: complete showing image gallery if possible 2022-09-02 18:36:41 +02:00
dece 33dab48706 plus: remove handler once it has been consumed 2022-09-02 11:33:14 +02:00
dece 95cc864474 wikipedia: add plus feature to "science" as well 2022-09-02 10:51:40 +02:00
dece 5caa857df7 plus: new plugin 2022-09-01 18:54:28 +02:00
dece bcdf7db830 doupsland: more items 2022-09-01 18:54:28 +02:00
dece dfed9d5be5 capture: add debug log because of issues 2022-09-01 18:54:28 +02:00
dece 1639ffb3ee doupsland: new plugin for kada 2022-09-01 18:54:28 +02:00
4 changed files with 181 additions and 40 deletions

View file

@ -147,6 +147,9 @@
"positive": ["I like it."], "positive": ["I like it."],
"negative": ["I don't like it."] "negative": ["I don't like it."]
}, },
"playlistoftheday": {
"commands": ["playlist of the day"]
},
"plus": { "plus": {
"commands": ["plus"], "commands": ["plus"],
"aliases": { "aliases": {

View file

@ -46,9 +46,21 @@ class Plugin:
can 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 if `is_ready` is True, else accessing its
content is undefined behaviour. content is undefined behaviour.
Plugins may use two types of special values, stored in two dicts: runtime
values and storage values. Runtime values can be accessed by other plugins
to get information about the runtime state of the bot. Currently it is used
to store if the bot is asleep or awake by the sleep plugin, and its current
mood by the mood plugin. They are lost when the bot shuts down. Storage
values on the other hand are written in a storage file everytime the object
is modified during runtime and also when the bot shuts down. Storage values
are stored as JSON, so values must be JSON-encodable. Storage values should
be used everytime information needs to persist over restarts instead of
external files. Access here means read and write, for both runtime and
storage values.
Plugins can have priorities and calling their callbacks will respect it. Plugins can have priorities and calling their callbacks will respect it.
For now these levels are used: For now these levels are used:
- 0: default - 0: default
@ -108,58 +120,90 @@ class Plugin:
return not missing return not missing
def get_runtime_value( def get_runtime_value(
self, key: str, default: Any = None, ns: str = None self,
key: str,
default: Any = None,
ns: Optional[str] = None,
) -> Any: ) -> 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
to get runtime values from other plugins using their name as `ns`. to get runtime values from other plugins using their name as `ns`.
""" """
if ns is None: name = ns or self.name
ns = self.name return self.bot.values[name].get(key, default)
return self.bot.values[ns].get(key, default)
def set_runtime_value(self, key: str, value: Any) -> Any: def set_runtime_value(
self,
key: str,
value: Any,
ns: Optional[str] = None,
) -> None:
"""Set a value in the plugin runtime dict.""" """Set a value in the plugin runtime dict."""
self.bot.values[self.name][key] = value name = ns or self.name
self.bot.values[name][key] = value
def get_storage_value(self, key: str, default=None, ns: str = None) -> Any: def get_storage_value(
self,
key: str,
default: Any = None,
ns: Optional[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
to get storage values from other plugins using their name as `ns`. to get storage values from other plugins using their name as `ns`.
""" """
if ns is None: name = ns or self.name
ns = self.name return self.bot.storage.get(name, {}).get(key, default)
return self.bot.storage.get(ns, {}).get(key, default)
def set_storage_value(self, key: str, value: Any, ns: str = None) -> None: def set_storage_value(
self,
key: str,
value: Any,
ns: Optional[str] = None,
skip_save: bool = False,
) -> 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() if not skip_save:
self.bot.save_storage()
def append_storage_list_value(self, key: str, value: Any) -> None: def append_storage_list_value(
self,
key: str,
value: Any,
ns: str = None,
skip_save: bool = False,
) -> 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: name = ns or self.name
self.bot.storage[self.name] = {key: [value]} if name not in self.bot.storage:
elif key not in self.bot.storage[self.name]: self.bot.storage[name] = {key: [value]}
self.bot.storage[self.name][key] = [value] elif key not in self.bot.storage[name]:
self.bot.storage[name][key] = [value]
else: else:
self.bot.storage[self.name][key].append(value) self.bot.storage[name][key].append(value)
self.bot.save_storage() if not skip_save:
self.bot.save_storage()
def remove_storage_list_value(self, key: str, value: Any) -> None: def remove_storage_list_value(
self,
key: str,
value: Any,
ns: Optional[str] = None,
skip_save: bool = False,
) -> None:
"""Remove a value from a persistent storage list.""" """Remove a value from a persistent storage list."""
if ( name = ns or self.name
self.name in self.bot.storage if name in self.bot.storage and key in self.bot.storage[name]:
and key in self.bot.storage[self.name] self.bot.storage[name][key].remove(value)
): if not skip_save:
self.bot.storage[self.name][key].remove(value) self.bot.save_storage()
self.bot.save_storage()
def should_read_message(self, message: str) -> Optional[str]: 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.

View file

@ -0,0 +1,66 @@
import datetime
from edmond.plugin import Plugin
class PlaylistOfTheDayPlugin(Plugin):
"""Collect music links from other platforms.
Plugins that want to feed the playlist must do so by calling the add_link method of
the plugin. Later this plugin may feed its playlist using links unhandled by other,
more specialized plugins (bandcamp, soundcloud, ).
The current behaviour for the playlist is that links are added for a same day, so
the date at which the latest link was added is also stored. If a link is the first
to be added on a new day (by using this date as reference), the previous list is
emptied. But if the list is requested even if no links have been posted today and
the playlist is therefore outdated, it is still posted by the bot, along with a
message explaining that it's not the current day's playlist.
Note that the playlist itself is just of bunch of lines, which should of course
contain links, but can also contain titles and other metadata.
"""
REQUIRED_CONFIGS = ["commands"]
DATE_KEY = "date_of_latest_link"
PLAYLIST_KEY = "current_playlist"
def __init__(self, bot):
super().__init__(bot)
def on_pubmsg(self, event):
if not self.should_handle_command(event.arguments[0], no_content=True):
return False
# self.post_playlist(event.target)
return True
def add_line(self, line: str) -> None:
"""Add this line to the current playlist."""
today = datetime.date.today()
last_update_str = self.get_storage_value(self.DATE_KEY)
if last_update_str:
last_update_date = datetime.date.fromisoformat(last_update_str)
else:
last_update_date = today
if last_update_date == today:
current_playlist: list[str] = self.get_storage_value(
self.PLAYLIST_KEY,
default=[],
)
current_playlist.append(line)
else:
current_playlist = [line]
self.set_storage_value(
self.PLAYLIST_KEY,
current_playlist,
skip_save=True, # save one write
)
self.set_storage_value(
self.DATE_KEY,
today.isoformat(),
)
# def post_playlist(self):
# pass

View file

@ -1,13 +1,16 @@
import re import re
from typing import cast, Optional
try: try:
from googleapiclient.errors import Error as GoogleApiError from googleapiclient.errors import Error as GoogleApiError # type: ignore
DEPENDENCIES_FOUND = True DEPENDENCIES_FOUND = True
except ImportError: except ImportError:
DEPENDENCIES_FOUND = False DEPENDENCIES_FOUND = False
from edmond.plugin import Plugin from edmond.plugin import Plugin
from edmond.plugins.playlist_of_the_day import PlaylistOfTheDayPlugin
from edmond.plugins.youtube import YoutubePlugin
class YoutubeParserPlugin(Plugin): class YoutubeParserPlugin(Plugin):
@ -20,14 +23,29 @@ class YoutubeParserPlugin(Plugin):
def __init__(self, bot): def __init__(self, bot):
super().__init__(bot) super().__init__(bot)
self.priority = -3 self.priority = -3
self._youtube_plugin = None self._youtube_plugin: Optional[YoutubePlugin] = None
self._playlist_of_the_day_plugin: Optional[
PlaylistOfTheDayPlugin
] = None
@property @property
def youtube_plugin(self): def youtube_plugin(self) -> Optional[YoutubePlugin]:
if self._youtube_plugin is None: if self._youtube_plugin is None:
self._youtube_plugin = self.bot.get_plugin("youtube") self._youtube_plugin = cast(
YoutubePlugin,
self.bot.get_plugin("youtube"),
)
return self._youtube_plugin return self._youtube_plugin
@property
def playlist_of_the_day_plugin(self) -> Optional[PlaylistOfTheDayPlugin]:
if self._playlist_of_the_day_plugin is None:
self._playlist_of_the_day_plugin = cast(
PlaylistOfTheDayPlugin,
self.bot.get_plugin("playlistoftheday"),
)
return self._playlist_of_the_day_plugin
def on_welcome(self, _): def on_welcome(self, _):
if not (self.youtube_plugin and self.youtube_plugin.is_ready): if not (self.youtube_plugin and self.youtube_plugin.is_ready):
self.bot.log_w("Youtube plugin is not available.") self.bot.log_w("Youtube plugin is not available.")
@ -40,14 +58,22 @@ class YoutubeParserPlugin(Plugin):
for word in words: for word in words:
matched = self.VIDEO_URL_RE.match(word) matched = self.VIDEO_URL_RE.match(word)
if matched: if matched:
return self.handle_match(matched, event.target) title = self.get_video_title(matched)
if title:
self.bot.say(event.target, title)
self.add_to_playlist(word, title)
return True
else:
self.signal_failure(event.target)
return False return False
def handle_match(self, matched, target): def get_video_title(self, matched) -> Optional[str]:
if self.youtube_plugin is None:
return None
groupdict = matched.groupdict() groupdict = matched.groupdict()
code = groupdict.get("code1") or groupdict.get("code2") code = groupdict.get("code1") or groupdict.get("code2")
if not code: if not code:
return False return None
try: try:
search_response = ( search_response = (
self.youtube_plugin.youtube.videos() self.youtube_plugin.youtube.videos()
@ -55,15 +81,17 @@ class YoutubeParserPlugin(Plugin):
.execute() .execute()
) )
except GoogleApiError: except GoogleApiError:
self.signal_failure(target) return None
return False
title = "" title = ""
for result in search_response.get("items", []): for result in search_response.get("items", []):
if result["kind"] == "youtube#video": if result["kind"] == "youtube#video":
title = result["snippet"]["title"] title = result["snippet"]["title"]
break break
else: else:
self.signal_failure(target) return None
return False return title
self.bot.say(target, title)
return True def add_to_playlist(self, url: str, title: str):
if self.playlist_of_the_day_plugin is None:
return
self.playlist_of_the_day_plugin.add_line(f"{url} {title}")