aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts/utilities
diff options
context:
space:
mode:
authorGravatar wookie184 <[email protected]>2023-08-23 10:31:24 +0100
committerGravatar wookie184 <[email protected]>2023-08-23 10:31:24 +0100
commit6ebc2806b1b2737c27c090da324d85577ea67fb6 (patch)
tree1fb65b1dd1c8185bab4874dfc8fe5e272f20de3d /bot/exts/utilities
parentHandle snakes without images (diff)
parentCorrected 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.py144
-rw-r--r--bot/exts/utilities/challenges.py2
-rw-r--r--bot/exts/utilities/colour.py2
-rw-r--r--bot/exts/utilities/emoji.py2
-rw-r--r--bot/exts/utilities/reddit.py33
-rw-r--r--bot/exts/utilities/wolfram.py4
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."""