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) | 
