From 7479cce09163fb0b18366b9ac49d549fde6f7ce0 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Wed, 2 Sep 2026 09:11:19 -0400 Subject: [PATCH 1/5] feat: Create Slash Commands For Avatar Modify Module --- bot/exts/avatar_modification/avatar_modify.py | 234 ++++++++++++++++++ bot/exts/holidays/easter/egg_decorating.py | 86 ++++--- 2 files changed, 285 insertions(+), 35 deletions(-) diff --git a/bot/exts/avatar_modification/avatar_modify.py b/bot/exts/avatar_modification/avatar_modify.py index 5f0b240058..8eccde2382 100644 --- a/bot/exts/avatar_modification/avatar_modify.py +++ b/bot/exts/avatar_modification/avatar_modify.py @@ -9,8 +9,12 @@ from typing import TypeVar import discord +from discord import Interaction, app_commands +from discord.app_commands import Group from discord.ext import commands +from discord.ext.commands import Greedy from pydis_core.utils.logging import get_logger +from rapidfuzz import process from bot.bot import Bot from bot.constants import Colours, Emojis @@ -368,6 +372,236 @@ async def mosaic_command(self, ctx: commands.Context, squares: int = 16) -> None await ctx.send(file=file, embed=embed) +class AvatarModifyAppCommand(commands.Cog): + """Various commands for users to apply affects to their own avatars.""" + + def __init__(self, bot: Bot): + self.bot = bot + + avatar_modify = Group(name="avatar_modify", description="Groups all of the pfp modifying commands to allow a single concurrency limit.") + + @avatar_modify.command(name="8bitify") + async def eightbit_command(self, ctx: Interaction) -> None: + """Pixelates your avatar and changes the palette to an 8bit one.""" + await ctx.response.defer() + + image_bytes = await ctx.user.display_avatar.replace(size=1024).read() + file_name = file_safe_name("eightbit_avatar", ctx.user.display_name) + + file = await in_executor( + PfpEffects.apply_effect, + image_bytes, + PfpEffects.eight_bitify_effect, + file_name + ) + + embed = discord.Embed( + title="Your 8-bit avatar", + description="Here is your avatar. I think it looks all cool and 'retro'." + ) + + embed.set_image(url=f"attachment://{file_name}") + embed.set_footer(text=f"Made by {ctx.user.display_name}.", icon_url=ctx.user.display_avatar.url) + + await ctx.edit_original_response(embed=embed, attachments=(file,)) + + @avatar_modify.command(name="reverse") + async def reverse(self, ctx: Interaction) -> None: + """Reverses the user's profile picture.""" + + await ctx.response.defer() + + image_bytes = await ctx.user.display_avatar.replace(size=1024).read() + filename = file_safe_name("reverse_avatar", ctx.user.display_name) + + file = await in_executor( + PfpEffects.apply_effect, + image_bytes, + PfpEffects.flip_effect, + filename + ) + + embed = discord.Embed( + title="Your reversed avatar.", + description="Here is your reversed avatar. I think it is a spitting image of you." + ) + + embed.set_image(url=f"attachment://{filename}") + embed.set_footer(text=f"Made by {ctx.user.display_name}.", icon_url=ctx.user.display_avatar.url) + + await ctx.edit_original_response(embed=embed, attachments=(file,)) + + @avatar_modify.command(name="easterify", description="Easterify the user's avatar.") + @app_commands.describe(colours="Discord colour names, HTML colour names, XKCD colour names and hex values are accepted.") + async def avatareasterify(self, ctx: Interaction, colours: str = "") -> None: + """ + Easterify the user's avatar. + + Given colours will produce a personalised egg in the corner, similar to the egg_decorate command. + If colours are not given, a nice little chocolate bunny will sit in the corner. + Colours are split by spaces, unless you wrap the colour name in double quotes. + Discord colour names, HTML colour names, XKCD colour names and hex values are accepted. + """ + + await ctx.response.defer() + + egg = None + if colours: + sep_colours = [] + for i in colours.split(" "): + try: + sep_colours.append(discord.Colour.from_str(i)) + except ValueError: + sep_colours.append(i) + result = self.bot.get_cog("EggDecorating").render_egg(*sep_colours) + if isinstance(result, str): + await ctx.edit_original_response(content=result) + return + else: + egg = result + + image_bytes = await ctx.user.display_avatar.replace(size=256).read() + file_name = file_safe_name("easterified_avatar", ctx.user.display_name) + + file = await in_executor( + PfpEffects.apply_effect, + image_bytes, + PfpEffects.easterify_effect, + file_name, + egg + ) + + embed = discord.Embed( + title="Your Lovely Easterified Avatar!", + description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D" + ) + embed.set_image(url=f"attachment://{file_name}") + embed.set_footer(text=f"Made by {ctx.user.display_name}.", icon_url=ctx.user.display_avatar.url) + + await ctx.edit_original_response(embed=embed, attachments=(file,)) + + @avatar_modify.command(name="pride") + @app_commands.describe(gender="The pride flag to use as the border.", pixels="The thickness of the flag border.") + async def prideavatar(self, ctx: Interaction, gender: str = "lgbt", + pixels: app_commands.Range[int, 0, 512] = 64) -> None: + """ + Surround an avatar with a border of a specified LGBT flag. + + Default to the LGBT rainbow flag if none is given. + The amount of pixels can be given which determines the thickness of the flag border. + A maximum of 512px is enforced, defaults to a 64px border. + The full image is 1024x1024. + """ + gender = gender.lower() + pixels = max(0, min(512, pixels)) + gender = GENDER_OPTIONS.get(gender) + if gender is None: + await ctx.response.send_message("I don't have that flag!") + return + + await ctx.response.defer() + image_bytes = await ctx.user.display_avatar.replace(size=1024).read() + file_name = file_safe_name("pride_avatar", ctx.user.display_name) + + file = await in_executor( + PfpEffects.apply_effect, + image_bytes, + PfpEffects.pridify_effect, + file_name, + pixels, + gender + ) + + embed = discord.Embed( + title="Your Lovely Pride Avatar!", + description=f"Here is your lovely avatar, surrounded by\n a beautiful {gender} flag. Enjoy :D" + ) + embed.set_image(url=f"attachment://{file_name}") + embed.set_footer(text=f"Made by {ctx.user.display_name}.", icon_url=ctx.user.display_avatar.url) + await ctx.edit_original_response(embed=embed, attachments=(file,)) + + @prideavatar.autocomplete("gender") + async def gender_autocomplete(self, interaction: Interaction, query: str) -> list[app_commands.Choice[str]]: + """Returns a list of pride flags that matches `query`.""" + if query == "": + return [app_commands.Choice[str](name=x, value=x) for x in GENDER_OPTIONS.keys()][:25] + result = process.extract(query=query, choices=GENDER_OPTIONS.keys(), limit=25, score_cutoff=61) + return [app_commands.Choice[str](name=x[0], value=x[0]) for x in result] + + @avatar_modify.command(name="spooky") + async def spookyavatar(self, ctx: Interaction) -> None: + """Spookify the user's avatar, with a random *spooky* effect.""" + + await ctx.response.defer() + image_bytes = await ctx.user.display_avatar.replace(size=1024).read() + + file_name = file_safe_name("spooky_avatar", ctx.user.display_name) + + file = await in_executor( + PfpEffects.apply_effect, + image_bytes, + spookifications.get_random_effect, + file_name + ) + + embed = discord.Embed( + title="Is this you or am I just really paranoid?", + colour=Colours.soft_red + ) + embed.set_image(url=f"attachment://{file_name}") + embed.set_footer(text=f"Made by {ctx.user.display_name}.", icon_url=ctx.user.display_avatar.url) + + await ctx.edit_original_response(embed=embed, attachments=(file,)) + + @avatar_modify.command(name="mosaic") + async def mosaic_command(self, ctx: Interaction, squares: app_commands.Range[int, 1, MAX_SQUARES] = 16) -> None: + """Splits your avatar into x squares, randomizes them and stitches them back into a new image!""" + await ctx.response.defer() + + if not 1 <= squares <= MAX_SQUARES: + await ctx.edit_original_response(content=f"Squares must be a positive number less than or equal to {MAX_SQUARES:,}.") + return + + sqrt = math.sqrt(squares) + + if not sqrt.is_integer(): + squares = math.ceil(sqrt) ** 2 # Get the next perfect square + + file_name = file_safe_name("mosaic_avatar", ctx.user.display_name) + + img_bytes = await ctx.user.display_avatar.replace(size=1024).read() + + file = await in_executor( + PfpEffects.apply_effect, + img_bytes, + PfpEffects.mosaic_effect, + file_name, + squares, + ) + + if squares == 1: + title = "Hooh... that was a lot of work" + description = "I present to you... Yourself!" + elif squares == MAX_SQUARES: + title = "Testing the limits I see..." + description = "What a masterpiece. :star:" + else: + title = "Your mosaic avatar" + description = f"Here is your avatar. I think it looks a bit *puzzling*\nMade with {squares} squares." + + embed = discord.Embed( + title=title, + description=description, + colour=Colours.blue + ) + + embed.set_image(url=f"attachment://{file_name}") + embed.set_footer(text=f"Made by {ctx.user.display_name}", icon_url=ctx.user.display_avatar.url) + + await ctx.edit_original_response(embed=embed, attachments=(file,)) + + async def setup(bot: Bot) -> None: """Load the AvatarModify cog.""" await bot.add_cog(AvatarModify(bot)) + await bot.add_cog(AvatarModifyAppCommand(bot)) diff --git a/bot/exts/holidays/easter/egg_decorating.py b/bot/exts/holidays/easter/egg_decorating.py index 097f958d58..00be683e73 100644 --- a/bot/exts/holidays/easter/egg_decorating.py +++ b/bot/exts/holidays/easter/egg_decorating.py @@ -40,68 +40,84 @@ def replace_invalid(colour: str) -> int | None: return int(XKCD_COLOURS[colour], 16) return None - @commands.command(aliases=("decorateegg",)) - async def eggdecorate( - self, ctx: commands.Context, *colours: discord.Colour | str - ) -> Image.Image | None: + @staticmethod + def render_egg(*colours: discord.Colour | str) -> str | Image: """ Picks a random egg design and decorates it using the given colours. Colours are split by spaces, unless you wrap the colour name in double quotes. Discord colour names, HTML colour names, XKCD colour names and hex values are accepted. + + Returns a response string that should be sent to the user if the input is invalid. + Otherwise, returns a completed discord.File object. """ if len(colours) < 2: - await ctx.send("You must include at least 2 colours!") - return None + return "You must include at least 2 colours!" invalid = [] colours = list(colours) for idx, colour in enumerate(colours): if isinstance(colour, discord.Colour): continue - value = self.replace_invalid(colour) + value = EggDecorating.replace_invalid(colour) if value: colours[idx] = discord.Colour(value) else: invalid.append(helpers.suppress_links(colour)) if len(invalid) > 1: - await ctx.send(f"Sorry, I don't know these colours: {' '.join(invalid)}") - return None + return f"Sorry, I don't know these colours: {' '.join(invalid)}" if len(invalid) == 1: - await ctx.send(f"Sorry, I don't know the colour {invalid[0]}!") - return None + return f"Sorry, I don't know the colour {invalid[0]}!" + + # Expand list to 8 colours + colours_n = len(colours) + if colours_n < 8: + q, r = divmod(8, colours_n) + colours = colours * q + colours[:r] + num = random.randint(1, 6) + im = Image.open(Path(f"bot/resources/holidays/easter/easter_eggs/design{num}.png")) + data = list(im.getdata()) + + replaceable = {x for x in data if x not in IRREPLACEABLE} + replaceable = sorted(replaceable, key=COLOURS.index) + + replacing_colours = {colour: colours[i] for i, colour in enumerate(replaceable)} + new_data = [] + for x in data: + if x in replacing_colours: + new_data.append((*replacing_colours[x].to_rgb(), 255)) + # Also ensures that the alpha channel has a value + else: + new_data.append(x) + new_im = Image.new(im.mode, im.size) + new_im.putdata(new_data) + return new_im - async with ctx.typing(): - # Expand list to 8 colours - colours_n = len(colours) - if colours_n < 8: - q, r = divmod(8, colours_n) - colours = colours * q + colours[:r] - num = random.randint(1, 6) - im = Image.open(Path(f"bot/resources/holidays/easter/easter_eggs/design{num}.png")) - data = list(im.getdata()) - - replaceable = {x for x in data if x not in IRREPLACEABLE} - replaceable = sorted(replaceable, key=COLOURS.index) - - replacing_colours = {colour: colours[i] for i, colour in enumerate(replaceable)} - new_data = [] - for x in data: - if x in replacing_colours: - new_data.append((*replacing_colours[x].to_rgb(), 255)) - # Also ensures that the alpha channel has a value - else: - new_data.append(x) - new_im = Image.new(im.mode, im.size) - new_im.putdata(new_data) + @commands.command(aliases=("decorateegg",)) + async def eggdecorate( + self, ctx: commands.Context, *colours: discord.Colour | str + ) -> Image.Image | None: + """ + Picks a random egg design and decorates it using the given colours. + + Colours are split by spaces, unless you wrap the colour name in double quotes. + Discord colour names, HTML colour names, XKCD colour names and hex values are accepted. + """ + async with ctx.typing(): + result = self.render_egg(*colours) + if isinstance(result, str): + await ctx.send(result) + return + else: + new_im = result bufferedio = BytesIO() new_im.save(bufferedio, format="PNG") bufferedio.seek(0) - file = discord.File(bufferedio, filename="egg.png") # Creates file to be used in embed + file = discord.File(bufferedio, filename="egg.png") embed = discord.Embed( title="Your Colourful Easter Egg", description="Here is your pretty little egg. Hope you like it!" From 38a067dd0e1270b26d9d0cc57ede578c401e4b5b Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Wed, 2 Sep 2026 09:20:13 -0400 Subject: [PATCH 2/5] chore: Linting --- bot/exts/avatar_modification/avatar_modify.py | 24 ++++++++++--------- bot/exts/holidays/easter/egg_decorating.py | 6 ++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/bot/exts/avatar_modification/avatar_modify.py b/bot/exts/avatar_modification/avatar_modify.py index 8eccde2382..75771d43e8 100644 --- a/bot/exts/avatar_modification/avatar_modify.py +++ b/bot/exts/avatar_modification/avatar_modify.py @@ -12,7 +12,6 @@ from discord import Interaction, app_commands from discord.app_commands import Group from discord.ext import commands -from discord.ext.commands import Greedy from pydis_core.utils.logging import get_logger from rapidfuzz import process @@ -34,7 +33,7 @@ GENDER_OPTIONS = json.loads(Path("bot/resources/holidays/pride/gender_options.json").read_text("utf8")) -async def in_executor(func: Callable[..., T], *args) -> T: +async def in_executor[T](func: Callable[..., T], *args) -> T: """ Runs the given synchronous function `func` in an executor. @@ -378,7 +377,10 @@ class AvatarModifyAppCommand(commands.Cog): def __init__(self, bot: Bot): self.bot = bot - avatar_modify = Group(name="avatar_modify", description="Groups all of the pfp modifying commands to allow a single concurrency limit.") + avatar_modify = Group( + name="avatar_modify", + description="Groups all of the pfp modifying commands to allow a single concurrency limit." + ) @avatar_modify.command(name="8bitify") async def eightbit_command(self, ctx: Interaction) -> None: @@ -408,7 +410,6 @@ async def eightbit_command(self, ctx: Interaction) -> None: @avatar_modify.command(name="reverse") async def reverse(self, ctx: Interaction) -> None: """Reverses the user's profile picture.""" - await ctx.response.defer() image_bytes = await ctx.user.display_avatar.replace(size=1024).read() @@ -432,7 +433,9 @@ async def reverse(self, ctx: Interaction) -> None: await ctx.edit_original_response(embed=embed, attachments=(file,)) @avatar_modify.command(name="easterify", description="Easterify the user's avatar.") - @app_commands.describe(colours="Discord colour names, HTML colour names, XKCD colour names and hex values are accepted.") + @app_commands.describe( + colours="Discord colour names, HTML colour names, XKCD colour names and hex values are accepted." + ) async def avatareasterify(self, ctx: Interaction, colours: str = "") -> None: """ Easterify the user's avatar. @@ -442,7 +445,6 @@ async def avatareasterify(self, ctx: Interaction, colours: str = "") -> None: Colours are split by spaces, unless you wrap the colour name in double quotes. Discord colour names, HTML colour names, XKCD colour names and hex values are accepted. """ - await ctx.response.defer() egg = None @@ -457,8 +459,7 @@ async def avatareasterify(self, ctx: Interaction, colours: str = "") -> None: if isinstance(result, str): await ctx.edit_original_response(content=result) return - else: - egg = result + egg = result image_bytes = await ctx.user.display_avatar.replace(size=256).read() file_name = file_safe_name("easterified_avatar", ctx.user.display_name) @@ -524,14 +525,13 @@ async def prideavatar(self, ctx: Interaction, gender: str = "lgbt", async def gender_autocomplete(self, interaction: Interaction, query: str) -> list[app_commands.Choice[str]]: """Returns a list of pride flags that matches `query`.""" if query == "": - return [app_commands.Choice[str](name=x, value=x) for x in GENDER_OPTIONS.keys()][:25] + return [app_commands.Choice[str](name=x, value=x) for x in GENDER_OPTIONS][:25] result = process.extract(query=query, choices=GENDER_OPTIONS.keys(), limit=25, score_cutoff=61) return [app_commands.Choice[str](name=x[0], value=x[0]) for x in result] @avatar_modify.command(name="spooky") async def spookyavatar(self, ctx: Interaction) -> None: """Spookify the user's avatar, with a random *spooky* effect.""" - await ctx.response.defer() image_bytes = await ctx.user.display_avatar.replace(size=1024).read() @@ -559,7 +559,9 @@ async def mosaic_command(self, ctx: Interaction, squares: app_commands.Range[int await ctx.response.defer() if not 1 <= squares <= MAX_SQUARES: - await ctx.edit_original_response(content=f"Squares must be a positive number less than or equal to {MAX_SQUARES:,}.") + await ctx.edit_original_response( + content=f"Squares must be a positive number less than or equal to {MAX_SQUARES:,}." + ) return sqrt = math.sqrt(squares) diff --git a/bot/exts/holidays/easter/egg_decorating.py b/bot/exts/holidays/easter/egg_decorating.py index 00be683e73..f3539caf28 100644 --- a/bot/exts/holidays/easter/egg_decorating.py +++ b/bot/exts/holidays/easter/egg_decorating.py @@ -104,14 +104,12 @@ async def eggdecorate( Colours are split by spaces, unless you wrap the colour name in double quotes. Discord colour names, HTML colour names, XKCD colour names and hex values are accepted. """ - async with ctx.typing(): result = self.render_egg(*colours) if isinstance(result, str): await ctx.send(result) - return - else: - new_im = result + return None + new_im = result bufferedio = BytesIO() new_im.save(bufferedio, format="PNG") From b36f54da8a773b911e9d186fdd525ed3fef34dbd Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Wed, 2 Sep 2026 09:27:42 -0400 Subject: [PATCH 3/5] feat: Reverse Text Command --- bot/exts/utilities/reverse.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 bot/exts/utilities/reverse.py diff --git a/bot/exts/utilities/reverse.py b/bot/exts/utilities/reverse.py new file mode 100644 index 0000000000..0c9ef57d9a --- /dev/null +++ b/bot/exts/utilities/reverse.py @@ -0,0 +1,24 @@ + +import discord +from discord import Interaction, app_commands +from discord.ext import commands + +from bot.bot import Bot + + +class Reverse(commands.Cog): + """Reverse the input text.""" + + def __init__(self, bot: Bot) -> None: + self.bot = bot + + @app_commands.command(name="reverse") + @app_commands.describe(text="The text to reverse.") + async def reverse(self, ctx: Interaction, text: str) -> None: + """Reverses the input text.""" + await ctx.response.send_message(f"> {text[::-1]}", allowed_mentions=discord.AllowedMentions.none()) + + +async def setup(bot: Bot) -> None: + """Load the Epoch cog.""" + await bot.add_cog(Reverse(bot)) From 09eaed17e27f7267aaf31d3b76febd9071d3ba86 Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Thu, 3 Sep 2026 18:09:50 -0400 Subject: [PATCH 4/5] refactor: Use Hybrid Commands --- bot/exts/utilities/reverse.py | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 bot/exts/utilities/reverse.py diff --git a/bot/exts/utilities/reverse.py b/bot/exts/utilities/reverse.py deleted file mode 100644 index 0c9ef57d9a..0000000000 --- a/bot/exts/utilities/reverse.py +++ /dev/null @@ -1,24 +0,0 @@ - -import discord -from discord import Interaction, app_commands -from discord.ext import commands - -from bot.bot import Bot - - -class Reverse(commands.Cog): - """Reverse the input text.""" - - def __init__(self, bot: Bot) -> None: - self.bot = bot - - @app_commands.command(name="reverse") - @app_commands.describe(text="The text to reverse.") - async def reverse(self, ctx: Interaction, text: str) -> None: - """Reverses the input text.""" - await ctx.response.send_message(f"> {text[::-1]}", allowed_mentions=discord.AllowedMentions.none()) - - -async def setup(bot: Bot) -> None: - """Load the Epoch cog.""" - await bot.add_cog(Reverse(bot)) From ae79cb8a4c974c1db574770ca20eb6aa061a627c Mon Sep 17 00:00:00 2001 From: Ice Wolfy Date: Thu, 3 Sep 2026 18:12:11 -0400 Subject: [PATCH 5/5] refactor: Use Hybrid Commands --- bot/exts/avatar_modification/avatar_modify.py | 454 +++++------------- 1 file changed, 123 insertions(+), 331 deletions(-) diff --git a/bot/exts/avatar_modification/avatar_modify.py b/bot/exts/avatar_modification/avatar_modify.py index 75771d43e8..2cb93634a7 100644 --- a/bot/exts/avatar_modification/avatar_modify.py +++ b/bot/exts/avatar_modification/avatar_modify.py @@ -10,8 +10,8 @@ import discord from discord import Interaction, app_commands -from discord.app_commands import Group from discord.ext import commands +from discord.ext.commands import Greedy from pydis_core.utils.logging import get_logger from rapidfuzz import process @@ -88,7 +88,7 @@ async def _fetch_user(self, user_id: int) -> discord.User | None: return user - @commands.group(aliases=("avatar_mod", "pfp_mod", "avatarmod", "pfpmod")) + @commands.hybrid_group(aliases=("avatar_mod", "pfp_mod", "avatarmod", "pfpmod")) async def avatar_modify(self, ctx: commands.Context) -> None: """Groups all of the pfp modifying commands to allow a single concurrency limit.""" if not ctx.invoked_subcommand: @@ -97,34 +97,35 @@ async def avatar_modify(self, ctx: commands.Context) -> None: @avatar_modify.command(name="8bitify", root_aliases=("8bitify",)) async def eightbit_command(self, ctx: commands.Context) -> None: """Pixelates your avatar and changes the palette to an 8bit one.""" - async with ctx.typing(): - user = await self._fetch_user(ctx.author.id) - if not user: - await ctx.send(f"{Emojis.cross_mark} Could not get user info.") - return + await ctx.defer() + user = await self._fetch_user(ctx.author.id) + if not user: + await ctx.send(f"{Emojis.cross_mark} Could not get user info.") + return - image_bytes = await user.display_avatar.replace(size=1024).read() - file_name = file_safe_name("eightbit_avatar", ctx.author.display_name) + image_bytes = await user.display_avatar.replace(size=1024).read() + file_name = file_safe_name("eightbit_avatar", ctx.author.display_name) - file = await in_executor( - PfpEffects.apply_effect, - image_bytes, - PfpEffects.eight_bitify_effect, - file_name - ) + file = await in_executor( + PfpEffects.apply_effect, + image_bytes, + PfpEffects.eight_bitify_effect, + file_name + ) - embed = discord.Embed( - title="Your 8-bit avatar", - description="Here is your avatar. I think it looks all cool and 'retro'." - ) + embed = discord.Embed( + title="Your 8-bit avatar", + description="Here is your avatar. I think it looks all cool and 'retro'." + ) - embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=user.display_avatar.url) + embed.set_image(url=f"attachment://{file_name}") + embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=user.display_avatar.url) await ctx.send(embed=embed, file=file) @avatar_modify.command(name="reverse", root_aliases=("reverse",)) - async def reverse(self, ctx: commands.Context, *, text: str | None) -> None: + @app_commands.describe(text="The text to reverse") + async def reverse(self, ctx: commands.Context, *, text: str | None = None) -> None: """ Reverses the sent text. @@ -134,34 +135,37 @@ async def reverse(self, ctx: commands.Context, *, text: str | None) -> None: await ctx.send(f"> {text[::-1]}", allowed_mentions=discord.AllowedMentions.none()) return - async with ctx.typing(): - user = await self._fetch_user(ctx.author.id) - if not user: - await ctx.send(f"{Emojis.cross_mark} Could not get user info.") - return + await ctx.defer() + user = await self._fetch_user(ctx.author.id) + if not user: + await ctx.send(f"{Emojis.cross_mark} Could not get user info.") + return - image_bytes = await user.display_avatar.replace(size=1024).read() - filename = file_safe_name("reverse_avatar", ctx.author.display_name) + image_bytes = await user.display_avatar.replace(size=1024).read() + filename = file_safe_name("reverse_avatar", ctx.author.display_name) - file = await in_executor( - PfpEffects.apply_effect, - image_bytes, - PfpEffects.flip_effect, - filename - ) + file = await in_executor( + PfpEffects.apply_effect, + image_bytes, + PfpEffects.flip_effect, + filename + ) - embed = discord.Embed( - title="Your reversed avatar.", - description="Here is your reversed avatar. I think it is a spitting image of you." - ) + embed = discord.Embed( + title="Your reversed avatar.", + description="Here is your reversed avatar. I think it is a spitting image of you." + ) - embed.set_image(url=f"attachment://{filename}") - embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=user.display_avatar.url) + embed.set_image(url=f"attachment://{filename}") + embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=user.display_avatar.url) - await ctx.send(embed=embed, file=file) + await ctx.send(embed=embed, file=file) @avatar_modify.command(aliases=("easterify",), root_aliases=("easterify", "avatareasterify")) - async def avatareasterify(self, ctx: commands.Context, *colours: discord.Colour | str) -> None: + @app_commands.describe( + colours="Discord colour names, HTML colour names, XKCD colour names and hex values are accepted." + ) + async def avatareasterify(self, ctx: commands.Context, colours: Greedy[discord.Colour | str]) -> None: """ Easterify the user's avatar. @@ -181,39 +185,42 @@ async def send(*args, **kwargs) -> str: return args[0] return None - async with ctx.typing(): - user = await self._fetch_user(ctx.author.id) - if not user: - await ctx.send(f"{Emojis.cross_mark} Could not get user info.") - return + await ctx.defer() + user = await self._fetch_user(ctx.author.id) + if not user: + await ctx.send(f"{Emojis.cross_mark} Could not get user info.") + return - egg = None - if colours: - send_message = ctx.send - ctx.send = send # Assigns ctx.send to a fake send - egg = await ctx.invoke(self.bot.get_command("eggdecorate"), *colours) - if isinstance(egg, str): # When an error message occurs in eggdecorate. - await send_message(egg) - return - ctx.send = send_message # Reassigns ctx.send + egg = None + if colours: + send_message = ctx.send + ctx.send = send # Assigns ctx.send to a fake send + if (cog := self.bot.get_cog("EggDecorating")) is not None: + egg = cog.render_egg(*colours) + else: + egg = None + if isinstance(egg, str): # When an error message occurs in eggdecorate. + await send_message(egg) + return + ctx.send = send_message # Reassigns ctx.send - image_bytes = await user.display_avatar.replace(size=256).read() - file_name = file_safe_name("easterified_avatar", ctx.author.display_name) + image_bytes = await user.display_avatar.replace(size=256).read() + file_name = file_safe_name("easterified_avatar", ctx.author.display_name) - file = await in_executor( - PfpEffects.apply_effect, - image_bytes, - PfpEffects.easterify_effect, - file_name, - egg - ) + file = await in_executor( + PfpEffects.apply_effect, + image_bytes, + PfpEffects.easterify_effect, + file_name, + egg + ) - embed = discord.Embed( - title="Your Lovely Easterified Avatar!", - description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D" - ) - embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=user.display_avatar.url) + embed = discord.Embed( + title="Your Lovely Easterified Avatar!", + description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D" + ) + embed.set_image(url=f"attachment://{file_name}") + embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=user.display_avatar.url) await ctx.send(file=file, embed=embed) @@ -249,9 +256,14 @@ async def send_pride_image( @avatar_modify.group( aliases=("avatarpride", "pridepfp", "prideprofile"), root_aliases=("prideavatar", "avatarpride", "pridepfp", "prideprofile"), - invoke_without_command=True + invoke_without_command=True, + fallback="pride", ) - async def prideavatar(self, ctx: commands.Context, option: str = "lgbt", pixels: int = 64) -> None: + @app_commands.describe(option="The pride flag to use as the border.", pixels="The thickness of the flag border.") + async def prideavatar( + self, ctx: commands.Context, option: str = "lgbt", + pixels: app_commands.Range[int, 0, 512] = 64 + ) -> None: """ Surround an avatar with a border of a specified LGBT flag. @@ -267,13 +279,21 @@ async def prideavatar(self, ctx: commands.Context, option: str = "lgbt", pixels: await ctx.send("I don't have that flag!") return - async with ctx.typing(): - user = await self._fetch_user(ctx.author.id) - if not user: - await ctx.send(f"{Emojis.cross_mark} Could not get user info.") - return - image_bytes = await user.display_avatar.replace(size=1024).read() - await self.send_pride_image(ctx, image_bytes, pixels, flag, option) + await ctx.defer() + user = await self._fetch_user(ctx.author.id) + if not user: + await ctx.send(f"{Emojis.cross_mark} Could not get user info.") + return + image_bytes = await user.display_avatar.replace(size=1024).read() + await self.send_pride_image(ctx, image_bytes, pixels, flag, option) + + @prideavatar.autocomplete("option") + async def gender_autocomplete(self, interaction: Interaction, query: str) -> list[app_commands.Choice[str]]: + """Returns a list of pride flags that matches `query`.""" + if query == "": + return [app_commands.Choice[str](name=x, value=x) for x in GENDER_OPTIONS][:25] + result = process.extract(query=query, choices=GENDER_OPTIONS.keys(), limit=25, score_cutoff=61) + return [app_commands.Choice[str](name=x[0], value=x[0]) for x in result] @prideavatar.command() async def flags(self, ctx: commands.Context) -> None: @@ -299,243 +319,10 @@ async def spookyavatar(self, ctx: commands.Context) -> None: await ctx.send(f"{Emojis.cross_mark} Could not get user info.") return - async with ctx.typing(): - image_bytes = await user.display_avatar.replace(size=1024).read() - - file_name = file_safe_name("spooky_avatar", ctx.author.display_name) - - file = await in_executor( - PfpEffects.apply_effect, - image_bytes, - spookifications.get_random_effect, - file_name - ) - - embed = discord.Embed( - title="Is this you or am I just really paranoid?", - colour=Colours.soft_red - ) - embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.display_avatar.url) - - await ctx.send(file=file, embed=embed) + await ctx.defer() + image_bytes = await user.display_avatar.replace(size=1024).read() - @avatar_modify.command(name="mosaic", root_aliases=("mosaic",)) - async def mosaic_command(self, ctx: commands.Context, squares: int = 16) -> None: - """Splits your avatar into x squares, randomizes them and stitches them back into a new image!""" - async with ctx.typing(): - user = await self._fetch_user(ctx.author.id) - if not user: - await ctx.send(f"{Emojis.cross_mark} Could not get user info.") - return - - if not 1 <= squares <= MAX_SQUARES: - raise commands.BadArgument(f"Squares must be a positive number less than or equal to {MAX_SQUARES:,}.") - - sqrt = math.sqrt(squares) - - if not sqrt.is_integer(): - squares = math.ceil(sqrt) ** 2 # Get the next perfect square - - file_name = file_safe_name("mosaic_avatar", ctx.author.display_name) - - img_bytes = await user.display_avatar.replace(size=1024).read() - - file = await in_executor( - PfpEffects.apply_effect, - img_bytes, - PfpEffects.mosaic_effect, - file_name, - squares, - ) - - if squares == 1: - title = "Hooh... that was a lot of work" - description = "I present to you... Yourself!" - elif squares == MAX_SQUARES: - title = "Testing the limits I see..." - description = "What a masterpiece. :star:" - else: - title = "Your mosaic avatar" - description = f"Here is your avatar. I think it looks a bit *puzzling*\nMade with {squares} squares." - - embed = discord.Embed( - title=title, - description=description, - colour=Colours.blue - ) - - embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=user.display_avatar.url) - - await ctx.send(file=file, embed=embed) - - -class AvatarModifyAppCommand(commands.Cog): - """Various commands for users to apply affects to their own avatars.""" - - def __init__(self, bot: Bot): - self.bot = bot - - avatar_modify = Group( - name="avatar_modify", - description="Groups all of the pfp modifying commands to allow a single concurrency limit." - ) - - @avatar_modify.command(name="8bitify") - async def eightbit_command(self, ctx: Interaction) -> None: - """Pixelates your avatar and changes the palette to an 8bit one.""" - await ctx.response.defer() - - image_bytes = await ctx.user.display_avatar.replace(size=1024).read() - file_name = file_safe_name("eightbit_avatar", ctx.user.display_name) - - file = await in_executor( - PfpEffects.apply_effect, - image_bytes, - PfpEffects.eight_bitify_effect, - file_name - ) - - embed = discord.Embed( - title="Your 8-bit avatar", - description="Here is your avatar. I think it looks all cool and 'retro'." - ) - - embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.user.display_name}.", icon_url=ctx.user.display_avatar.url) - - await ctx.edit_original_response(embed=embed, attachments=(file,)) - - @avatar_modify.command(name="reverse") - async def reverse(self, ctx: Interaction) -> None: - """Reverses the user's profile picture.""" - await ctx.response.defer() - - image_bytes = await ctx.user.display_avatar.replace(size=1024).read() - filename = file_safe_name("reverse_avatar", ctx.user.display_name) - - file = await in_executor( - PfpEffects.apply_effect, - image_bytes, - PfpEffects.flip_effect, - filename - ) - - embed = discord.Embed( - title="Your reversed avatar.", - description="Here is your reversed avatar. I think it is a spitting image of you." - ) - - embed.set_image(url=f"attachment://{filename}") - embed.set_footer(text=f"Made by {ctx.user.display_name}.", icon_url=ctx.user.display_avatar.url) - - await ctx.edit_original_response(embed=embed, attachments=(file,)) - - @avatar_modify.command(name="easterify", description="Easterify the user's avatar.") - @app_commands.describe( - colours="Discord colour names, HTML colour names, XKCD colour names and hex values are accepted." - ) - async def avatareasterify(self, ctx: Interaction, colours: str = "") -> None: - """ - Easterify the user's avatar. - - Given colours will produce a personalised egg in the corner, similar to the egg_decorate command. - If colours are not given, a nice little chocolate bunny will sit in the corner. - Colours are split by spaces, unless you wrap the colour name in double quotes. - Discord colour names, HTML colour names, XKCD colour names and hex values are accepted. - """ - await ctx.response.defer() - - egg = None - if colours: - sep_colours = [] - for i in colours.split(" "): - try: - sep_colours.append(discord.Colour.from_str(i)) - except ValueError: - sep_colours.append(i) - result = self.bot.get_cog("EggDecorating").render_egg(*sep_colours) - if isinstance(result, str): - await ctx.edit_original_response(content=result) - return - egg = result - - image_bytes = await ctx.user.display_avatar.replace(size=256).read() - file_name = file_safe_name("easterified_avatar", ctx.user.display_name) - - file = await in_executor( - PfpEffects.apply_effect, - image_bytes, - PfpEffects.easterify_effect, - file_name, - egg - ) - - embed = discord.Embed( - title="Your Lovely Easterified Avatar!", - description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D" - ) - embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.user.display_name}.", icon_url=ctx.user.display_avatar.url) - - await ctx.edit_original_response(embed=embed, attachments=(file,)) - - @avatar_modify.command(name="pride") - @app_commands.describe(gender="The pride flag to use as the border.", pixels="The thickness of the flag border.") - async def prideavatar(self, ctx: Interaction, gender: str = "lgbt", - pixels: app_commands.Range[int, 0, 512] = 64) -> None: - """ - Surround an avatar with a border of a specified LGBT flag. - - Default to the LGBT rainbow flag if none is given. - The amount of pixels can be given which determines the thickness of the flag border. - A maximum of 512px is enforced, defaults to a 64px border. - The full image is 1024x1024. - """ - gender = gender.lower() - pixels = max(0, min(512, pixels)) - gender = GENDER_OPTIONS.get(gender) - if gender is None: - await ctx.response.send_message("I don't have that flag!") - return - - await ctx.response.defer() - image_bytes = await ctx.user.display_avatar.replace(size=1024).read() - file_name = file_safe_name("pride_avatar", ctx.user.display_name) - - file = await in_executor( - PfpEffects.apply_effect, - image_bytes, - PfpEffects.pridify_effect, - file_name, - pixels, - gender - ) - - embed = discord.Embed( - title="Your Lovely Pride Avatar!", - description=f"Here is your lovely avatar, surrounded by\n a beautiful {gender} flag. Enjoy :D" - ) - embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.user.display_name}.", icon_url=ctx.user.display_avatar.url) - await ctx.edit_original_response(embed=embed, attachments=(file,)) - - @prideavatar.autocomplete("gender") - async def gender_autocomplete(self, interaction: Interaction, query: str) -> list[app_commands.Choice[str]]: - """Returns a list of pride flags that matches `query`.""" - if query == "": - return [app_commands.Choice[str](name=x, value=x) for x in GENDER_OPTIONS][:25] - result = process.extract(query=query, choices=GENDER_OPTIONS.keys(), limit=25, score_cutoff=61) - return [app_commands.Choice[str](name=x[0], value=x[0]) for x in result] - - @avatar_modify.command(name="spooky") - async def spookyavatar(self, ctx: Interaction) -> None: - """Spookify the user's avatar, with a random *spooky* effect.""" - await ctx.response.defer() - image_bytes = await ctx.user.display_avatar.replace(size=1024).read() - - file_name = file_safe_name("spooky_avatar", ctx.user.display_name) + file_name = file_safe_name("spooky_avatar", ctx.author.display_name) file = await in_executor( PfpEffects.apply_effect, @@ -549,29 +336,35 @@ async def spookyavatar(self, ctx: Interaction) -> None: colour=Colours.soft_red ) embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.user.display_name}.", icon_url=ctx.user.display_avatar.url) + embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.display_avatar.url) - await ctx.edit_original_response(embed=embed, attachments=(file,)) + await ctx.send(file=file, embed=embed) - @avatar_modify.command(name="mosaic") - async def mosaic_command(self, ctx: Interaction, squares: app_commands.Range[int, 1, MAX_SQUARES] = 16) -> None: + @avatar_modify.command(name="mosaic", root_aliases=("mosaic",)) + @app_commands.describe(squares="Amount of squares to use. Will be converted to next perfect square.") + async def mosaic_command( + self, + ctx: commands.Context, + squares: app_commands.Range[int, 1, MAX_SQUARES] = 16 + ) -> None: """Splits your avatar into x squares, randomizes them and stitches them back into a new image!""" - await ctx.response.defer() + await ctx.defer() + user = await self._fetch_user(ctx.author.id) + if not user: + await ctx.send(f"{Emojis.cross_mark} Could not get user info.") + return if not 1 <= squares <= MAX_SQUARES: - await ctx.edit_original_response( - content=f"Squares must be a positive number less than or equal to {MAX_SQUARES:,}." - ) - return + raise commands.BadArgument(f"Squares must be a positive number less than or equal to {MAX_SQUARES:,}.") sqrt = math.sqrt(squares) if not sqrt.is_integer(): squares = math.ceil(sqrt) ** 2 # Get the next perfect square - file_name = file_safe_name("mosaic_avatar", ctx.user.display_name) + file_name = file_safe_name("mosaic_avatar", ctx.author.display_name) - img_bytes = await ctx.user.display_avatar.replace(size=1024).read() + img_bytes = await user.display_avatar.replace(size=1024).read() file = await in_executor( PfpEffects.apply_effect, @@ -598,12 +391,11 @@ async def mosaic_command(self, ctx: Interaction, squares: app_commands.Range[int ) embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.user.display_name}", icon_url=ctx.user.display_avatar.url) + embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=user.display_avatar.url) - await ctx.edit_original_response(embed=embed, attachments=(file,)) + await ctx.send(file=file, embed=embed) async def setup(bot: Bot) -> None: """Load the AvatarModify cog.""" await bot.add_cog(AvatarModify(bot)) - await bot.add_cog(AvatarModifyAppCommand(bot))