diff options
author | 2023-08-23 10:31:24 +0100 | |
---|---|---|
committer | 2023-08-23 10:31:24 +0100 | |
commit | 6ebc2806b1b2737c27c090da324d85577ea67fb6 (patch) | |
tree | 1fb65b1dd1c8185bab4874dfc8fe5e272f20de3d /bot/exts/utilities | |
parent | Handle snakes without images (diff) | |
parent | Corrected attribute name to fetch github url in extensions.py (#1348) (diff) |
Merge branch 'main' into snakes-cleanup
Diffstat (limited to 'bot/exts/utilities')
-rw-r--r-- | bot/exts/utilities/bookmark.py | 144 | ||||
-rw-r--r-- | bot/exts/utilities/challenges.py | 2 | ||||
-rw-r--r-- | bot/exts/utilities/colour.py | 2 | ||||
-rw-r--r-- | bot/exts/utilities/emoji.py | 2 | ||||
-rw-r--r-- | bot/exts/utilities/reddit.py | 33 | ||||
-rw-r--r-- | bot/exts/utilities/wolfram.py | 4 |
6 files changed, 138 insertions, 49 deletions
diff --git a/bot/exts/utilities/bookmark.py b/bot/exts/utilities/bookmark.py index 150dfc48..22801d14 100644 --- a/bot/exts/utilities/bookmark.py +++ b/bot/exts/utilities/bookmark.py @@ -6,10 +6,76 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Colours, ERROR_REPLIES, Icons, Roles +from bot.utils.converters import WrappedMessageConverter from bot.utils.decorators import whitelist_override log = logging.getLogger(__name__) +MESSAGE_NOT_FOUND_ERROR = ( + "You must either provide a reference to a valid message, or reply to one." + "\n\nThe lookup strategy for a message is as follows (in order):" + "\n1. Lookup by '{channel ID}-{message ID}' (retrieved by shift-clicking on 'Copy ID')" + "\n2. Lookup by message ID (the message **must** be in the current channel)" + "\n3. Lookup by message URL" +) + + +async def dm_bookmark( + target_user: discord.Member | discord.User, + target_message: discord.Message, + title: str, +) -> None: + """ + Sends the `target_message` as a bookmark to the `target_user` DMs, with `title` as the embed title. + + Raises `discord.Forbidden` if the user's DMs are closed. + """ + embed = Bookmark.build_bookmark_dm(target_message, title) + message_url_view = discord.ui.View().add_item( + discord.ui.Button(label="View Message", url=target_message.jump_url) + ) + await target_user.send(embed=embed, view=message_url_view) + log.info(f"{target_user} bookmarked {target_message.jump_url} with title {title!r}") + + +class SendBookmark(discord.ui.View): + """The button that sends a bookmark to other users.""" + + def __init__( + self, + author: discord.Member, + channel: discord.TextChannel, + target_message: discord.Message, + title: str, + ): + super().__init__() + + self.clicked = [] + self.channel = channel + self.target_message = target_message + self.title = title + + @discord.ui.button(label="Receive Bookmark", style=discord.ButtonStyle.green) + async def button_callback(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + """The button callback.""" + if interaction.user.id in self.clicked: + await interaction.response.send_message( + "You have already received a bookmark to that message.", + ephemeral=True, + ) + return + + try: + await dm_bookmark(interaction.user, self.target_message, self.title) + except discord.Forbidden: + await interaction.response.send_message( + embed=Bookmark.build_error_embed("Enable your DMs to receive the bookmark."), + ephemeral=True, + ) + else: + self.clicked.append(interaction.user.id) + await interaction.response.send_message("You have received a bookmark to that message.", ephemeral=True) + class BookmarkForm(discord.ui.Modal): """The form where a user can fill in a custom title for their bookmark & submit it.""" @@ -31,7 +97,7 @@ class BookmarkForm(discord.ui.Modal): """Sends the bookmark embed to the user with the newly chosen title.""" title = self.bookmark_title.value or self.bookmark_title.default try: - await self.dm_bookmark(interaction, self.message, title) + await dm_bookmark(interaction.user, self.message, title) except discord.Forbidden: await interaction.response.send_message( embed=Bookmark.build_error_embed("Enable your DMs to receive the bookmark."), @@ -44,24 +110,6 @@ class BookmarkForm(discord.ui.Modal): ephemeral=True, ) - async def dm_bookmark( - self, - interaction: discord.Interaction, - target_message: discord.Message, - title: str, - ) -> None: - """ - Sends the target_message as a bookmark to the interaction user's DMs. - - Raises ``discord.Forbidden`` if the user's DMs are closed. - """ - embed = Bookmark.build_bookmark_dm(target_message, title) - message_url_view = discord.ui.View().add_item( - discord.ui.Button(label="View Message", url=target_message.jump_url) - ) - await interaction.user.send(embed=embed, view=message_url_view) - log.info(f"{interaction.user} bookmarked {target_message.jump_url} with title {title!r}") - class Bookmark(commands.Cog): """Creates personal bookmarks by relaying a message link to the user's DMs.""" @@ -104,6 +152,17 @@ class Bookmark(commands.Cog): colour=Colours.soft_red, ) + @staticmethod + def build_bookmark_embed(target_message: discord.Message) -> discord.Embed: + """Build the channel embed to the bookmark requester.""" + return discord.Embed( + description=( + f"Click the button to be sent your very own bookmark to " + f"[this message]({target_message.jump_url})." + ), + colour=Colours.soft_green, + ) + async def _bookmark_context_menu_callback(self, interaction: discord.Interaction, message: discord.Message) -> None: """The callback that will be invoked upon using the bookmark's context menu command.""" permissions = interaction.channel.permissions_for(interaction.user) @@ -122,15 +181,46 @@ class Bookmark(commands.Cog): @commands.guild_only() @whitelist_override(roles=(Roles.everyone,)) @commands.cooldown(1, 30, commands.BucketType.channel) - async def bookmark(self, ctx: commands.Context) -> None: - """Teach the invoker how to use the new context-menu based command for a smooth migration.""" - await ctx.send( - embed=self.build_error_embed( - "The bookmark text command has been replaced with a context menu command!\n\n" - "To bookmark a message simply right-click (press and hold on mobile) " - "on a message, open the 'Apps' menu, and click 'Bookmark'." + async def bookmark( + self, + ctx: commands.Context, + target_message: WrappedMessageConverter | None, + *, + title: str = "Bookmark", + ) -> None: + """ + Send the author a link to the specified message via DMs. + + Members can either give a message as an argument, or reply to a message. + + Bookmarks can subsequently be deleted by using the `bookmark delete` command in DMs. + """ + target_message: discord.Message | None = target_message or getattr(ctx.message.reference, "resolved", None) + if target_message is None: + raise commands.UserInputError(MESSAGE_NOT_FOUND_ERROR) + + permissions = target_message.channel.permissions_for(ctx.author) + if not permissions.read_messages: + log.info(f"{ctx.author} tried to bookmark a message in #{target_message.channel} but has no permissions.") + embed = self.build_error_embed("You don't have permission to view this channel.") + await ctx.send(embed=embed) + return + + view = SendBookmark(ctx.author, ctx.channel, target_message, title) + try: + await dm_bookmark(ctx.author, target_message, title) + except discord.Forbidden: + error_embed = self.build_error_embed( + f"{ctx.author.mention}, please enable your DMs to receive the bookmark." ) - ) + await ctx.send(embed=error_embed) + else: + view.clicked.append(ctx.author.id) + log.info(f"{ctx.author.mention} bookmarked {target_message.jump_url} with title '{title}'") + + embed = self.build_bookmark_embed(target_message) + + await ctx.send(embed=embed, view=view, delete_after=180) @bookmark.command(name="delete", aliases=("del", "rm"), root_aliases=("unbm", "unbookmark", "dmdelete", "dmdel")) @whitelist_override(bypass_defaults=True, allow_dm=True) diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py index 2f9ac73e..6d1813bb 100644 --- a/bot/exts/utilities/challenges.py +++ b/bot/exts/utilities/challenges.py @@ -258,7 +258,7 @@ class Challenges(commands.Cog): @commands.command(aliases=["kata"]) @commands.cooldown(1, 5, commands.BucketType.user) - async def challenge(self, ctx: commands.Context, language: str = "python", *, query: str = None) -> None: + async def challenge(self, ctx: commands.Context, language: str = "python", *, query: str | None = None) -> None: """ The challenge command pulls a random kata (challenge) from codewars.com. diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 95f9ac22..b0ff8747 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -241,7 +241,7 @@ class Colour(commands.Cog): choices=self.colour_mapping.values(), score_cutoff=80 ) - colour_name = [name for name, hex_code in self.colour_mapping.items() if hex_code == match][0] + colour_name = next(name for name, hex_code in self.colour_mapping.items() if hex_code == match) except TypeError: colour_name = None return colour_name diff --git a/bot/exts/utilities/emoji.py b/bot/exts/utilities/emoji.py index ce352fe2..bbaf6d25 100644 --- a/bot/exts/utilities/emoji.py +++ b/bot/exts/utilities/emoji.py @@ -78,7 +78,7 @@ class Emojis(commands.Cog): await self.bot.invoke_help_command(ctx) @emoji_group.command(name="count", aliases=("c",)) - async def count_command(self, ctx: commands.Context, *, category_query: str = None) -> None: + async def count_command(self, ctx: commands.Context, *, category_query: str | None = None) -> None: """Returns embed with emoji category and info given by the user.""" emoji_dict = defaultdict(list) diff --git a/bot/exts/utilities/reddit.py b/bot/exts/utilities/reddit.py index cfc70d85..5dd4a377 100644 --- a/bot/exts/utilities/reddit.py +++ b/bot/exts/utilities/reddit.py @@ -20,16 +20,15 @@ from bot.utils.pagination import ImagePaginator, LinePaginator log = logging.getLogger(__name__) AccessToken = namedtuple("AccessToken", ["token", "expires_at"]) +HEADERS = {"User-Agent": "python3:python-discord/bot:1.0.0 (by /u/PythonDiscord)"} +URL = "https://www.reddit.com" +OAUTH_URL = "https://oauth.reddit.com" +MAX_RETRIES = 3 class Reddit(Cog): """Track subreddit posts and show detailed statistics about them.""" - HEADERS = {"User-Agent": "python3:python-discord/bot:1.0.0 (by /u/PythonDiscord)"} - URL = "https://www.reddit.com" - OAUTH_URL = "https://oauth.reddit.com" - MAX_RETRIES = 3 - def __init__(self, bot: Bot): self.bot = bot @@ -37,7 +36,7 @@ class Reddit(Cog): self.access_token = None self.client_auth = BasicAuth(RedditConfig.client_id.get_secret_value(), RedditConfig.secret.get_secret_value()) - # self.auto_poster_loop.start() + self.auto_poster_loop.start() async def cog_unload(self) -> None: """Stop the loop task and revoke the access token when the cog is unloaded.""" @@ -68,7 +67,7 @@ class Reddit(Cog): # Normal brackets interfere with Markdown. title = escape_markdown(title).replace("[", "⦋").replace("]", "⦌") - link = self.URL + data["permalink"] + link = URL + data["permalink"] first_page += f"**[{title.replace('*', '')}]({link})**\n" @@ -121,10 +120,10 @@ class Reddit(Cog): A token is valid for 1 hour. There will be MAX_RETRIES to get a token, after which the cog will be unloaded and a ClientError raised if retrieval was still unsuccessful. """ - for i in range(1, self.MAX_RETRIES + 1): + for i in range(1, MAX_RETRIES + 1): response = await self.bot.http_session.post( - url=f"{self.URL}/api/v1/access_token", - headers=self.HEADERS, + url=f"{URL}/api/v1/access_token", + headers=HEADERS, auth=self.client_auth, data={ "grant_type": "client_credentials", @@ -144,7 +143,7 @@ class Reddit(Cog): return log.debug( f"Failed to get an access token: status {response.status} & content type {response.content_type}; " - f"retrying ({i}/{self.MAX_RETRIES})" + f"retrying ({i}/{MAX_RETRIES})" ) await asyncio.sleep(3) @@ -159,8 +158,8 @@ class Reddit(Cog): For security reasons, it's good practice to revoke the token when it's no longer being used. """ response = await self.bot.http_session.post( - url=f"{self.URL}/api/v1/revoke_token", - headers=self.HEADERS, + url=f"{URL}/api/v1/revoke_token", + headers=HEADERS, auth=self.client_auth, data={ "token": self.access_token.token, @@ -173,7 +172,7 @@ class Reddit(Cog): else: log.warning(f"Unable to revoke access token: status {response.status}.") - async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None) -> list[dict]: + async def fetch_posts(self, route: str, *, amount: int = 25, params: dict | None = None) -> list[dict]: """A helper method to fetch a certain amount of Reddit posts at a given route.""" # Reddit's JSON responses only provide 25 posts at most. if not 25 >= amount > 0: @@ -183,11 +182,11 @@ class Reddit(Cog): if not self.access_token or self.access_token.expires_at < datetime.now(tz=UTC): await self.get_access_token() - url = f"{self.OAUTH_URL}/{route}" - for _ in range(self.MAX_RETRIES): + url = f"{OAUTH_URL}/{route}" + for _ in range(MAX_RETRIES): response = await self.bot.http_session.get( url=url, - headers={**self.HEADERS, "Authorization": f"bearer {self.access_token.token}"}, + headers=HEADERS | {"Authorization": f"bearer {self.access_token.token}"}, params=params ) if response.status == 200 and response.content_type == "application/json": diff --git a/bot/exts/utilities/wolfram.py b/bot/exts/utilities/wolfram.py index d5669c6b..e77573a7 100644 --- a/bot/exts/utilities/wolfram.py +++ b/bot/exts/utilities/wolfram.py @@ -33,8 +33,8 @@ async def send_embed( ctx: Context, message_txt: str, colour: int = Colours.soft_red, - footer: str = None, - img_url: str = None, + footer: str | None = None, + img_url: str | None = None, f: discord.File = None ) -> None: """Generate & send a response embed with Wolfram as the author.""" |