|
39 | 39 | WHITELISTED_CHANNELS = Guild.reminder_whitelist
|
40 | 40 | MAXIMUM_REMINDERS = 5
|
41 | 41 | 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 |
42 | 46 |
|
43 | 47 | Mentionable = discord.Member | discord.Role
|
44 | 48 | ReminderMention = UnambiguousUser | discord.Role
|
@@ -75,6 +79,137 @@ async def cancel(self, interaction: Interaction, button: discord.ui.Button) -> N
|
75 | 79 | self.stop()
|
76 | 80 |
|
77 | 81 |
|
| 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 | + |
78 | 213 | class Reminders(Cog):
|
79 | 214 | """Provide in-channel reminder functionality."""
|
80 | 215 |
|
@@ -207,6 +342,18 @@ async def _reschedule_reminder(self, reminder: dict) -> None:
|
207 | 342 | log.trace(f"Scheduling new task #{reminder['id']}")
|
208 | 343 | self.schedule_reminder(reminder)
|
209 | 344 |
|
| 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 | + |
210 | 357 | @lock_arg(LOCK_NAMESPACE, "reminder", itemgetter("id"), raise_error=True)
|
211 | 358 | async def send_reminder(self, reminder: dict, expected_time: time.Timestamp | None = None) -> None:
|
212 | 359 | """Send the reminder."""
|
@@ -360,19 +507,19 @@ async def new_reminder(
|
360 | 507 | )
|
361 | 508 |
|
362 | 509 | 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}!" |
368 | 511 |
|
369 | 512 | # Confirm to the user that it worked.
|
370 | 513 | await self._send_confirmation(
|
371 | 514 | ctx,
|
372 |
| - on_success=mention_string, |
| 515 | + on_success=success_message, |
373 | 516 | reminder_id=reminder["id"]
|
374 | 517 | )
|
375 | 518 |
|
| 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 | + |
376 | 523 | self.schedule_reminder(reminder)
|
377 | 524 |
|
378 | 525 | @remind_group.command(name="list")
|
|
0 commit comments