aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar hedy <[email protected]>2024-03-24 14:59:10 +0800
committerGravatar hedy <[email protected]>2024-03-24 15:16:10 +0800
commite7780bd8890aa16f2f701e387442f5c45d0bffc1 (patch)
tree5beeadda2a04d62648a112a28a83e91ecb64c77f
parentMerge pull request #2966 from python-discord/dependabot/pip/ruff-0.3.4 (diff)
Reminders: Add a button for others to opt-in to a ping
This is the initial implementation - it's currently _far_ from perfect and is very susceptible to errors. Notable features beyond the basic requirements: - Fails safely when max mentions reached (we're limited by the 2000-character limit when sending the reminder with the pings), and disables the button. - Adds an additional embed to the initial confirmation message to show who clicked on the notify button. - Edits the additional embed and disables the button when the reminder is sent. In many ways, this implementation is quite bad: - Uses an async callback to delegate the task of PATCH-ing the API to edit mentions to the `new_reminders` method. - Edits to the opt-in list embed relies on the fact that the reminder is not edited (using !remind edit) before someone clicks on the button. A trivial way to fix this would be to add another field to the site schema to store the `notification_view` in some way. - The button is neither disabled nor any edits to the embed made when the reminder is deleted before someone clicks on the button. - String splitting is used which relies on the exact format of the embed message when editing the embed to disable the button. We have to reminder to update this piece of code when adjusting its format in the future. The UX can also be improved. Currently, I can't think of a way to concisely phrase the button embed message so that it is clear that the button is for people _other than_ the reminder author. Notes: - Max reminder mentions: - Mentions are pinged directly in a discord message when the reminder is sent. This means we're limited by the 2000-char limit. If we take each User ID snowflake to be 18-characters, and considering each mention to be formated as "<@ID> " (with extra space), it results in about 90 mentions max. I've set the constant to 80 just in case. - This is not an issue when the mentions are added in through other means than the button we're adding in this commit, because the user has to use @-mentions when sending the `!remind edit` command, which is already under the discord's character limit. - Log messages are added when something unexpected occurs within the code. Hopefully this is unlikely to happen after the implementation issues listed above are solved. - The opt-in list in the second embed is separate from mentions added in the original reminder creation, or any further edits, because mentions are added by the to-be-mentioned-user, rather than by the reminder author in this way. (Even though they are stored the same way.)
-rw-r--r--bot/exts/utils/reminders.py137
1 files changed, 130 insertions, 7 deletions
diff --git a/bot/exts/utils/reminders.py b/bot/exts/utils/reminders.py
index c79e0499c..c09e427c0 100644
--- a/bot/exts/utils/reminders.py
+++ b/bot/exts/utils/reminders.py
@@ -39,6 +39,9 @@ LOCK_NAMESPACE = "reminder"
WHITELISTED_CHANNELS = Guild.reminder_whitelist
MAXIMUM_REMINDERS = 5
REMINDER_EDIT_CONFIRMATION_TIMEOUT = 60
+# The number of mentions that can be sent when a reminder arrives is limited by
+# the 2000-character message limit.
+MAXIMUM_REMINDER_MENTION_OPT_INS = 80
Mentionable = discord.Member | discord.Role
ReminderMention = UnambiguousUser | discord.Role
@@ -75,6 +78,99 @@ class ModifyReminderConfirmationView(discord.ui.View):
self.stop()
+class GetNotificationView(discord.ui.View):
+ """A button to get notified of someone else's reminder."""
+
+ def __init__(
+ self,
+ reminder: dict,
+ callback: t.Callable[[list], t.Awaitable[t.Any]],
+ ):
+ super().__init__()
+ self.reminder = reminder
+ self.callback = callback
+ self.first_click: bool = True
+ self.success_message: discord.Message | None = None
+ self.button: discord.ui.Button | None = None
+
+ @discord.ui.button(label="Notify me", style=discord.ButtonStyle.green)
+ async def button_callback(self, interaction: discord.Interaction, button: discord.ui.Button) -> None:
+ """The button callback."""
+
+ self.button = button
+
+ if interaction.user.id in self.reminder['mentions']:
+ await interaction.response.send_message(
+ "You are already in the list of mentions for that reminder!",
+ ephemeral=True,
+ )
+ return
+
+ if interaction.user.id == self.reminder['author']:
+ await interaction.response.send_message(
+ "As the author of that reminder, you will already be notified when the reminder arrives.",
+ ephemeral=True,
+ )
+ return
+
+ if len(self.reminder['mentions']) >= MAXIMUM_REMINDER_MENTION_OPT_INS:
+ await interaction.response.send_message(
+ "Sorry, this reminder has reached the maximum number of allowed mentions.",
+ ephemeral=True,
+ )
+ await self.disable_button("Maximum number of allowed mentions reached!")
+ return
+
+ new_mentions = self.reminder['mentions'] + [interaction.user.id]
+ await self.callback(new_mentions)
+
+ await interaction.response.send_message(
+ "You were successfully added to the list of mentions for that reminder.",
+ ephemeral=True
+ )
+
+ # Edit original message to show the opt-ins
+ if not self.success_message or len(self.success_message.embeds) != 2:
+ log.trace(f"Unable to update the message for the list of member opt-ins for reminder #{self.reminder['id']}.")
+ return
+
+ embeds = self.success_message.embeds
+
+ if self.first_click:
+ embeds[1].description += "\n\nThe following member(s) opted-in to be notified:\n"
+ self.first_click = False
+ embeds[1].description += f"{interaction.user.mention} "
+
+ await self.success_message.edit(embeds=embeds)
+
+ async def disable_button(self, reason: str | None = None) -> None:
+ """Disable the button."""
+ if not self.success_message or not self.button:
+ log.trace("Unable to disable the button for opting-in for the reminder notification.")
+ return
+
+ self.button.disabled = True
+
+ if not reason:
+ await self.success_message.edit(view=self)
+ return
+
+ embeds = self.success_message.embeds
+
+ if len(embeds) != 2:
+ log.trace("Unable to get the button instruction embed for editing reason of disabling the reminder notification button.")
+ return
+
+ description = embeds[1].description
+ if "\n\n" in description:
+ # The button was clicked at least once. Don't remove the text that
+ # shows the list of opt-ins.
+ embeds[1].description = reason + "\n\n"
+ embeds[1].description += description.split("\n\n", maxsplit=2)[1]
+
+ await self.success_message.edit(embeds=embeds, view=self)
+
+
class Reminders(Cog):
"""Provide in-channel reminder functionality."""
@@ -127,10 +223,12 @@ class Reminders(Cog):
async def _send_confirmation(
ctx: Context,
on_success: str,
- reminder_id: str | int
- ) -> None:
+ reminder_id: str | int,
+ embed: discord.Embed | None = None,
+ view: discord.ui.View | None = None,
+ ) -> discord.Message:
"""Send an embed confirming the reminder change was made successfully."""
- embed = discord.Embed(
+ success_embed = discord.Embed(
description=on_success,
colour=discord.Colour.green(),
title=random.choice(POSITIVE_REPLIES)
@@ -138,9 +236,12 @@ class Reminders(Cog):
footer_str = f"ID: {reminder_id}"
- embed.set_footer(text=footer_str)
+ success_embed.set_footer(text=footer_str)
- await ctx.send(embed=embed)
+ if embed:
+ return await ctx.send(embeds=[success_embed, embed], view=view)
+ else:
+ return await ctx.send(embed=success_embed, view=view)
@staticmethod
async def _check_mentions(ctx: Context, mentions: t.Iterable[Mentionable]) -> tuple[bool, str]:
@@ -251,6 +352,11 @@ class Reminders(Cog):
log.debug(f"Deleting reminder #{reminder['id']} (the user has been reminded).")
await self.bot.api_client.delete(f"bot/reminders/{reminder['id']}")
+ # Remove the button that lets others opt-in to pings.
+ view = reminder.get("notification_view")
+ if view:
+ await view.disable_button()
+
@staticmethod
async def try_get_content_from_reply(ctx: Context) -> str | None:
"""
@@ -373,13 +479,30 @@ class Reminders(Cog):
mention_string += f" and will mention {len(mentions)} other(s)"
mention_string += "!"
+ # Add a button for others to also get notified.
+ button_embed = discord.Embed(
+ description= "Click on the button to also get a ping when the reminder arrives.",
+ )
+
+ async def callback(new_mentions: list):
+ reminder['mentions'] = new_mentions
+ await self._edit_reminder(reminder['id'], { 'mentions': new_mentions })
+
+ view = GetNotificationView(reminder, callback)
+
# Confirm to the user that it worked.
- await self._send_confirmation(
+ message = await self._send_confirmation(
ctx,
on_success=mention_string,
- reminder_id=reminder["id"]
+ reminder_id=reminder["id"],
+ embed=button_embed,
+ view=view,
)
+ view.success_message = message
+
+ reminder['notification_view'] = view
+
self.schedule_reminder(reminder)
@remind_group.command(name="list")