diff --git a/tux/cogs/admin/mail.py b/tux/cogs/admin/mail.py index 112aa7e..7b4a302 100644 --- a/tux/cogs/admin/mail.py +++ b/tux/cogs/admin/mail.py @@ -99,7 +99,8 @@ class Mail(commands.Cog): delete_after=30, ) - def _generate_password(self) -> str: + @staticmethod + def _generate_password() -> str: password = "changeme" + "".join(str(random.randint(0, 9)) for _ in range(6)) password += "".join(random.choice("!@#$%^&*") for _ in range(4)) return password @@ -161,7 +162,8 @@ class Mail(commands.Cog): delete_after=30, ) - def _extract_mailbox_info(self, result: list[dict[str, str | None]]) -> str | None: + @staticmethod + def _extract_mailbox_info(result: list[dict[str, str | None]]) -> str | None: for item in result: if "msg" in item: msg = item["msg"] @@ -173,8 +175,8 @@ class Mail(commands.Cog): return None + @staticmethod async def _send_dm( - self, interaction: discord.Interaction, member: discord.Member, mailbox_info: str, diff --git a/tux/cogs/fun/imgeffect.py b/tux/cogs/fun/imgeffect.py index fe63538..c4dcb1e 100644 --- a/tux/cogs/fun/imgeffect.py +++ b/tux/cogs/fun/imgeffect.py @@ -14,71 +14,46 @@ from tux.ui.embeds import EmbedCreator class ImgEffect(commands.Cog): def __init__(self, bot: Tux) -> None: self.bot = bot - self.allowed_mimetypes = [ - "image/jpeg", - "image/png", - ] + self.allowed_mimetypes = ["image/jpeg", "image/png"] imgeffect = app_commands.Group(name="imgeffect", description="Image effects") - @imgeffect.command( - name="deepfry", - description="Deepfry an image", - ) + @imgeffect.command(name="deepfry", description="Deepfry an image") async def deepfry(self, interaction: discord.Interaction, image: discord.Attachment) -> None: - """ - Deepfry an image. - - Parameters - ---------- - interaction : discord.Interaction - The interaction object for the command. - image : discord.File - The image to deepfry. - """ - - # check if the image is a image - logger.info(f"Content type: {image.content_type}, Filename: {image.filename}, URL: {image.url}") - - if image.content_type not in self.allowed_mimetypes: - logger.error("The file is not a permitted image.") - - embed = EmbedCreator.create_embed( - bot=self.bot, - embed_type=EmbedCreator.ERROR, - user_name=interaction.user.name, - user_display_avatar=interaction.user.display_avatar.url, - title="Invalid File", - description="The file must be an image. Allowed types are PNG, JPEG, and JPG.", - ) - - await interaction.response.send_message(embed=embed, ephemeral=True) + if not self.is_valid_image(image): + await self.send_invalid_image_response(interaction) return - # say that the image is being processed - logger.info("Processing image...") await interaction.response.defer(ephemeral=True) - # open url with PIL - logger.info("Opening image with PIL and HTTPX...") + pil_image = await self.fetch_image(image.url) + + if pil_image: + deepfried_image = self.deepfry_image(pil_image) + await self.send_deepfried_image(interaction, deepfried_image) + + else: + await self.send_error_response(interaction) + + def is_valid_image(self, image: discord.Attachment) -> bool: + logger.info(f"Content type: {image.content_type}, Filename: {image.filename}, URL: {image.url}") + + return image.content_type in self.allowed_mimetypes + + @staticmethod + async def fetch_image(url: str) -> Image.Image: + logger.info("Fetching image from URL with HTTPX...") + async with httpx.AsyncClient() as client: - response = await client.get(image.url) + response = await client.get(url) - pil_image = Image.open(io.BytesIO(response.content)) - pil_image = pil_image.convert("RGB") - logger.info("Image opened with PIL.") + return Image.open(io.BytesIO(response.content)).convert("RGB") - # resize image to 25% then back to original size - logger.info("Resizing image...") + @staticmethod + def deepfry_image(pil_image: Image.Image) -> Image.Image: pil_image = pil_image.resize((int(pil_image.width * 0.25), int(pil_image.height * 0.25))) - logger.info("Image resized.") - - # increase sharpness - logger.info("Increasing sharpness...") pil_image = ImageEnhance.Sharpness(pil_image).enhance(100.0) - logger.info("Sharpness increased.") - logger.info("Adjusting color...") r = pil_image.split()[0] r = ImageEnhance.Contrast(r).enhance(2.0) r = ImageEnhance.Brightness(r).enhance(1.5) @@ -86,14 +61,43 @@ class ImgEffect(commands.Cog): colours = ((254, 0, 2), (255, 255, 15)) r = ImageOps.colorize(r, colours[0], colours[1]) pil_image = Image.blend(pil_image, r, 0.75) - logger.info("Color adjustment complete.") - # send image - logger.info("Sending image...") - pil_image = pil_image.resize((int(pil_image.width * 4), int(pil_image.height * 4))) + return pil_image.resize((int(pil_image.width * 4), int(pil_image.height * 4))) + + async def send_invalid_image_response(self, interaction: discord.Interaction) -> None: + logger.error("The file is not a permitted image.") + + embed = EmbedCreator.create_embed( + bot=self.bot, + embed_type=EmbedCreator.ERROR, + user_name=interaction.user.name, + user_display_avatar=interaction.user.display_avatar.url, + title="Invalid File", + description="The file must be an image. Allowed types are PNG, JPEG, and JPG.", + ) + + await interaction.response.send_message(embed=embed, ephemeral=True) + + async def send_error_response(self, interaction: discord.Interaction) -> None: + logger.error("Error processing the image.") + + embed = EmbedCreator.create_embed( + bot=self.bot, + embed_type=EmbedCreator.ERROR, + user_name=interaction.user.name, + user_display_avatar=interaction.user.display_avatar.url, + title="Error", + description="An error occurred while processing the image.", + ) + + await interaction.response.send_message(embed=embed, ephemeral=True) + + @staticmethod + async def send_deepfried_image(interaction: discord.Interaction, deepfried_image: Image.Image) -> None: arr = io.BytesIO() - pil_image.save(arr, format="JPEG", quality=1) + deepfried_image.save(arr, format="JPEG", quality=1) arr.seek(0) + file = discord.File(arr, filename="deepfried.jpg") await interaction.followup.send(file=file, ephemeral=True) diff --git a/tux/cogs/fun/random.py b/tux/cogs/fun/random.py index 33462c7..ab58ea9 100644 --- a/tux/cogs/fun/random.py +++ b/tux/cogs/fun/random.py @@ -57,7 +57,13 @@ class Random(commands.Cog): aliases=["eightball", "8b"], ) @commands.guild_only() - async def eight_ball(self, ctx: commands.Context[Tux], *, question: str, cow: bool = False) -> None: + async def eight_ball( + self, + ctx: commands.Context[Tux], + *, + question: str, + cow: bool = False, + ) -> None: """ Ask the magic 8ball a question. diff --git a/tux/cogs/info/info.py b/tux/cogs/info/info.py index 4f475b5..41df0af 100644 --- a/tux/cogs/info/info.py +++ b/tux/cogs/info/info.py @@ -65,7 +65,7 @@ class Info(commands.Cog): custom_color=discord.Color.blurple(), custom_author_text="Server Information", custom_author_icon_url=guild.icon.url, - custom_footer_text=f"ID: {guild.id} | Created: {guild.created_at.strftime('%B %d, %Y')}", + custom_footer_text=f"ID: {guild.id} | Created: {guild.created_at.strftime("%B %d, %Y")}", ) .add_field(name="Owner", value=str(guild.owner.mention) if guild.owner else "Unknown") .add_field(name="Vanity URL", value=guild.vanity_url_code or "None") @@ -73,7 +73,7 @@ class Info(commands.Cog): .add_field(name="Text Channels", value=len(guild.text_channels)) .add_field(name="Voice Channels", value=len(guild.voice_channels)) .add_field(name="Forum Channels", value=len(guild.forums)) - .add_field(name="Emojis", value=f"{len(guild.emojis)}/{2*guild.emoji_limit}") + .add_field(name="Emojis", value=f"{len(guild.emojis)}/{2 * guild.emoji_limit}") .add_field(name="Stickers", value=f"{len(guild.stickers)}/{guild.sticker_limit}") .add_field(name="Roles", value=len(guild.roles)) .add_field(name="Humans", value=sum(not member.bot for member in guild.members)) @@ -213,7 +213,7 @@ class Info(commands.Cog): menu: ViewMenu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed) for chunk in chunks: page_embed: discord.Embed = embed.copy() - page_embed.description = f"{list_type.capitalize()} list for {guild_name}:\n{' '.join(chunk)}" + page_embed.description = f"{list_type.capitalize()} list for {guild_name}:\n{" ".join(chunk)}" menu.add_page(page_embed) buttons = [ @@ -229,7 +229,8 @@ class Info(commands.Cog): await menu.start() - def _chunks(self, it: Iterator[str], size: int) -> Generator[list[str], None, None]: + @staticmethod + def _chunks(it: Iterator[str], size: int) -> Generator[list[str], None, None]: """ Split an iterator into chunks of a specified size. diff --git a/tux/cogs/moderation/__init__.py b/tux/cogs/moderation/__init__.py index c2e1080..e0bc929 100644 --- a/tux/cogs/moderation/__init__.py +++ b/tux/cogs/moderation/__init__.py @@ -100,8 +100,8 @@ class ModerationCogBase(commands.Cog): if isinstance(log_channel, discord.TextChannel): await log_channel.send(embed=embed) + @staticmethod async def send_dm( - self, ctx: commands.Context[Tux], silent: bool, user: discord.Member, diff --git a/tux/cogs/moderation/cases.py b/tux/cogs/moderation/cases.py index a80e0b7..098b632 100644 --- a/tux/cogs/moderation/cases.py +++ b/tux/cogs/moderation/cases.py @@ -319,8 +319,8 @@ class Cases(ModerationCogBase): await menu.start() + @staticmethod def _create_case_fields( - self, moderator: discord.Member, user: discord.Member | discord.User, reason: str, @@ -361,7 +361,8 @@ class Cases(ModerationCogBase): return embed - def _format_emoji(self, emoji: discord.Emoji | None) -> str: + @staticmethod + def _format_emoji(emoji: discord.Emoji | None) -> str: return f"<:{emoji.name}:{emoji.id}>" if emoji else "" def _get_case_status_emoji(self, case_status: bool | None) -> discord.Emoji | None: @@ -392,16 +393,16 @@ class Cases(ModerationCogBase): def _get_case_action_emoji(self, case_type: CaseType) -> discord.Emoji | None: action = None - if case_type in [ + if case_type in { CaseType.BAN, CaseType.KICK, CaseType.TIMEOUT, CaseType.WARN, CaseType.JAIL, CaseType.SNIPPETBAN, - ]: + }: action = "added" - elif case_type in [CaseType.UNBAN, CaseType.UNTIMEOUT, CaseType.UNJAIL, CaseType.SNIPPETUNBAN]: + elif case_type in {CaseType.UNBAN, CaseType.UNTIMEOUT, CaseType.UNJAIL, CaseType.SNIPPETUNBAN}: action = "removed" if action is not None: @@ -410,8 +411,8 @@ class Cases(ModerationCogBase): return self.bot.get_emoji(emoji_id) return None + @staticmethod def _get_case_description( - self, case: Case, case_status_emoji: str, case_type_emoji: str, diff --git a/tux/cogs/moderation/jail.py b/tux/cogs/moderation/jail.py index 5a51bcb..9155ba3 100644 --- a/tux/cogs/moderation/jail.py +++ b/tux/cogs/moderation/jail.py @@ -105,8 +105,8 @@ class Jail(ModerationCogBase): dm_sent = await self.send_dm(ctx, flags.silent, member, flags.reason, "jailed") await self.handle_case_response(ctx, CaseType.JAIL, case.case_number, flags.reason, member, dm_sent) + @staticmethod def _get_manageable_roles( - self, member: discord.Member, jail_role: discord.Role, ) -> list[discord.Role]: diff --git a/tux/cogs/moderation/slowmode.py b/tux/cogs/moderation/slowmode.py index 0d4ec14..a55bd39 100644 --- a/tux/cogs/moderation/slowmode.py +++ b/tux/cogs/moderation/slowmode.py @@ -15,7 +15,8 @@ class Slowmode(commands.Cog): @commands.hybrid_command( name="slowmode", aliases=["sm"], - usage="slowmode [channel]\nor slowmode [channel] ", # only place where generate_usage shouldn't be used + # only place where generate_usage shouldn't be used: + usage="slowmode [channel]\nor slowmode [channel] ", ) @commands.guild_only() @checks.has_pl(2) @@ -83,11 +84,12 @@ class Slowmode(commands.Cog): return action, channel - def _get_channel(self, ctx: commands.Context[Tux]) -> discord.TextChannel | discord.Thread | None: + @staticmethod + def _get_channel(ctx: commands.Context[Tux]) -> discord.TextChannel | discord.Thread | None: return ctx.channel if isinstance(ctx.channel, discord.TextChannel | discord.Thread) else None + @staticmethod async def _get_slowmode( - self, ctx: commands.Context[Tux], channel: discord.TextChannel | discord.Thread, ) -> None: @@ -135,7 +137,8 @@ class Slowmode(commands.Cog): await ctx.send(f"Failed to set slowmode. Error: {error}", delete_after=30, ephemeral=True) logger.error(f"Failed to set slowmode. Error: {error}") - def _parse_delay(self, delay: str) -> int | None: + @staticmethod + def _parse_delay(delay: str) -> int | None: try: if delay.endswith("s"): delay = delay[:-1] diff --git a/tux/cogs/services/bookmarks.py b/tux/cogs/services/bookmarks.py index 7e86a8b..d1135be 100644 --- a/tux/cogs/services/bookmarks.py +++ b/tux/cogs/services/bookmarks.py @@ -52,7 +52,7 @@ class Bookmarks(commands.Cog): message: discord.Message, ) -> discord.Embed: if len(message.content) > CONST.EMBED_MAX_DESC_LENGTH: - message.content = f"{message.content[:CONST.EMBED_MAX_DESC_LENGTH - 3]}..." + message.content = f"{message.content[: CONST.EMBED_MAX_DESC_LENGTH - 3]}..." embed = EmbedCreator.create_embed( bot=self.bot, @@ -71,8 +71,8 @@ class Bookmarks(commands.Cog): return embed + @staticmethod async def _send_bookmark( - self, user: discord.User, message: discord.Message, embed: discord.Embed, diff --git a/tux/cogs/utility/run.py b/tux/cogs/utility/run.py index 72644ef..4fe8e59 100644 --- a/tux/cogs/utility/run.py +++ b/tux/cogs/utility/run.py @@ -46,7 +46,8 @@ class Run(commands.Cog): self.run.usage = generate_usage(self.run) self.languages.usage = generate_usage(self.languages) - def remove_ansi(self, ansi: str) -> str: + @staticmethod + def remove_ansi(ansi: str) -> str: """ Converts ANSI encoded text into non-ANSI. @@ -63,7 +64,8 @@ class Run(commands.Cog): return ansi_re.sub("", ansi) - def remove_backticks(self, st: str) -> str: + @staticmethod + def remove_backticks(st: str) -> str: """ Removes backticks from the provided string. @@ -277,7 +279,7 @@ class Run(commands.Cog): code, ) await msg.delete() - if filtered_output == "" and gen_one == "" and normalized_lang == "": + if not filtered_output and not gen_one and not normalized_lang: return await self.send_embedded_reply( ctx, @@ -340,7 +342,7 @@ class Run(commands.Cog): user_name=ctx.author.name, user_display_avatar=ctx.author.display_avatar.url, title="Supported Languages", - description=f"```{', '.join(compiler_map.keys())}```", + description=f"```{", ".join(compiler_map.keys())}```", ) await ctx.send(embed=embed) diff --git a/tux/cogs/utility/snippets.py b/tux/cogs/utility/snippets.py index dedfb96..8bc9c8d 100644 --- a/tux/cogs/utility/snippets.py +++ b/tux/cogs/utility/snippets.py @@ -152,7 +152,7 @@ class Snippets(commands.Cog): text = "```\n" for i, snippet in enumerate(snippets[:10]): - text += f"{i+1}. {snippet.snippet_name.ljust(20)} | uses: {snippet.uses}\n" + text += f"{i + 1}. {snippet.snippet_name.ljust(20)} | uses: {snippet.uses}\n" text += "```" # only show top 10, no pagination @@ -321,7 +321,7 @@ class Snippets(commands.Cog): embed.add_field(name="Name", value=snippet.snippet_name, inline=False) embed.add_field( name="Author", - value=f"{author.mention if author else f'<@!{snippet.snippet_user_id}>'}", + value=f"{author.mention if author else f"<@!{snippet.snippet_user_id}>"}", inline=False, ) embed.add_field(name="Content", value=f"> {snippet.snippet_content}", inline=False) @@ -495,7 +495,7 @@ class Snippets(commands.Cog): if author := self.bot.get_user(snippet.snippet_user_id): with contextlib.suppress(discord.Forbidden): await author.send( - f"""Your snippet `{snippet.snippet_name}` has been {'locked' if status.locked else 'unlocked'}. + f"""Your snippet `{snippet.snippet_name}` has been {"locked" if status.locked else "unlocked"}. **What does this mean?** If a snippet is locked, it cannot be edited by anyone other than moderators. This means that you can no longer edit this snippet. diff --git a/tux/cogs/utility/tldr.py b/tux/cogs/utility/tldr.py index b6c7ada..029fbb9 100644 --- a/tux/cogs/utility/tldr.py +++ b/tux/cogs/utility/tldr.py @@ -135,7 +135,8 @@ class Tldr(commands.Cog): return self._run_subprocess(["tldr", "--list"], "No TLDR pages found.").split("\n") - def _run_subprocess(self, command_list: list[str], default_response: str) -> str: + @staticmethod + def _run_subprocess(command_list: list[str], default_response: str) -> str: """ Helper method to run subprocesses for CLI interactions. diff --git a/tux/handlers/activity.py b/tux/handlers/activity.py index 8217e6d..683c0ed 100644 --- a/tux/handlers/activity.py +++ b/tux/handlers/activity.py @@ -13,7 +13,8 @@ class ActivityHandler(commands.Cog): self.delay = delay self.activities = self.build_activity_list() - def build_activity_list(self) -> list[discord.Activity | discord.Streaming]: + @staticmethod + def build_activity_list() -> list[discord.Activity | discord.Streaming]: activity_data = [ {"type": discord.ActivityType.watching, "name": "{member_count} members"}, {"type": discord.ActivityType.watching, "name": "All Things Linux"}, diff --git a/tux/handlers/error.py b/tux/handlers/error.py index d8132cb..5e42eb0 100644 --- a/tux/handlers/error.py +++ b/tux/handlers/error.py @@ -273,7 +273,8 @@ class ErrorHandler(commands.Cog): return error_map.get(type(error), self.error_message).format(error=error) - def log_error_traceback(self, error: Exception) -> None: + @staticmethod + def log_error_traceback(error: Exception) -> None: """ Log the error traceback. diff --git a/tux/handlers/event.py b/tux/handlers/event.py index 4f5a068..868618c 100644 --- a/tux/handlers/event.py +++ b/tux/handlers/event.py @@ -20,7 +20,8 @@ class EventHandler(commands.Cog): async def on_guild_remove(self, guild: discord.Guild) -> None: await self.db.guild.delete_guild_by_id(guild.id) - async def handle_harmful_message(self, message: discord.Message) -> None: + @staticmethod + async def handle_harmful_message(message: discord.Message) -> None: if message.author.bot: return diff --git a/tux/help.py b/tux/help.py index b854b39..bd162a3 100644 --- a/tux/help.py +++ b/tux/help.py @@ -38,7 +38,8 @@ class TuxHelp(commands.HelpCommand): return self.context.clean_prefix or CONST.DEFAULT_PREFIX - def _embed_base(self, title: str, description: str | None = None) -> discord.Embed: + @staticmethod + def _embed_base(title: str, description: str | None = None) -> discord.Embed: """ Creates a base embed with uniform styling. @@ -61,8 +62,8 @@ class TuxHelp(commands.HelpCommand): color=CONST.EMBED_COLORS["DEFAULT"], ) + @staticmethod def _add_command_field( - self, embed: discord.Embed, command: commands.Command[Any, Any, Any], prefix: str, @@ -84,11 +85,12 @@ class TuxHelp(commands.HelpCommand): embed.add_field( name=f"{prefix}{command.qualified_name} ({command_aliases})", - value=f"> {command.short_doc or 'No documentation summary.'}", + value=f"> {command.short_doc or "No documentation summary."}", inline=False, ) - def _get_flag_type(self, flag_annotation: Any) -> str: + @staticmethod + def _get_flag_type(flag_annotation: Any) -> str: """ Determines the type of a flag based on its annotation. @@ -111,7 +113,8 @@ class TuxHelp(commands.HelpCommand): case _: return str(flag_annotation) - def _format_flag_name(self, flag: commands.Flag) -> str: + @staticmethod + def _format_flag_name(flag: commands.Flag) -> str: """ Formats the flag name based on whether it is required. @@ -158,11 +161,11 @@ class TuxHelp(commands.HelpCommand): flag_str = self._format_flag_name(flag) if flag.aliases: - flag_str += f" ({', '.join(flag.aliases)})" + flag_str += f" ({", ".join(flag.aliases)})" # else: # flag_str += f" : {flag_type}" - flag_str += f"\n\t{flag.description or 'No description provided.'}" + flag_str += f"\n\t{flag.description or "No description provided."}" if flag.default is not discord.utils.MISSING: flag_str += f"\n\tDefault: {flag.default}" @@ -289,12 +292,13 @@ class TuxHelp(commands.HelpCommand): for command in mapping_commands: cmd_name_and_aliases = f"`{command.name}`" if command.aliases: - cmd_name_and_aliases += f" ({', '.join(f'`{alias}`' for alias in command.aliases)})" + cmd_name_and_aliases += f" ({", ".join(f"`{alias}`" for alias in command.aliases)})" command_categories[cog_group][command.name] = cmd_name_and_aliases return command_categories - def _get_cog_groups(self) -> list[str]: + @staticmethod + def _get_cog_groups() -> list[str]: """ Retrieves a list of cog groups from the 'cogs' folder. @@ -349,8 +353,8 @@ class TuxHelp(commands.HelpCommand): return select_options + @staticmethod def _add_navigation_and_selection( - self, menu: ViewMenu, select_options: dict[discord.SelectOption, list[Page]], ) -> None: @@ -368,7 +372,8 @@ class TuxHelp(commands.HelpCommand): menu.add_select(ViewSelect(title="Command Categories", options=select_options)) menu.add_button(ViewButton.end_session()) - def _extract_cog_group(self, cog: commands.Cog) -> str | None: + @staticmethod + def _extract_cog_group(cog: commands.Cog) -> str | None: """ Extracts the cog group from a cog's string representation. @@ -424,7 +429,7 @@ class TuxHelp(commands.HelpCommand): embed = self._embed_base( title=f"{prefix}{command.qualified_name}", - description=f"> {command.help or 'No documentation available.'}", + description=f"> {command.help or "No documentation available."}", ) await self._add_command_help_fields(embed, command) @@ -450,12 +455,12 @@ class TuxHelp(commands.HelpCommand): embed.add_field( name="Usage", - value=f"`{prefix}{command.usage or 'No usage.'}`", + value=f"`{prefix}{command.usage or "No usage."}`", inline=False, ) embed.add_field( name="Aliases", - value=(f"`{', '.join(command.aliases)}`" if command.aliases else "No aliases."), + value=(f"`{", ".join(command.aliases)}`" if command.aliases else "No aliases."), inline=False, ) @@ -471,7 +476,7 @@ class TuxHelp(commands.HelpCommand): prefix = await self._get_prefix() - embed = self._embed_base(f"{group.name}", f"> {group.help or 'No documentation available.'}") + embed = self._embed_base(f"{group.name}", f"> {group.help or "No documentation available."}") await self._add_command_help_fields(embed, group) for command in group.commands: diff --git a/tux/utils/flags.py b/tux/utils/flags.py index 176b616..43785b6 100644 --- a/tux/utils/flags.py +++ b/tux/utils/flags.py @@ -37,7 +37,7 @@ def generate_usage( flags: dict[str, commands.Flag] = flag_converter.get_flags() if flag_converter else {} for param_name, param in parameters.items(): - if param_name in ["ctx", "flags"]: + if param_name in {"ctx", "flags"}: continue is_required = param.default == inspect.Parameter.empty matching_string = get_matching_string(param_name) @@ -60,7 +60,7 @@ def generate_usage( usage += f" {flag}" if optional_flags: - usage += f" [{' | '.join(optional_flags)}]" + usage += f" [{" | ".join(optional_flags)}]" return usage