Skip to content

Commit 2101946

Browse files
Merge pull request #2973 from python-discord/feat/reminder-add-notify
Button for others to opt-in to be notified for reminders
2 parents 0f18d70 + 964ee58 commit 2101946

File tree

1 file changed

+153
-6
lines changed

1 file changed

+153
-6
lines changed

bot/exts/utils/reminders.py

Lines changed: 153 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
WHITELISTED_CHANNELS = Guild.reminder_whitelist
4040
MAXIMUM_REMINDERS = 5
4141
REMINDER_EDIT_CONFIRMATION_TIMEOUT = 60
42+
REMINDER_MENTION_BUTTON_TIMEOUT = 5*60
43+
# The number of mentions that can be sent when a reminder arrives is limited by
44+
# the 2000-character message limit.
45+
MAXIMUM_REMINDER_MENTION_OPT_INS = 80
4246

4347
Mentionable = discord.Member | discord.Role
4448
ReminderMention = UnambiguousUser | discord.Role
@@ -75,6 +79,137 @@ async def cancel(self, interaction: Interaction, button: discord.ui.Button) -> N
7579
self.stop()
7680

7781

82+
class OptInReminderMentionView(discord.ui.View):
83+
"""A button to opt-in to get notified of someone else's reminder."""
84+
85+
def __init__(self, cog: "Reminders", reminder: dict, expiration: Duration):
86+
super().__init__()
87+
88+
self.cog = cog
89+
self.reminder = reminder
90+
91+
self.timeout = min(
92+
(expiration - datetime.now(UTC)).total_seconds(),
93+
REMINDER_MENTION_BUTTON_TIMEOUT
94+
)
95+
96+
async def get_embed(
97+
self,
98+
message: str = "Click on the button to add yourself to the list of mentions."
99+
) -> discord.Embed:
100+
"""Return an embed to show the button together with."""
101+
description = "The following user(s) will be notified when the reminder arrives:\n"
102+
description += " ".join([
103+
mentionable.mention async for mentionable in self.cog.get_mentionables(
104+
[self.reminder["author"]] + self.reminder["mentions"]
105+
)
106+
])
107+
108+
if message:
109+
description += f"\n\n{message}"
110+
111+
return discord.Embed(description=description)
112+
113+
@discord.ui.button(emoji="🔔", label="Notify me", style=discord.ButtonStyle.green)
114+
async def button_callback(self, interaction: Interaction, button: discord.ui.Button) -> None:
115+
"""The button callback."""
116+
# This is required in case the reminder was edited/deleted between
117+
# creation and the opt-in button click.
118+
try:
119+
api_response = await self.cog.bot.api_client.get(f"bot/reminders/{self.reminder['id']}")
120+
except ResponseCodeError as e:
121+
await self.handle_api_error(interaction, button, e)
122+
return
123+
124+
self.reminder = api_response
125+
126+
# Check whether the user should be added.
127+
if interaction.user.id == self.reminder["author"]:
128+
await interaction.response.send_message(
129+
"As the author of that reminder, you will already be notified when the reminder arrives.",
130+
ephemeral=True,
131+
)
132+
return
133+
134+
if interaction.user.id in self.reminder["mentions"]:
135+
await interaction.response.send_message(
136+
"You are already in the list of mentions for that reminder.",
137+
ephemeral=True,
138+
delete_after=5,
139+
)
140+
return
141+
142+
if len(self.reminder["mentions"]) >= MAXIMUM_REMINDER_MENTION_OPT_INS:
143+
await interaction.response.send_message(
144+
"Sorry, this reminder has reached the maximum number of allowed mentions.",
145+
ephemeral=True,
146+
delete_after=5,
147+
)
148+
await self.disable(interaction, button, "Maximum number of allowed mentions reached!")
149+
return
150+
151+
# Add the user to the list of mentions.
152+
try:
153+
api_response = await self.cog.add_mention_opt_in(self.reminder, interaction.user.id)
154+
except ResponseCodeError as e:
155+
await self.handle_api_error(interaction, button, e)
156+
return
157+
158+
self.reminder = api_response
159+
160+
# Confirm that it was successful.
161+
await interaction.response.send_message(
162+
"You were successfully added to the list of mentions for that reminder.",
163+
ephemeral=True,
164+
delete_after=5,
165+
)
166+
167+
# Update the embed to show the new list of mentions.
168+
await interaction.message.edit(embed=await self.get_embed())
169+
170+
async def handle_api_error(
171+
self,
172+
interaction: Interaction,
173+
button: discord.ui.Button,
174+
error: ResponseCodeError
175+
) -> None:
176+
"""Handle a ResponseCodeError from the API responsibly."""
177+
log.trace(f"API returned {error.status} for reminder #{self.reminder['id']}.")
178+
179+
if error.status == 404:
180+
# This might happen if the reminder was edited to arrive before the
181+
# button was initially scheduled to timeout.
182+
await interaction.response.send_message(
183+
"This reminder was either deleted or has already arrived.",
184+
ephemeral=True,
185+
delete_after=5,
186+
)
187+
# Don't delete the whole interaction message here or the user will
188+
# see the above response message seemingly without context.
189+
await self.disable(interaction, button)
190+
191+
else:
192+
await interaction.response.send_message(
193+
"Sorry, an unexpected error occurred when performing this operation.\n"
194+
"Please create your own reminder instead.",
195+
ephemeral=True,
196+
delete_after=5,
197+
)
198+
await self.disable(
199+
interaction,
200+
button,
201+
"An unexpected error occurred when attempting to add users."
202+
)
203+
204+
async def disable(self, interaction: Interaction, button: discord.ui.Button, reason: str = "") -> None:
205+
"""Disable the button and add an optional reason to the original interaction message."""
206+
button.disabled = True
207+
await interaction.message.edit(
208+
embed=await self.get_embed(reason),
209+
view=self,
210+
)
211+
212+
78213
class Reminders(Cog):
79214
"""Provide in-channel reminder functionality."""
80215

@@ -207,6 +342,18 @@ async def _reschedule_reminder(self, reminder: dict) -> None:
207342
log.trace(f"Scheduling new task #{reminder['id']}")
208343
self.schedule_reminder(reminder)
209344

345+
@lock_arg(LOCK_NAMESPACE, "reminder", itemgetter("id"), raise_error=True)
346+
async def add_mention_opt_in(self, reminder: dict, user_id: int) -> dict:
347+
"""Add an opt-in user to a reminder's mentions and return the edited reminder."""
348+
if user_id in reminder["mentions"] or user_id == reminder["author"]:
349+
return reminder
350+
351+
reminder["mentions"].append(user_id)
352+
reminder = await self._edit_reminder(reminder["id"], {"mentions": reminder["mentions"]})
353+
354+
await self._reschedule_reminder(reminder)
355+
return reminder
356+
210357
@lock_arg(LOCK_NAMESPACE, "reminder", itemgetter("id"), raise_error=True)
211358
async def send_reminder(self, reminder: dict, expected_time: time.Timestamp | None = None) -> None:
212359
"""Send the reminder."""
@@ -360,19 +507,19 @@ async def new_reminder(
360507
)
361508

362509
formatted_time = time.discord_timestamp(expiration, time.TimestampFormats.DAY_TIME)
363-
mention_string = f"Your reminder will arrive on {formatted_time}"
364-
365-
if mentions:
366-
mention_string += f" and will mention {len(mentions)} other(s)"
367-
mention_string += "!"
510+
success_message = f"Your reminder will arrive on {formatted_time}!"
368511

369512
# Confirm to the user that it worked.
370513
await self._send_confirmation(
371514
ctx,
372-
on_success=mention_string,
515+
on_success=success_message,
373516
reminder_id=reminder["id"]
374517
)
375518

519+
# Add a button for others to also get notified.
520+
view = OptInReminderMentionView(self, reminder, expiration)
521+
await ctx.send(embed=await view.get_embed(), view=view, delete_after=view.timeout)
522+
376523
self.schedule_reminder(reminder)
377524

378525
@remind_group.command(name="list")

0 commit comments

Comments
 (0)