diff --git a/README.md b/README.md index 724233622c4f95c90deca0e373ddc11962d0b194..5b3aab7851ca96648404101ef669a1dde4036791 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,12 @@ This is pretty easy to use with the [ansible deployment](https://github.com/span ## Usage ### Creating optionally recurring reminders: -`!remind <message containing date>` Adds a reminder by extracting the date from the text -* `!remind abolish closed-access journals at 3pm tomorrow` +`!remind|remindme|r <date> <message>` Adds a reminder * `!remind 8 hours buy more pumpkins` * `!remind 2023-11-30 15:00 befriend rats` - -`!remind <date>; <message>` Bypasses text parsing by explicitly specifying the date -* `!remind 2 days 4 hours; do something` +* `!remind abolish closed-access journals at 3pm tomorrow` +* `July 2`, `tuesday at 2pm`, `8pm`, `20 days`, `4d`, `2wk`, ... +* Dates doesn't need to be at the beginning of the string, but parsing works better if they are. `!remind [room] [every] ...` * `[room]` pings the whole room @@ -46,7 +45,7 @@ This is pretty easy to use with the [ansible deployment](https://github.com/span * `!remind cron 30 9 * * mon-fri do something` sets reminders for 9:30am, Monday through Friday. * `!remind cron` lists more examples -You can also reply to any message with `!remind ...` to get reminded about that message.\\ +You can also reply to any message with `!remind ...` to get reminded about that message.\ To get pinged by someone else's reminder, react to their message with ðŸ‘. ### Creating agenda items @@ -59,10 +58,10 @@ To get pinged by someone else's reminder, react to their message with ðŸ‘. * `subscribed` lists only reminders you are subscribed to ### Deleting reminders -Cancel reminders by removing the message creating it, unsubscribe by removing your upvote.\\ -Cancel recurring reminders by replying to the ping with `!remind cancel|delete` +Cancel reminders by removing the message creating it, unsubscribe by removing your upvote.\ +Cancel recurring reminders by replying with `!remind cancel|delete` * `!remind cancel|delete <ID>` deletes a reminder matching the 4 letter ID shown by `list` -* `!remind cancel|delete <message>` deletes a reminder *beginning with* <message> +* `!remind cancel|delete <message>` deletes a reminder **beginning with** <message> * e.g. `!remind delete buy more` would delete the reminder `buy more pumpkins` ### Rescheduling diff --git a/base-config.yaml b/base-config.yaml index 22a4eb698a1fbc18c534a9e91c5ba664b15ee4b7..e44fc98eb59f5b0cf584ef48394245021377a065 100644 --- a/base-config.yaml +++ b/base-config.yaml @@ -14,9 +14,15 @@ base_command: - remind - remindme -# Alias used to create an agenda items +# subcommands used to create an agenda items agenda_command: - agenda +- todo + +# subcommands used to cancel reminders +cancel_command: +- cancel +- delete # If verbose is true, display full confirmation messages. If false, confirm by reacting with :thumbs-up: diff --git a/reminder/bot.py b/reminder/bot.py index dbd7872f2fd53cf381bee7b6acbfa8ff8d7d268d..3aad7f2ace12d64457e5e9ffc13301db9ee57d94 100644 --- a/reminder/bot.py +++ b/reminder/bot.py @@ -27,7 +27,7 @@ from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper from .migrations import upgrade_table from .db import ReminderDatabase -from .util import validate_locale, validate_timezone, CommandSyntaxError, parse_date, CommandSyntax +from .util import validate_locale, validate_timezone, CommandSyntaxError, parse_date, CommandSyntax, make_pill from .reminder import Reminder from apscheduler.schedulers.asyncio import AsyncIOScheduler @@ -39,15 +39,16 @@ class Config(BaseProxyConfig): helper.copy("default_locale") helper.copy("base_command") helper.copy("agenda_command") + helper.copy("cancel_command") helper.copy("rate_limit_minutes") helper.copy("rate_limit") helper.copy("verbose") helper.copy("admin_power_level") class ReminderBot(Plugin): - base_command: str - base_aliases: Tuple[str, ...] + base_command: Tuple[str, ...] agenda_command: Tuple[str, ...] + cancel_command: Tuple[str, ...] default_timezone: pytz.timezone scheduler: AsyncIOScheduler reminders: Dict[EventID, Reminder] @@ -73,12 +74,14 @@ class ReminderBot(Plugin): self.reminders = await self.db.load_all(self) def on_external_config_update(self) -> None: + self.config.load_and_update() - bc = self.config["base_command"] - ac = self.config["agenda_command"] - self.base_command = bc[0] if isinstance(bc, list) else bc - self.base_aliases = tuple(bc) if isinstance(bc, list) else (bc,) - self.agenda_command = tuple(ac) if isinstance(ac, list) else (ac,) + + def config_to_tuple(list_or_str: List | str): + return tuple(list_or_str) if isinstance(list_or_str, list) else (list_or_str,) + self.base_command = config_to_tuple(self.config["base_command"]) + self.agenda_command = config_to_tuple(self.config["agenda_command"]) + self.cancel_command = config_to_tuple(self.config["cancel_command"]) # If the locale or timezone is invalid, use default one self.db.defaults.locale = self.config["default_locale"] @@ -96,8 +99,8 @@ class ReminderBot(Plugin): - @command.new(name=lambda self: self.base_command, - aliases=lambda self, alias: alias in self.base_aliases + self.agenda_command, + @command.new(name=lambda self: self.base_command[0], + aliases=lambda self, alias: alias in self.base_command + self.agenda_command, help="Create a reminder", require_subcommand=False, arg_fallthrough=False) @command.argument("room", matches="room", required=False) @command.argument("every", matches="every", required=False) @@ -114,9 +117,9 @@ class ReminderBot(Plugin): """Create a reminder or an alarm with a given target Args: evt: - room: - cron: - every: + room: if true, ping the whole room + cron: crontab syntax + every: is the reminder recurring? start_time: message: again: @@ -230,7 +233,9 @@ class ReminderBot(Plugin): # @command.new("cancel", help="Cancel a recurring reminder", aliases=("delete",)) - @create_reminder.subcommand("cancel", help="Cancel a recurring reminder", aliases=("delete",)) + @create_reminder.subcommand(name=lambda self: self.cancel_command[0], + help="Cancel a recurring reminder", + aliases=lambda self, alias: alias in self.cancel_command) @command.argument("search_text", pass_raw=True, required=False) async def cancel_reminder(self, evt: MessageEvent, search_text: str) -> None: """Cancel a reminder by replying to a reminder, or searching by either message or event ID""" @@ -251,7 +256,9 @@ class ReminderBot(Plugin): reminder = rem break else: # Display the help message - await evt.reply(CommandSyntax.REMINDER_CANCEL.value) + await evt.reply(CommandSyntax.REMINDER_CANCEL.value.format(base_command=self.base_command[0], + cancel_command=self.cancel_command[0], + cancel_aliases="|".join(self.cancel_command))) return if not reminder: @@ -313,7 +320,11 @@ class ReminderBot(Plugin): category = "**1ï¸âƒ£ One-time reminders**" else: category = "**📜 Agenda items**" - categories[category].append(f"* {short_event_id} {next_run} **{message}**") + + room_link = f"https://matrix.to/#/{reminder.room_id}" if all else "" + # creator_link = await make_pill(reminder.creator) if not my else "" + + categories[category].append(f"* {short_event_id + room_link} {next_run} **{message}**") # Upack the nested dict into a flat list of reminders seperated by category in_room_msg = " in this room" if room_id else "" @@ -326,8 +337,10 @@ class ReminderBot(Plugin): output = "\n".join(output) if not output: - output = f"You have no upcoming reminders{in_room_msg} :(" - await evt.reply(output) + await evt.reply(f"You have no upcoming reminders{in_room_msg} :(") + + await evt.reply(output + f"\n\n`!{self.base_command[0]} list [all] [my] [subscribed]`\\" + f"\n`!{self.base_command[0]} {self.cancel_command[0]} [4-letter ID or start of message]`") @@ -395,30 +408,32 @@ class ReminderBot(Plugin): def _help_message(self) -> str: return f""" -**â° Maubot [Reminder](https://github.com/maubot/reminder) plugin**\\ -TLDR: `!remind every friday 3pm take out the trash` `!remind cancel take out the trash`\\ -All commands can be called with `!{"|".join(self.base_aliases)}` +**â° Maubot [Reminder](https://github.com/MxMarx/reminder) plugin**\\ +TLDR: `!{self.base_command[0]} every friday 3pm take out the trash` `!{self.base_command[0]} {self.cancel_command[0]} take out the trash` **Creating optionally recurring reminders:** -{CommandSyntax.REMINDER_CREATE.value.format(base_command=self.base_command)} +{CommandSyntax.REMINDER_CREATE.value.format(base_command=self.base_command[0], + base_aliases="|".join(self.base_command))} **Creating agenda items** {CommandSyntax.AGENDA_CREATE.value.format(agenda_command="|".join(self.agenda_command))} **Listing active reminders** -{CommandSyntax.REMINDER_LIST.value.format(base_command=self.base_command)} +{CommandSyntax.REMINDER_LIST.value.format(base_command=self.base_command[0])} **Deleting reminders** -{CommandSyntax.REMINDER_CANCEL.value.format(base_command=self.base_command)} +{CommandSyntax.REMINDER_CANCEL.value.format(base_command=self.base_command[0], + cancel_command=self.cancel_command[0], + cancel_aliases="|".join(self.cancel_command))} **Rescheduling** -{CommandSyntax.REMINDER_RESCHEDULE.value.format(base_command=self.base_command)} +{CommandSyntax.REMINDER_RESCHEDULE.value.format(base_command=self.base_command[0])} **Settings** -{CommandSyntax.REMINDER_SETTINGS.value.format(base_command=self.base_command, +{CommandSyntax.REMINDER_SETTINGS.value.format(base_command=self.base_command[0], default_tz=self.db.defaults.timezone, default_locale=self.db.defaults.locale)} """ \ No newline at end of file diff --git a/reminder/util.py b/reminder/util.py index 7252a992cc42fafa30e5ba2029172943f667a8f0..76d9855d3fea1a8dd2627b9651d05979996334b3 100644 --- a/reminder/util.py +++ b/reminder/util.py @@ -40,13 +40,12 @@ logger = logging.getLogger(__name__) class CommandSyntax(Enum): REMINDER_CREATE = """ -`!{base_command} <message containing date>` Adds a reminder by extracting the date from the text -* `!{base_command} abolish closed-access journals at 3pm tomorrow` +`!{base_aliases} <date> <message>` Adds a reminder * `!{base_command} 8 hours buy more pumpkins` * `!{base_command} 2023-11-30 15:00 befriend rats` - -`!{base_command} <date>; <message>` Bypasses text parsing by explicitly specifying the date -* `!{base_command} 2 days 4 hours; do something` +* `!{base_command} abolish closed-access journals at 3pm tomorrow` +* `July 2`, `tuesday at 2pm`, `8pm`, `20 days`, `4d`, `2wk`, ... +* Dates doesn't need to be at the beginning of the string, but parsing works better if they are. `!{base_command} [room] [every] ...` * `[room]` pings the whole room @@ -73,14 +72,14 @@ To get pinged by someone else's reminder, react to their message with ðŸ‘. REMINDER_CANCEL = """ Cancel reminders by removing the message creating it, unsubscribe by removing your upvote.\\ -Cancel recurring reminders by replying to the ping with `!{base_command} cancel|delete` -* `!{base_command} cancel|delete <ID>` deletes a reminder matching the 4 letter ID shown by `list` -* `!{base_command} cancel|delete <message>` deletes a reminder *beginning with* <message> - * e.g. `!remind delete buy more` would delete the reminder `buy more pumpkins` +Cancel recurring reminders by replying with `!{base_command} {cancel_aliases}` +* `!{base_command} {cancel_aliases} <ID>` deletes a reminder matching the 4 letter ID shown by `list` +* `!{base_command} {cancel_aliases} <message>` deletes a reminder **beginning with** <message> + * e.g. `!remind {cancel_command} buy more` would delete the reminder `buy more pumpkins` """ REMINDER_RESCHEDULE = """ -Reminders can be rescheduled after they have fired by replying with `!{base_command} <new date>` +Reminders can be rescheduled by replying to the ping with `!{base_command} <new_date>` """ REMINDER_SETTINGS = """ @@ -90,11 +89,7 @@ Defaults are `{default_tz}` and `{default_locale}` * `!{base_command} locale [new-locale]` view or set your locale """ - SEARCH_DATE_EXAMPLES = "Example: `abolish closed-access journals at 11am on wednesday`, `8 hours buy more pumpkins`, `2023-11-30 15:00 befriend rats`" - PARSE_DATE_EXAMPLES = "Examples: `Tuesday at noon`, `8 hours`, `2023-11-30 10:15 pm`" - - - "Cancel a reminder by either redacting the message, using `!cancel <message>`, or replying to a recurring reminder with `!cancel`" + PARSE_DATE_EXAMPLES = "Examples: `Tuesday at noon`, `2023-11-30 10:15 pm`, `July 2`, `6 hours`, `8pm`, `4d`, `2wk`" CRON_EXAMPLE = """ ``` @@ -205,7 +200,7 @@ def parse_date(str_with_time: str, user_info: UserInfo, search_text: bool=False) if not date: results = search_dates(str_with_time, languages=[user_info.locale.split('-')[0]], settings=settings) if not results: - raise CommandSyntaxError("Unable to extract date from string", CommandSyntax.SEARCH_DATE_EXAMPLES) + raise CommandSyntaxError("Unable to extract date from string", CommandSyntax.PARSE_DATE_EXAMPLES) date_str, date = results[0] else: date = dateparser.parse(str_with_time, locales=[user_info.locale], settings=settings)