diff --git a/edmond/plugins/reminder.py b/edmond/plugins/reminder.py index 8bb0011..d5716ad 100644 --- a/edmond/plugins/reminder.py +++ b/edmond/plugins/reminder.py @@ -1,6 +1,7 @@ import datetime import re import string +import uuid from typing import Optional from apscheduler.triggers.date import DateTrigger @@ -9,13 +10,52 @@ from edmond.plugin import Plugin class ReminderPlugin(Plugin): - """Reminders using an async scheduler.""" + """Reminders using an async scheduler. + + Reminders can be set in a given amount of time or to a given hour. They are + kept between restarts. + """ REQUIRED_CONFIGS = [ "commands", "at_word", "in_word", "day_letter", "hour_letter", "minute_letter", "second_letter", "reminder_format", "done" ] + def on_join(self, event): + """Reschedule undelivered reminders.""" + nick = event.source.nick + if nick != self.bot.nick: + return + + joined_channel = event.target + reminder_format = self.config["reminder_format"] + saved_reminders = self.get_storage_value("reminders", []) + self.set_storage_value("reminders", []) + + for reminder in saved_reminders: + # Ignore reminders for other targets than this channel. + if reminder["target"] != joined_channel: + continue + + when = datetime.datetime.fromisoformat(reminder["when"]) + # If the timer passed, deliver it now and forget it. + if when <= datetime.datetime.now(): + message = reminder_format.format( + username=reminder["sender"], + reminder=reminder["message"] + ) + self.bot.say(reminder["target"], message) + continue + + # Else schedule it properly (but skip storage backup, we're on it). + self.schedule( + when, + reminder["message"], + reminder["sender"], + reminder["target"], + ) + + def on_pubmsg(self, event): if not self.should_handle_command(event.arguments[0]): return False @@ -43,7 +83,6 @@ class ReminderPlugin(Plugin): reminder = " ".join(words[2:]) self.setup_reminder(mode, time, reminder, sender, target) return True - # self.bot.scheduler.add_job(, 'cron', hour='1', minute='52') def parse_time(self, time: str) -> Optional[dict[str, int]]: """Parse a time request string. @@ -72,11 +111,20 @@ class ReminderPlugin(Plugin): self, mode: str, time: dict[str, int], - reminder: str, + message: str, sender: str, target: str ) -> None: - """Remind something at a given time.""" + """Remind something at a given time. + + There are two possible modes, "at" for a given time and "in" for a + given delay. In both cases, it should result to a datetime in the + future, or it will be discarded. + + Reminders are saved in the bot's storage to prevent losing reminders + between restarts. Only when a reminder is delivered it is removed from + the storage. + """ now = datetime.datetime.now() if mode == self.config["at_word"]: if "day" in time: # "day" is not supported in at mode. @@ -100,15 +148,53 @@ class ReminderPlugin(Plugin): self.signal_failure(target) return + self.schedule(when, message, sender, target) + self.bot.say(target, self.config["done"]) + + def schedule( + self, + when: datetime.datetime, + message: str, + sender: str, + target: str, + ) -> None: + """Schedule the reminder to be delivered at the given datetime.""" + identifier = str(uuid.uuid4()) + # Store the reminder in case the bot shuts down before delivery. + reminder = { + "id": identifier, + "when": when.isoformat(), + "message": message, + "sender": sender, + "target": target, + } + self.append_storage_list_value("reminders", reminder) + self.bot.scheduler.add_job( self.remind, trigger=DateTrigger(when), - args=(reminder, sender, target) + args=(identifier, message, sender, target) ) - self.bot.log_d(f"Scheduled for {when}, time was {time}.") - self.bot.say(target, self.config["done"]) + self.bot.log_d(f"Scheduled {identifier} for {when}.") - async def remind(self, reminder: str, username: str, target: str) -> None: + async def remind( + self, + identifier: str, + reminder: str, + username: str, + target: str + ) -> None: + self.bot.log_d( + f"Delivering reminder {identifier} in {target} for {username}…" + ) reminder_format = self.config["reminder_format"] message = reminder_format.format(username=username, reminder=reminder) self.bot.say(target, message) + + # Remove the reminder from the saved reminder list. + saved_reminders = self.get_storage_value("reminders", []) + for reminder in saved_reminders.copy(): + if reminder["id"] == identifier: + saved_reminders.remove(reminder) + break + self.set_storage_value("reminders", saved_reminders)