diff options
| -rw-r--r-- | bot/cogs/reminders.py | 143 | ||||
| -rw-r--r-- | bot/utils/messages.py | 16 |
2 files changed, 82 insertions, 77 deletions
diff --git a/bot/cogs/reminders.py b/bot/cogs/reminders.py index 5f76164cd..b5998cc0e 100644 --- a/bot/cogs/reminders.py +++ b/bot/cogs/reminders.py @@ -9,14 +9,14 @@ from operator import itemgetter import discord from dateutil.parser import isoparse from dateutil.relativedelta import relativedelta -from discord import Member, Role from discord.ext.commands import Cog, Context, Greedy, group from bot.bot import Bot -from bot.constants import Guild, Icons, MODERATION_ROLES, NEGATIVE_REPLIES, POSITIVE_REPLIES, STAFF_ROLES +from bot.constants import Guild, Icons, MODERATION_ROLES, POSITIVE_REPLIES, STAFF_ROLES from bot.converters import Duration from bot.pagination import LinePaginator from bot.utils.checks import without_role_check +from bot.utils.messages import send_denial from bot.utils.scheduling import Scheduler from bot.utils.time import humanize_delta @@ -25,7 +25,7 @@ log = logging.getLogger(__name__) WHITELISTED_CHANNELS = Guild.reminder_whitelist MAXIMUM_REMINDERS = 5 -Mentionable = t.Union[Member, Role] +Mentionable = t.Union[discord.Member, discord.Role] class Reminders(Cog): @@ -58,7 +58,6 @@ class Reminders(Cog): if remind_at < now: late = relativedelta(now, remind_at) await self.send_reminder(reminder, late) - await self._delete_reminder(reminder["id"]) else: self.schedule_reminder(reminder) @@ -104,17 +103,7 @@ class Reminders(Cog): await ctx.send(embed=embed) @staticmethod - async def _send_denial(ctx: Context, reason: str) -> None: - """Send an embed denying the user from creating a reminder.""" - embed = discord.Embed() - embed.colour = discord.Colour.red() - embed.title = random.choice(NEGATIVE_REPLIES) - embed.description = reason - - await ctx.send(embed=embed) - - @staticmethod - async def allow_mentions(ctx: Context, mentions: t.List[Mentionable]) -> t.Tuple[bool, str]: + async def _check_mentions(ctx: Context, mentions: t.Iterable[Mentionable]) -> t.Tuple[bool, str]: """ Returns whether or not the list of mentions is allowed. @@ -127,11 +116,26 @@ class Reminders(Cog): if without_role_check(ctx, *STAFF_ROLES): return False, "members/roles" elif without_role_check(ctx, *MODERATION_ROLES): - return all(isinstance(mention, Member) for mention in mentions), "roles" + return all(isinstance(mention, discord.Member) for mention in mentions), "roles" else: return True, "" - def get_mentionables_from_ids(self, mention_ids: t.List[str]) -> t.Iterator[Mentionable]: + @staticmethod + async def validate_mentions(ctx: Context, mentions: t.Iterable[Mentionable]) -> bool: + """ + Filter mentions to see if the user can mention, and sends a denial if not allowed. + + Returns whether or not the validation is successful. + """ + mentions_allowed, disallowed_mentions = await Reminders._check_mentions(ctx, mentions) + + if not mentions or mentions_allowed: + return True + else: + await send_denial(ctx, f"You can't mention other {disallowed_mentions} in your reminder!") + return False + + def get_mentionables(self, mention_ids: t.List[int]) -> t.Iterator[Mentionable]: """Converts Role and Member ids to their corresponding objects if possible.""" guild = self.bot.get_guild(Guild.id) for mention_id in mention_ids: @@ -159,6 +163,19 @@ class Reminders(Cog): # Now we can remove it from the schedule list self.scheduler.cancel(reminder_id) + async def _edit_reminder(self, reminder_id: int, payload: dict) -> dict: + """ + Edits a reminder in the database given the ID and payload. + + Returns the edited reminder. + """ + # Send the request to update the reminder in the database + reminder = await self.bot.api_client.patch( + 'bot/reminders/' + str(reminder_id), + json=payload + ) + return reminder + async def _reschedule_reminder(self, reminder: dict) -> None: """Reschedule a reminder object.""" log.trace(f"Cancelling old task #{reminder['id']}") @@ -193,13 +210,14 @@ class Reminders(Cog): ) additional_mentions = ' '.join( - mentionable.mention for mentionable in self.get_mentionables_from_ids(reminder["mentions"]) + mentionable.mention for mentionable in self.get_mentionables(reminder["mentions"]) ) await channel.send( content=f"{user.mention} {additional_mentions}", embed=embed ) + await self._delete_reminder(reminder["id"]) @group(name="remind", aliases=("reminder", "reminders", "remindme"), invoke_without_command=True) async def remind_group( @@ -211,7 +229,7 @@ class Reminders(Cog): @remind_group.command(name="new", aliases=("add", "create")) async def new_reminder( self, ctx: Context, mentions: Greedy[Mentionable], expiration: Duration, *, content: str - ) -> t.Optional[discord.Message]: + ) -> None: """ Set yourself a simple reminder. @@ -222,7 +240,8 @@ class Reminders(Cog): # If they don't have permission to set a reminder in this channel if ctx.channel.id not in WHITELISTED_CHANNELS: - return await self._send_denial(ctx, "Sorry, you can't do that here!") + await send_denial(ctx, "Sorry, you can't do that here!") + return # Get their current active reminders active_reminders = await self.bot.api_client.get( @@ -235,15 +254,16 @@ class Reminders(Cog): # Let's limit this, so we don't get 10 000 # reminders from kip or something like that :P if len(active_reminders) > MAXIMUM_REMINDERS: - return await self._send_denial(ctx, "You have too many active reminders!") + await send_denial(ctx, "You have too many active reminders!") + return + + # Remove duplicate mentions + mentions = set(mentions) + mentions.discard(ctx.author) # Filter mentions to see if the user can mention members/roles - if mentions: - mentions_allowed, disallowed_mentions = await self.allow_mentions(ctx, mentions) - if not mentions_allowed: - return await self._send_denial( - ctx, f"You can't mention other {disallowed_mentions} in your reminder!" - ) + if not await self.validate_mentions(ctx, mentions): + return mention_ids = [mention.id for mention in mentions] @@ -278,7 +298,7 @@ class Reminders(Cog): self.schedule_reminder(reminder) @remind_group.command(name="list") - async def list_reminders(self, ctx: Context) -> t.Optional[discord.Message]: + async def list_reminders(self, ctx: Context) -> None: """View a paginated embed of all reminders for your user.""" # Get all the user's reminders from the database. data = await self.bot.api_client.get( @@ -306,12 +326,12 @@ class Reminders(Cog): mentions = ", ".join( # Both Role and User objects have the `name` attribute - mention.name for mention in self.get_mentionables_from_ids(mentions) + mention.name for mention in self.get_mentionables(mentions) ) mention_string = f"\n**Mentions:** {mentions}" if mentions else "" text = textwrap.dedent(f""" - **Reminder #{id_}:** *expires in {time}* (ID: {id_}) {mention_string} + **Reminder #{id_}:** *expires in {time}* (ID: {id_}){mention_string} {content} """).strip() @@ -324,7 +344,8 @@ class Reminders(Cog): # Remind the user that they have no reminders :^) if not lines: embed.description = "No active reminders could be found." - return await ctx.send(embed=embed) + await ctx.send(embed=embed) + return # Construct the embed and paginate it. embed.colour = discord.Colour.blurple() @@ -344,65 +365,37 @@ class Reminders(Cog): @edit_reminder_group.command(name="duration", aliases=("time",)) async def edit_reminder_duration(self, ctx: Context, id_: int, expiration: Duration) -> None: """ - Edit one of your reminder's expiration. + Edit one of your reminder's expiration. Expiration is parsed per: http://strftime.org/ """ - # Send the request to update the reminder in the database - reminder = await self.bot.api_client.patch( - 'bot/reminders/' + str(id_), - json={'expiration': expiration.isoformat()} - ) - - # Send a confirmation message to the channel - await self._send_confirmation( - ctx, - on_success="That reminder has been edited successfully!", - reminder_id=id_, - delivery_dt=expiration, - ) - - await self._reschedule_reminder(reminder) + await self.edit_reminder(ctx, id_, {'expiration': expiration.isoformat()}) @edit_reminder_group.command(name="content", aliases=("reason",)) async def edit_reminder_content(self, ctx: Context, id_: int, *, content: str) -> None: """Edit one of your reminder's content.""" - # Send the request to update the reminder in the database - reminder = await self.bot.api_client.patch( - 'bot/reminders/' + str(id_), - json={'content': content} - ) - - # Parse the reminder expiration back into a datetime for the confirmation message - expiration = isoparse(reminder['expiration']).replace(tzinfo=None) - - # Send a confirmation message to the channel - await self._send_confirmation( - ctx, - on_success="That reminder has been edited successfully!", - reminder_id=id_, - delivery_dt=expiration, - ) - await self._reschedule_reminder(reminder) + await self.edit_reminder(ctx, id_, {"content": content}) @edit_reminder_group.command(name="mentions", aliases=("pings",)) async def edit_reminder_mentions(self, ctx: Context, id_: int, mentions: Greedy[Mentionable]) -> None: """Edit one of your reminder's mentions.""" + # Remove duplicate mentions + mentions = set(mentions) + mentions.discard(ctx.author) + # Filter mentions to see if the user can mention members/roles - mentions_allowed, disallowed_mentions = await self.allow_mentions(ctx, mentions) - if not mentions_allowed: - return await self._send_denial( - ctx, f"You can't mention other {disallowed_mentions} in your reminder!" - ) + if not await self.validate_mentions(ctx, mentions): + return mention_ids = [mention.id for mention in mentions] - reminder = await self.bot.api_client.patch( - 'bot/reminders/' + str(id_), - json={"mentions": mention_ids} - ) + await self.edit_reminder(ctx, id_, {"mentions": mention_ids}) + + async def edit_reminder(self, ctx: Context, id_: int, payload: dict) -> None: + """Edits a reminder with the given payload, then sends a confirmation message.""" + reminder = await self._edit_reminder(id_, payload) - # Parse the reminder expiration back into a datetime for the confirmation message - expiration = isoparse(reminder['expiration']).replace(tzinfo=None) + # Parse the reminder expiration back into a datetime + expiration = isoparse(reminder["expiration"]).replace(tzinfo=None) # Send a confirmation message to the channel await self._send_confirmation( diff --git a/bot/utils/messages.py b/bot/utils/messages.py index a40a12e98..670289941 100644 --- a/bot/utils/messages.py +++ b/bot/utils/messages.py @@ -1,15 +1,17 @@ import asyncio import contextlib import logging +import random import re from io import BytesIO from typing import List, Optional, Sequence, Union -from discord import Client, Embed, File, Member, Message, Reaction, TextChannel, Webhook +from discord import Client, Colour, Embed, File, Member, Message, Reaction, TextChannel, Webhook from discord.abc import Snowflake from discord.errors import HTTPException +from discord.ext.commands import Context -from bot.constants import Emojis +from bot.constants import Emojis, NEGATIVE_REPLIES log = logging.getLogger(__name__) @@ -132,3 +134,13 @@ def sub_clyde(username: Optional[str]) -> Optional[str]: return re.sub(r"(clyd)(e)", replace_e, username, flags=re.I) else: return username # Empty string or None + + +async def send_denial(ctx: Context, reason: str) -> None: + """Send an embed denying the user with the given reason.""" + embed = Embed() + embed.colour = Colour.red() + embed.title = random.choice(NEGATIVE_REPLIES) + embed.description = reason + + await ctx.send(embed=embed) |