diff options
-rw-r--r-- | .github/workflows/build.yaml | 4 | ||||
-rw-r--r-- | .github/workflows/lint.yaml | 3 | ||||
-rw-r--r-- | .github/workflows/sentry_release.yaml | 4 | ||||
-rw-r--r-- | .github/workflows/status_embed.yaml | 4 | ||||
-rw-r--r-- | bot/exts/evergreen/bookmark.py | 125 | ||||
-rw-r--r-- | bot/utils/converters.py | 3 |
6 files changed, 119 insertions, 24 deletions
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index baa046ce..e857a6cf 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -8,6 +8,10 @@ on: types: - completed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build: if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 7f157da3..9e9cbbbf 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -6,6 +6,9 @@ on: - main pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: lint: diff --git a/.github/workflows/sentry_release.yaml b/.github/workflows/sentry_release.yaml index 3d15e01e..c1073386 100644 --- a/.github/workflows/sentry_release.yaml +++ b/.github/workflows/sentry_release.yaml @@ -5,6 +5,10 @@ on: branches: - main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: create_sentry_release: runs-on: ubuntu-latest diff --git a/.github/workflows/status_embed.yaml b/.github/workflows/status_embed.yaml index 28caa8c2..737efe00 100644 --- a/.github/workflows/status_embed.yaml +++ b/.github/workflows/status_embed.yaml @@ -8,6 +8,10 @@ on: types: - completed +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: status_embed: # We send the embed in the following situations: diff --git a/bot/exts/evergreen/bookmark.py b/bot/exts/evergreen/bookmark.py index f0fad0ea..29915627 100644 --- a/bot/exts/evergreen/bookmark.py +++ b/bot/exts/evergreen/bookmark.py @@ -1,3 +1,4 @@ +import asyncio import logging import random @@ -5,15 +6,84 @@ import discord from discord.ext import commands from bot.bot import Bot -from bot.constants import Colours, ERROR_REPLIES, Emojis, Icons +from bot.constants import Colours, ERROR_REPLIES, Icons from bot.utils.converters import WrappedMessageConverter log = logging.getLogger(__name__) +# Number of seconds to wait for other users to bookmark the same message +TIMEOUT = 120 +BOOKMARK_EMOJI = "📌" + class Bookmark(commands.Cog): """Creates personal bookmarks by relaying a message link to the user's DMs.""" + def __init__(self, bot: Bot): + self.bot = bot + + @staticmethod + def build_bookmark_dm(target_message: discord.Message, title: str) -> discord.Embed: + """Build the embed to DM the bookmark requester.""" + embed = discord.Embed( + title=title, + description=target_message.content, + colour=Colours.soft_green + ) + embed.add_field( + name="Wanna give it a visit?", + value=f"[Visit original message]({target_message.jump_url})" + ) + embed.set_author(name=target_message.author, icon_url=target_message.author.avatar_url) + embed.set_thumbnail(url=Icons.bookmark) + + return embed + + @staticmethod + def build_error_embed(user: discord.Member) -> discord.Embed: + """Builds an error embed for when a bookmark requester has DMs disabled.""" + return discord.Embed( + title=random.choice(ERROR_REPLIES), + description=f"{user.mention}, please enable your DMs to receive the bookmark.", + colour=Colours.soft_red + ) + + async def action_bookmark( + self, + channel: discord.TextChannel, + user: discord.Member, + target_message: discord.Message, + title: str + ) -> None: + """Sends the bookmark DM, or sends an error embed when a user bookmarks a message.""" + try: + embed = self.build_bookmark_dm(target_message, title) + await user.send(embed=embed) + except discord.Forbidden: + error_embed = self.build_error_embed(user) + await channel.send(embed=error_embed) + else: + log.info(f"{user} bookmarked {target_message.jump_url} with title '{title}'") + + @staticmethod + async def send_reaction_embed( + channel: discord.TextChannel, + target_message: discord.Message + ) -> discord.Message: + """Sends an embed, with a reaction, so users can react to bookmark the message too.""" + message = await channel.send( + embed=discord.Embed( + description=( + f"React with {BOOKMARK_EMOJI} to be sent your very own bookmark to " + f"[this message]({target_message.jump_url})." + ), + colour=Colours.soft_green + ) + ) + + await message.add_reaction(BOOKMARK_EMOJI) + return message + @commands.command(name="bookmark", aliases=("bm", "pin")) async def bookmark( self, @@ -35,29 +105,40 @@ class Bookmark(commands.Cog): await ctx.send(embed=embed) return - embed = discord.Embed( - title=title, - colour=Colours.soft_green, - description=target_message.content - ) - embed.add_field(name="Wanna give it a visit?", value=f"[Visit original message]({target_message.jump_url})") - embed.set_author(name=target_message.author, icon_url=target_message.author.avatar_url) - embed.set_thumbnail(url=Icons.bookmark) - - try: - await ctx.author.send(embed=embed) - except discord.Forbidden: - error_embed = discord.Embed( - title=random.choice(ERROR_REPLIES), - description=f"{ctx.author.mention}, please enable your DMs to receive the bookmark.", - colour=Colours.soft_red + def event_check(reaction: discord.Reaction, user: discord.Member) -> bool: + """Make sure that this reaction is what we want to operate on.""" + return ( + # Conditions for a successful pagination: + all(( + # Reaction is on this message + reaction.message.id == reaction_message.id, + # User has not already bookmarked this message + user.id not in bookmarked_users, + # Reaction is the `BOOKMARK_EMOJI` emoji + str(reaction.emoji) == BOOKMARK_EMOJI, + # Reaction was not made by the Bot + user.id != self.bot.user.id + )) ) - await ctx.send(embed=error_embed) - else: - log.info(f"{ctx.author} bookmarked {target_message.jump_url} with title '{title}'") - await ctx.message.add_reaction(Emojis.envelope) + await self.action_bookmark(ctx.channel, ctx.author, target_message, title) + + # Keep track of who has already bookmarked, so users can't spam reactions and cause loads of DMs + bookmarked_users = [ctx.author.id] + reaction_message = await self.send_reaction_embed(ctx.channel, target_message) + + while True: + try: + _, user = await self.bot.wait_for("reaction_add", timeout=TIMEOUT, check=event_check) + except asyncio.TimeoutError: + log.debug("Timed out waiting for a reaction") + break + log.trace(f"{user} has successfully bookmarked from a reaction, attempting to DM them.") + await self.action_bookmark(ctx.channel, user, target_message, title) + bookmarked_users.append(user.id) + + await reaction_message.delete() def setup(bot: Bot) -> None: """Load the Bookmark cog.""" - bot.add_cog(Bookmark()) + bot.add_cog(Bookmark(bot)) diff --git a/bot/utils/converters.py b/bot/utils/converters.py index 9e9616d8..fe2c980c 100644 --- a/bot/utils/converters.py +++ b/bot/utils/converters.py @@ -8,8 +8,7 @@ from discord.ext import commands class WrappedMessageConverter(commands.MessageConverter): """A converter that handles embed-suppressed links like <http://example.com>.""" - @staticmethod - async def convert(ctx: commands.Context, argument: str) -> discord.Message: + async def convert(self, ctx: commands.Context, argument: str) -> discord.Message: """Wrap the commands.MessageConverter to handle <> delimited message links.""" # It's possible to wrap a message in [<>] as well, and it's supported because its easy if argument.startswith("[") and argument.endswith("]"): |