From f63af81bb9f1613ca2b4e00c3f12978a02a86983 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 18 Feb 2021 19:23:04 +0000 Subject: Move gender options to a resource file --- bot/resources/pride/gender_options.json | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 bot/resources/pride/gender_options.json (limited to 'bot') diff --git a/bot/resources/pride/gender_options.json b/bot/resources/pride/gender_options.json new file mode 100644 index 00000000..062742fb --- /dev/null +++ b/bot/resources/pride/gender_options.json @@ -0,0 +1,41 @@ +{ + "agender": "agender", + "androgyne": "androgyne", + "androgynous": "androgyne", + "aromantic": "aromantic", + "aro": "aromantic", + "ace": "asexual", + "asexual": "asexual", + "bigender": "bigender", + "bisexual": "bisexual", + "bi": "bisexual", + "demiboy": "demiboy", + "demigirl": "demigirl", + "demi": "demisexual", + "demisexual": "demisexual", + "gay": "gay", + "lgbt": "gay", + "queer": "gay", + "homosexual": "gay", + "fluid": "genderfluid", + "genderfluid": "genderfluid", + "genderqueer": "genderqueer", + "intersex": "intersex", + "lesbian": "lesbian", + "non-binary": "nonbinary", + "enby": "nonbinary", + "nb": "nonbinary", + "nonbinary": "nonbinary", + "omnisexual": "omnisexual", + "omni": "omnisexual", + "pansexual": "pansexual", + "pan": "pansexual", + "pangender": "pangender", + "poly": "polysexual", + "polysexual": "polysexual", + "polyamory": "polyamory", + "polyamorous": "polyamory", + "transgender": "transgender", + "trans": "transgender", + "trigender": "trigender" +} -- cgit v1.2.3 From dd59719df6a36f6cea98faba262fa171ce3289f0 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 18 Feb 2021 19:25:47 +0000 Subject: Merge all avatar cogs into one. Also includes some in_executor and max_concurrency code. --- bot/exts/easter/avatar_easterifier.py | 128 ------------- bot/exts/evergreen/8bitify.py | 54 ------ bot/exts/evergreen/pfp_modify.py | 337 ++++++++++++++++++++++++++++++++++ bot/exts/halloween/spookyavatar.py | 52 ------ bot/exts/pride/pride_avatar.py | 177 ------------------ 5 files changed, 337 insertions(+), 411 deletions(-) delete mode 100644 bot/exts/easter/avatar_easterifier.py delete mode 100644 bot/exts/evergreen/8bitify.py create mode 100644 bot/exts/evergreen/pfp_modify.py delete mode 100644 bot/exts/halloween/spookyavatar.py delete mode 100644 bot/exts/pride/pride_avatar.py (limited to 'bot') diff --git a/bot/exts/easter/avatar_easterifier.py b/bot/exts/easter/avatar_easterifier.py deleted file mode 100644 index 8e8a3500..00000000 --- a/bot/exts/easter/avatar_easterifier.py +++ /dev/null @@ -1,128 +0,0 @@ -import asyncio -import logging -from io import BytesIO -from pathlib import Path -from typing import Tuple, Union - -import discord -from PIL import Image -from PIL.ImageOps import posterize -from discord.ext import commands - -log = logging.getLogger(__name__) - -COLOURS = [ - (255, 247, 0), (255, 255, 224), (0, 255, 127), (189, 252, 201), (255, 192, 203), - (255, 160, 122), (181, 115, 220), (221, 160, 221), (200, 162, 200), (238, 130, 238), - (135, 206, 235), (0, 204, 204), (64, 224, 208) -] # Pastel colours - Easter-like - - -class AvatarEasterifier(commands.Cog): - """Put an Easter spin on your avatar or image!""" - - def __init__(self, bot: commands.Bot): - self.bot = bot - - @staticmethod - def closest(x: Tuple[int, int, int]) -> Tuple[int, int, int]: - """ - Finds the closest easter colour to a given pixel. - - Returns a merge between the original colour and the closest colour - """ - r1, g1, b1 = x - - def distance(point: Tuple[int, int, int]) -> Tuple[int, int, int]: - """Finds the difference between a pastel colour and the original pixel colour.""" - r2, g2, b2 = point - return ((r1 - r2)**2 + (g1 - g2)**2 + (b1 - b2)**2) - - closest_colours = sorted(COLOURS, key=lambda point: distance(point)) - r2, g2, b2 = closest_colours[0] - r = (r1 + r2) // 2 - g = (g1 + g2) // 2 - b = (b1 + b2) // 2 - - return (r, g, b) - - @commands.command(pass_context=True, aliases=["easterify"]) - async def avatareasterify(self, ctx: commands.Context, *colours: Union[discord.Colour, str]) -> None: - """ - This "Easterifies" 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. - """ - async def send(*args, **kwargs) -> str: - """ - This replaces the original ctx.send. - - When invoking the egg decorating command, the egg itself doesn't print to to the channel. - Returns the message content so that if any errors occur, the error message can be output. - """ - if args: - return args[0] - - async with ctx.typing(): - - # Grabs image of avatar - image_bytes = await ctx.author.avatar_url_as(size=256).read() - - old = Image.open(BytesIO(image_bytes)) - old = old.convert("RGBA") - - # Grabs alpha channel since posterize can't be used with an RGBA image. - alpha = old.getchannel("A").getdata() - old = old.convert("RGB") - old = posterize(old, 6) - - data = old.getdata() - setted_data = set(data) - new_d = {} - - for x in setted_data: - new_d[x] = self.closest(x) - await asyncio.sleep(0) # Ensures discord doesn't break in the background. - new_data = [(*new_d[x], alpha[i]) if x in new_d else x for i, x in enumerate(data)] - - im = Image.new("RGBA", old.size) - im.putdata(new_data) - - 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. - return await send_message(egg) - - ratio = 64 / egg.height - egg = egg.resize((round(egg.width * ratio), round(egg.height * ratio))) - egg = egg.convert("RGBA") - im.alpha_composite(egg, (im.width - egg.width, (im.height - egg.height)//2)) # Right centre. - ctx.send = send_message # Reassigns ctx.send - else: - bunny = Image.open(Path("bot/resources/easter/chocolate_bunny.png")) - im.alpha_composite(bunny, (im.width - bunny.width, (im.height - bunny.height)//2)) # Right centre. - - bufferedio = BytesIO() - im.save(bufferedio, format="PNG") - - bufferedio.seek(0) - - file = discord.File(bufferedio, filename="easterified_avatar.png") # Creates file to be used in embed - embed = discord.Embed( - name="Your Lovely Easterified Avatar", - description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D" - ) - embed.set_image(url="attachment://easterified_avatar.png") - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) - - await ctx.send(file=file, embed=embed) - - -def setup(bot: commands.Bot) -> None: - """Avatar Easterifier Cog load.""" - bot.add_cog(AvatarEasterifier(bot)) diff --git a/bot/exts/evergreen/8bitify.py b/bot/exts/evergreen/8bitify.py deleted file mode 100644 index 54e68f80..00000000 --- a/bot/exts/evergreen/8bitify.py +++ /dev/null @@ -1,54 +0,0 @@ -from io import BytesIO - -import discord -from PIL import Image -from discord.ext import commands - - -class EightBitify(commands.Cog): - """Make your avatar 8bit!""" - - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - - @staticmethod - def pixelate(image: Image) -> Image: - """Takes an image and pixelates it.""" - return image.resize((32, 32), resample=Image.NEAREST).resize((1024, 1024), resample=Image.NEAREST) - - @staticmethod - def quantize(image: Image) -> Image: - """Reduces colour palette to 256 colours.""" - return image.quantize() - - @commands.command(name="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(): - image_bytes = await ctx.author.avatar_url.read() - avatar = Image.open(BytesIO(image_bytes)) - avatar = avatar.convert("RGBA").resize((1024, 1024)) - - eightbit = self.pixelate(avatar) - eightbit = self.quantize(eightbit) - - bufferedio = BytesIO() - eightbit.save(bufferedio, format="PNG") - bufferedio.seek(0) - - file = discord.File(bufferedio, filename="8bitavatar.png") - - 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="attachment://8bitavatar.png") - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) - - await ctx.send(file=file, embed=embed) - - -def setup(bot: commands.Bot) -> None: - """Cog load.""" - bot.add_cog(EightBitify(bot)) diff --git a/bot/exts/evergreen/pfp_modify.py b/bot/exts/evergreen/pfp_modify.py new file mode 100644 index 00000000..f356d2f6 --- /dev/null +++ b/bot/exts/evergreen/pfp_modify.py @@ -0,0 +1,337 @@ +import asyncio +import json +import logging +import os +import typing as t +from concurrent.futures import ThreadPoolExecutor +from io import BytesIO +from pathlib import Path + +import aiofiles +import aiohttp +import discord +from PIL import Image, ImageDraw, ImageOps, UnidentifiedImageError +from discord.ext import commands + +from bot.constants import Colours +from bot.utils.halloween import spookifications + +log = logging.getLogger(__name__) + +EASTER_COLOURS = [ + (255, 247, 0), (255, 255, 224), (0, 255, 127), (189, 252, 201), (255, 192, 203), + (255, 160, 122), (181, 115, 220), (221, 160, 221), (200, 162, 200), (238, 130, 238), + (135, 206, 235), (0, 204, 204), (64, 224, 208) +] # Pastel colours - Easter-like + +_EXECUTOR = ThreadPoolExecutor(10) + + +async def in_thread(func: t.Callable, *args) -> asyncio.Future: + """Allows non-async functions to work in async functions.""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(_EXECUTOR, func, *args) + + +class PfpModify(commands.Cog): + """Various commands for users to change their own profile picture.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + self.bot.loop.create_task(self.init_cog()) + + async def init_cog(self) -> None: + """Initial load from resources asynchronously.""" + async with aiofiles.open('bot/resources/pride/gender_options.json') as f: + self.GENDER_OPTIONS = json.loads(await f.read()) + + @staticmethod + def closest(x: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: + """ + Finds the closest easter colour to a given pixel. + + Returns a merge between the original colour and the closest colour + """ + r1, g1, b1 = x + + def distance(point: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: + """Finds the difference between a pastel colour and the original pixel colour.""" + r2, g2, b2 = point + return ((r1 - r2)**2 + (g1 - g2)**2 + (b1 - b2)**2) + + closest_colours = sorted(EASTER_COLOURS, key=lambda point: distance(point)) + r2, g2, b2 = closest_colours[0] + r = (r1 + r2) // 2 + g = (g1 + g2) // 2 + b = (b1 + b2) // 2 + + return (r, g, b) + + @staticmethod + def crop_avatar(avatar: Image) -> Image: + """This crops the avatar given into a circle.""" + mask = Image.new("L", avatar.size, 0) + draw = ImageDraw.Draw(mask) + draw.ellipse((0, 0) + avatar.size, fill=255) + avatar.putalpha(mask) + return avatar + + @staticmethod + def crop_ring(ring: Image, px: int) -> Image: + """This crops the given ring into a circle.""" + mask = Image.new("L", ring.size, 0) + draw = ImageDraw.Draw(mask) + draw.ellipse((0, 0) + ring.size, fill=255) + draw.ellipse((px, px, 1024-px, 1024-px), fill=0) + ring.putalpha(mask) + return ring + + def process_options(self, option: str, pixels: int) -> t.Tuple[str, int, str]: + """Does some shared preprocessing for the prideavatar commands.""" + return option.lower(), max(0, min(512, pixels)), self.GENDER_OPTIONS.get(option) + + def process_image( + self, + image_bytes: bytes, + pixels: int, + flag: str + ) -> discord.File: + """Constructs and returns the final image. Used by the pride commands.""" + # This line can raise UnidentifiedImageError and must be handled by the calling func. + avatar = Image.open(BytesIO(image_bytes)) + avatar = avatar.convert("RGBA").resize((1024, 1024)) + + avatar = self.crop_avatar(avatar) + + ring = Image.open(Path(f"bot/resources/pride/flags/{flag}.png")).resize((1024, 1024)) + ring = ring.convert("RGBA") + ring = self.crop_ring(ring, pixels) + + avatar.alpha_composite(ring, (0, 0)) + bufferedio = BytesIO() + avatar.save(bufferedio, format="PNG") + bufferedio.seek(0) + + return discord.File(bufferedio, filename="pride_avatar.png") # Creates file to be used in embed + + async def send_image( + self, + ctx: commands.Context, + image_bytes: bytes, + pixels: int, + flag: str, + option: str + ) -> None: + """Gets and sends the image in an embed. Used by the pride commands.""" + try: + file = await in_thread(self.process_image, image_bytes, pixels, flag) + except UnidentifiedImageError: + ctx.send("Cannot identify image from provided URL") + return + + embed = discord.Embed( + name="Your Lovely Pride Avatar", + description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D" + ) + embed.set_image(url="attachment://pride_avatar.png") + embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + await ctx.send(file=file, embed=embed) + + @commands.max_concurrency(1, commands.BucketType.guild, wait=True) + @commands.group() + async def pfp_modify(self, ctx: commands.Context) -> None: + """Groups all of the pfp modifing commands to allow a single concurrency limit.""" + if not ctx.invoked_subcommand: + await ctx.send_help(ctx.command) + + @pfp_modify.command(name="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(): + image_bytes = await ctx.author.avatar_url.read() + avatar = Image.open(BytesIO(image_bytes)) + avatar = avatar.convert("RGBA").resize((1024, 1024)) + + # Pixilate and quantize + eightbit = avatar.resize((32, 32), resample=Image.NEAREST).resize((1024, 1024), resample=Image.NEAREST) + eightbit = eightbit.quantize() + + bufferedio = BytesIO() + eightbit.save(bufferedio, format="PNG") + bufferedio.seek(0) + + file = discord.File(bufferedio, filename="8bitavatar.png") + + 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="attachment://8bitavatar.png") + embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + + await ctx.send(file=file, embed=embed) + + @pfp_modify.command(pass_context=True, aliases=["easterify"]) + async def avatareasterify(self, ctx: commands.Context, *colours: t.Union[discord.Colour, str]) -> None: + """ + This "Easterifies" 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. + """ + async def send(*args, **kwargs) -> str: + """ + This replaces the original ctx.send. + + When invoking the egg decorating command, the egg itself doesn't print to to the channel. + Returns the message content so that if any errors occur, the error message can be output. + """ + if args: + return args[0] + + async with ctx.typing(): + + # Grabs image of avatar + image_bytes = await ctx.author.avatar_url_as(size=256).read() + + old = Image.open(BytesIO(image_bytes)) + old = old.convert("RGBA") + + # Grabs alpha channel since posterize can't be used with an RGBA image. + alpha = old.getchannel("A").getdata() + old = old.convert("RGB") + old = ImageOps.posterize(old, 6) + + data = old.getdata() + setted_data = set(data) + new_d = {} + + for x in setted_data: + new_d[x] = self.closest(x) + await asyncio.sleep(0) # Ensures discord doesn't break in the background. + new_data = [(*new_d[x], alpha[i]) if x in new_d else x for i, x in enumerate(data)] + + im = Image.new("RGBA", old.size) + im.putdata(new_data) + + 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. + return await send_message(egg) + + ratio = 64 / egg.height + egg = egg.resize((round(egg.width * ratio), round(egg.height * ratio))) + egg = egg.convert("RGBA") + im.alpha_composite(egg, (im.width - egg.width, (im.height - egg.height)//2)) # Right centre. + ctx.send = send_message # Reassigns ctx.send + else: + bunny = Image.open(Path("bot/resources/easter/chocolate_bunny.png")) + im.alpha_composite(bunny, (im.width - bunny.width, (im.height - bunny.height)//2)) # Right centre. + + bufferedio = BytesIO() + im.save(bufferedio, format="PNG") + + bufferedio.seek(0) + + file = discord.File(bufferedio, filename="easterified_avatar.png") # Creates file to be used in embed + embed = discord.Embed( + name="Your Lovely Easterified Avatar", + description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D" + ) + embed.set_image(url="attachment://easterified_avatar.png") + embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + + await ctx.send(file=file, embed=embed) + + @pfp_modify.group(aliases=["avatarpride", "pridepfp", "prideprofile"], invoke_without_command=True) + async def prideavatar(self, ctx: commands.Context, option: str = "lgbt", pixels: int = 64) -> None: + """ + This surrounds an avatar with a border of a specified LGBT flag. + + This defaults to the LGBT rainbow flag if none is given. + The amount of pixels can be given which determines the thickness of the flag border. + This has a maximum of 512px and defaults to a 64px border. + The full image is 1024x1024. + """ + option, pixels, flag = self.process_options(option, pixels) + if flag is None: + return await ctx.send("I don't have that flag!") + + async with ctx.typing(): + image_bytes = await ctx.author.avatar_url.read() + await self.send_image(ctx, image_bytes, pixels, flag, option) + + @prideavatar.command() + async def image(self, ctx: commands.Context, url: str, option: str = "lgbt", pixels: int = 64) -> None: + """ + This surrounds the image specified by the URL with a border of a specified LGBT flag. + + This defaults to the LGBT rainbow flag if none is given. + The amount of pixels can be given which determines the thickness of the flag border. + This has a maximum of 512px and defaults to a 64px border. + The full image is 1024x1024. + """ + option, pixels, flag = self.process_options(option, pixels) + if flag is None: + return await ctx.send("I don't have that flag!") + + async with ctx.typing(): + async with aiohttp.ClientSession() as session: + try: + response = await session.get(url) + except aiohttp.client_exceptions.ClientConnectorError: + return await ctx.send("Cannot connect to provided URL!") + except aiohttp.client_exceptions.InvalidURL: + return await ctx.send("Invalid URL!") + if response.status != 200: + return await ctx.send("Bad response from provided URL!") + image_bytes = await response.read() + await self.send_image(ctx, image_bytes, pixels, flag, option) + + @prideavatar.command() + async def flags(self, ctx: commands.Context) -> None: + """This lists the flags that can be used with the prideavatar command.""" + choices = sorted(set(self.GENDER_OPTIONS.values())) + options = "• " + "\n• ".join(choices) + embed = discord.Embed( + title="I have the following flags:", + description=options, + colour=Colours.soft_red + ) + + await ctx.send(embed=embed) + + @pfp_modify.command( + name='savatar', + aliases=('spookyavatar', 'spookify'), + brief='Spookify an user\'s avatar.' + ) + async def spooky_avatar(self, ctx: commands.Context, user: discord.Member = None) -> None: + """A command to print the user's spookified avatar.""" + if user is None: + user = ctx.message.author + + async with ctx.typing(): + embed = discord.Embed(colour=0xFF0000) + embed.title = "Is this you or am I just really paranoid?" + embed.set_author(name=str(user.name), icon_url=user.avatar_url) + + image_bytes = await ctx.author.avatar_url.read() + im = Image.open(BytesIO(image_bytes)) + modified_im = spookifications.get_random_effect(im) + modified_im.save(str(ctx.message.id)+'.png') + f = discord.File(str(ctx.message.id)+'.png') + embed.set_image(url='attachment://'+str(ctx.message.id)+'.png') + + await ctx.send(file=f, embed=embed) + os.remove(str(ctx.message.id)+'.png') + + +def setup(bot: commands.Bot) -> None: + """Load the PfpModify cog.""" + bot.add_cog(PfpModify(bot)) diff --git a/bot/exts/halloween/spookyavatar.py b/bot/exts/halloween/spookyavatar.py deleted file mode 100644 index 2d7df678..00000000 --- a/bot/exts/halloween/spookyavatar.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging -import os -from io import BytesIO - -import aiohttp -import discord -from PIL import Image -from discord.ext import commands - -from bot.utils.halloween import spookifications - -log = logging.getLogger(__name__) - - -class SpookyAvatar(commands.Cog): - """A cog that spookifies an avatar.""" - - def __init__(self, bot: commands.Bot): - self.bot = bot - - async def get(self, url: str) -> bytes: - """Returns the contents of the supplied URL.""" - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - return await resp.read() - - @commands.command(name='savatar', aliases=('spookyavatar', 'spookify'), - brief='Spookify an user\'s avatar.') - async def spooky_avatar(self, ctx: commands.Context, user: discord.Member = None) -> None: - """A command to print the user's spookified avatar.""" - if user is None: - user = ctx.message.author - - async with ctx.typing(): - embed = discord.Embed(colour=0xFF0000) - embed.title = "Is this you or am I just really paranoid?" - embed.set_author(name=str(user.name), icon_url=user.avatar_url) - - image_bytes = await ctx.author.avatar_url.read() - im = Image.open(BytesIO(image_bytes)) - modified_im = spookifications.get_random_effect(im) - modified_im.save(str(ctx.message.id)+'.png') - f = discord.File(str(ctx.message.id)+'.png') - embed.set_image(url='attachment://'+str(ctx.message.id)+'.png') - - await ctx.send(file=f, embed=embed) - os.remove(str(ctx.message.id)+'.png') - - -def setup(bot: commands.Bot) -> None: - """Spooky avatar Cog load.""" - bot.add_cog(SpookyAvatar(bot)) diff --git a/bot/exts/pride/pride_avatar.py b/bot/exts/pride/pride_avatar.py deleted file mode 100644 index 2eade796..00000000 --- a/bot/exts/pride/pride_avatar.py +++ /dev/null @@ -1,177 +0,0 @@ -import logging -from io import BytesIO -from pathlib import Path -from typing import Tuple - -import aiohttp -import discord -from PIL import Image, ImageDraw, UnidentifiedImageError -from discord.ext.commands import Bot, Cog, Context, group - -from bot.constants import Colours - -log = logging.getLogger(__name__) - -OPTIONS = { - "agender": "agender", - "androgyne": "androgyne", - "androgynous": "androgyne", - "aromantic": "aromantic", - "aro": "aromantic", - "ace": "asexual", - "asexual": "asexual", - "bigender": "bigender", - "bisexual": "bisexual", - "bi": "bisexual", - "demiboy": "demiboy", - "demigirl": "demigirl", - "demi": "demisexual", - "demisexual": "demisexual", - "gay": "gay", - "lgbt": "gay", - "queer": "gay", - "homosexual": "gay", - "fluid": "genderfluid", - "genderfluid": "genderfluid", - "genderqueer": "genderqueer", - "intersex": "intersex", - "lesbian": "lesbian", - "non-binary": "nonbinary", - "enby": "nonbinary", - "nb": "nonbinary", - "nonbinary": "nonbinary", - "omnisexual": "omnisexual", - "omni": "omnisexual", - "pansexual": "pansexual", - "pan": "pansexual", - "pangender": "pangender", - "poly": "polysexual", - "polysexual": "polysexual", - "polyamory": "polyamory", - "polyamorous": "polyamory", - "transgender": "transgender", - "trans": "transgender", - "trigender": "trigender" -} - - -class PrideAvatar(Cog): - """Put an LGBT spin on your avatar!""" - - def __init__(self, bot: Bot): - self.bot = bot - - @staticmethod - def crop_avatar(avatar: Image) -> Image: - """This crops the avatar into a circle.""" - mask = Image.new("L", avatar.size, 0) - draw = ImageDraw.Draw(mask) - draw.ellipse((0, 0) + avatar.size, fill=255) - avatar.putalpha(mask) - return avatar - - @staticmethod - def crop_ring(ring: Image, px: int) -> Image: - """This crops the ring into a circle.""" - mask = Image.new("L", ring.size, 0) - draw = ImageDraw.Draw(mask) - draw.ellipse((0, 0) + ring.size, fill=255) - draw.ellipse((px, px, 1024-px, 1024-px), fill=0) - ring.putalpha(mask) - return ring - - @staticmethod - def process_options(option: str, pixels: int) -> Tuple[str, int, str]: - """Does some shared preprocessing for the prideavatar commands.""" - return option.lower(), max(0, min(512, pixels)), OPTIONS.get(option) - - async def process_image(self, ctx: Context, image_bytes: bytes, pixels: int, flag: str, option: str) -> None: - """Constructs the final image, embeds it, and sends it.""" - try: - avatar = Image.open(BytesIO(image_bytes)) - except UnidentifiedImageError: - return await ctx.send("Cannot identify image from provided URL") - avatar = avatar.convert("RGBA").resize((1024, 1024)) - - avatar = self.crop_avatar(avatar) - - ring = Image.open(Path(f"bot/resources/pride/flags/{flag}.png")).resize((1024, 1024)) - ring = ring.convert("RGBA") - ring = self.crop_ring(ring, pixels) - - avatar.alpha_composite(ring, (0, 0)) - bufferedio = BytesIO() - avatar.save(bufferedio, format="PNG") - bufferedio.seek(0) - - file = discord.File(bufferedio, filename="pride_avatar.png") # Creates file to be used in embed - embed = discord.Embed( - name="Your Lovely Pride Avatar", - description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D" - ) - embed.set_image(url="attachment://pride_avatar.png") - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) - await ctx.send(file=file, embed=embed) - - @group(aliases=["avatarpride", "pridepfp", "prideprofile"], invoke_without_command=True) - async def prideavatar(self, ctx: Context, option: str = "lgbt", pixels: int = 64) -> None: - """ - This surrounds an avatar with a border of a specified LGBT flag. - - This defaults to the LGBT rainbow flag if none is given. - The amount of pixels can be given which determines the thickness of the flag border. - This has a maximum of 512px and defaults to a 64px border. - The full image is 1024x1024. - """ - option, pixels, flag = self.process_options(option, pixels) - if flag is None: - return await ctx.send("I don't have that flag!") - - async with ctx.typing(): - image_bytes = await ctx.author.avatar_url.read() - await self.process_image(ctx, image_bytes, pixels, flag, option) - - @prideavatar.command() - async def image(self, ctx: Context, url: str, option: str = "lgbt", pixels: int = 64) -> None: - """ - This surrounds the image specified by the URL with a border of a specified LGBT flag. - - This defaults to the LGBT rainbow flag if none is given. - The amount of pixels can be given which determines the thickness of the flag border. - This has a maximum of 512px and defaults to a 64px border. - The full image is 1024x1024. - """ - option, pixels, flag = self.process_options(option, pixels) - if flag is None: - return await ctx.send("I don't have that flag!") - - async with ctx.typing(): - async with aiohttp.ClientSession() as session: - try: - response = await session.get(url) - except aiohttp.client_exceptions.ClientConnectorError: - return await ctx.send("Cannot connect to provided URL!") - except aiohttp.client_exceptions.InvalidURL: - return await ctx.send("Invalid URL!") - if response.status != 200: - return await ctx.send("Bad response from provided URL!") - image_bytes = await response.read() - await self.process_image(ctx, image_bytes, pixels, flag, option) - - @prideavatar.command() - async def flags(self, ctx: Context) -> None: - """This lists the flags that can be used with the prideavatar command.""" - choices = sorted(set(OPTIONS.values())) - options = "• " + "\n• ".join(choices) - embed = discord.Embed( - title="I have the following flags:", - description=options, - colour=Colours.soft_red - ) - - await ctx.send(embed=embed) - - -def setup(bot: Bot) -> None: - """Cog load.""" - bot.add_cog(PrideAvatar(bot)) -- cgit v1.2.3 From e79a7730a3d8151622670f9ba38d05f2b267e2ec Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 18 Feb 2021 19:32:14 +0000 Subject: Log what func is being ran in the executor. --- bot/exts/evergreen/pfp_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/pfp_modify.py b/bot/exts/evergreen/pfp_modify.py index f356d2f6..4245432e 100644 --- a/bot/exts/evergreen/pfp_modify.py +++ b/bot/exts/evergreen/pfp_modify.py @@ -29,6 +29,7 @@ _EXECUTOR = ThreadPoolExecutor(10) async def in_thread(func: t.Callable, *args) -> asyncio.Future: """Allows non-async functions to work in async functions.""" + log.trace(f"Running {func.__name__} in an executor.") loop = asyncio.get_event_loop() return await loop.run_in_executor(_EXECUTOR, func, *args) @@ -138,7 +139,6 @@ class PfpModify(commands.Cog): await ctx.send(file=file, embed=embed) @commands.max_concurrency(1, commands.BucketType.guild, wait=True) - @commands.group() async def pfp_modify(self, ctx: commands.Context) -> None: """Groups all of the pfp modifing commands to allow a single concurrency limit.""" if not ctx.invoked_subcommand: -- cgit v1.2.3 From 032d4ae8300ed4570e0b471dd49f628f446cb1fa Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 18 Feb 2021 19:43:26 +0000 Subject: Add root alias support for commands --- bot/__init__.py | 9 +++++++++ bot/bot.py | 41 ++++++++++++++++++++++++++++++++++++++++ bot/command.py | 18 ++++++++++++++++++ bot/exts/evergreen/help.py | 4 +++- bot/exts/evergreen/pfp_modify.py | 3 ++- 5 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 bot/command.py (limited to 'bot') diff --git a/bot/__init__.py b/bot/__init__.py index bdb18666..c8550537 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -2,10 +2,13 @@ import asyncio import logging import logging.handlers import os +from functools import partial, partialmethod from pathlib import Path import arrow +from discord.ext import commands +from bot.command import Command from bot.constants import Client @@ -70,3 +73,9 @@ logging.getLogger().info('Logging initialization complete') # On Windows, the selector event loop is required for aiodns. if os.name == "nt": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + +# Monkey-patch discord.py decorators to use the Command subclass which supports root aliases. +# Must be patched before any cogs are added. +commands.command = partial(commands.command, cls=Command) +commands.GroupMixin.command = partialmethod(commands.GroupMixin.command, cls=Command) diff --git a/bot/bot.py b/bot/bot.py index 97b09243..f51139bb 100644 --- a/bot/bot.py +++ b/bot/bot.py @@ -64,6 +64,26 @@ class Bot(commands.Bot): super().add_cog(cog) log.info(f"Cog loaded: {cog.qualified_name}") + def add_command(self, command: commands.Command) -> None: + """Add `command` as normal and then add its root aliases to the bot.""" + super().add_command(command) + self._add_root_aliases(command) + + def remove_command(self, name: str) -> Optional[commands.Command]: + """ + Remove a command/alias as normal and then remove its root aliases from the bot. + + Individual root aliases cannot be removed by this function. + To remove them, either remove the entire command or manually edit `bot.all_commands`. + """ + command = super().remove_command(name) + if command is None: + # Even if it's a root alias, there's no way to get the Bot instance to remove the alias. + return + + self._remove_root_aliases(command) + return command + async def on_command_error(self, context: commands.Context, exception: DiscordException) -> None: """Check command errors for UserInputError and reset the cooldown if thrown.""" if isinstance(exception, commands.UserInputError): @@ -124,6 +144,27 @@ class Bot(commands.Bot): """ await self._guild_available.wait() + def _add_root_aliases(self, command: commands.Command) -> None: + """Recursively add root aliases for `command` and any of its subcommands.""" + if isinstance(command, commands.Group): + for subcommand in command.commands: + self._add_root_aliases(subcommand) + + for alias in getattr(command, "root_aliases", ()): + if alias in self.all_commands: + raise commands.CommandRegistrationError(alias, alias_conflict=True) + + self.all_commands[alias] = command + + def _remove_root_aliases(self, command: commands.Command) -> None: + """Recursively remove root aliases for `command` and any of its subcommands.""" + if isinstance(command, commands.Group): + for subcommand in command.commands: + self._remove_root_aliases(subcommand) + + for alias in getattr(command, "root_aliases", ()): + self.all_commands.pop(alias, None) + _allowed_roles = [discord.Object(id_) for id_ in constants.MODERATION_ROLES] diff --git a/bot/command.py b/bot/command.py new file mode 100644 index 00000000..0fb900f7 --- /dev/null +++ b/bot/command.py @@ -0,0 +1,18 @@ +from discord.ext import commands + + +class Command(commands.Command): + """ + A `discord.ext.commands.Command` subclass which supports root aliases. + + A `root_aliases` keyword argument is added, which is a sequence of alias names that will act as + top-level commands rather than being aliases of the command's group. It's stored as an attribute + also named `root_aliases`. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.root_aliases = kwargs.get("root_aliases", []) + + if not isinstance(self.root_aliases, (list, tuple)): + raise TypeError("Root aliases of a command must be a list or a tuple of strings.") diff --git a/bot/exts/evergreen/help.py b/bot/exts/evergreen/help.py index 91147243..f557e42e 100644 --- a/bot/exts/evergreen/help.py +++ b/bot/exts/evergreen/help.py @@ -289,7 +289,9 @@ class HelpSession: parent = self.query.full_parent_name + ' ' if self.query.parent else '' paginator.add_line(f'**```{prefix}{parent}{signature}```**') - aliases = ', '.join(f'`{a}`' for a in self.query.aliases) + aliases = [f"`{alias}`" if not parent else f"`{parent} {alias}`" for alias in self.query.aliases] + aliases += [f"`{alias}`" for alias in getattr(self.query, "root_aliases", ())] + aliases = ", ".join(sorted(aliases)) if aliases: paginator.add_line(f'**Can also use:** {aliases}\n') diff --git a/bot/exts/evergreen/pfp_modify.py b/bot/exts/evergreen/pfp_modify.py index 4245432e..a3f7e3f8 100644 --- a/bot/exts/evergreen/pfp_modify.py +++ b/bot/exts/evergreen/pfp_modify.py @@ -139,12 +139,13 @@ class PfpModify(commands.Cog): await ctx.send(file=file, embed=embed) @commands.max_concurrency(1, commands.BucketType.guild, wait=True) + @commands.group() async def pfp_modify(self, ctx: commands.Context) -> None: """Groups all of the pfp modifing commands to allow a single concurrency limit.""" if not ctx.invoked_subcommand: await ctx.send_help(ctx.command) - @pfp_modify.command(name="8bitify") + @pfp_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(): -- cgit v1.2.3 From c7de5a2577b4f2975b3fab683f2d7459c5bda636 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 18 Feb 2021 22:04:27 +0000 Subject: Extend root aliases to support commands.Group --- bot/__init__.py | 6 +++++- bot/exts/evergreen/pfp_modify.py | 9 +++++++-- bot/group.py | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 bot/group.py (limited to 'bot') diff --git a/bot/__init__.py b/bot/__init__.py index c8550537..d0992912 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -10,6 +10,7 @@ from discord.ext import commands from bot.command import Command from bot.constants import Client +from bot.group import Group # Configure the "TRACE" logging level (e.g. "log.trace(message)") @@ -75,7 +76,10 @@ if os.name == "nt": asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) -# Monkey-patch discord.py decorators to use the Command subclass which supports root aliases. +# Monkey-patch discord.py decorators to use the both the Command and Group subclasses which supports root aliases. # Must be patched before any cogs are added. commands.command = partial(commands.command, cls=Command) commands.GroupMixin.command = partialmethod(commands.GroupMixin.command, cls=Command) + +commands.group = partial(commands.group, cls=Group) +commands.GroupMixin.group = partialmethod(commands.GroupMixin.group, cls=Group) diff --git a/bot/exts/evergreen/pfp_modify.py b/bot/exts/evergreen/pfp_modify.py index a3f7e3f8..8a3eb77c 100644 --- a/bot/exts/evergreen/pfp_modify.py +++ b/bot/exts/evergreen/pfp_modify.py @@ -173,7 +173,7 @@ class PfpModify(commands.Cog): await ctx.send(file=file, embed=embed) - @pfp_modify.command(pass_context=True, aliases=["easterify"]) + @pfp_modify.command(pass_context=True, aliases=["easterify"], root_aliases=("easterify", "avatareasterify")) async def avatareasterify(self, ctx: commands.Context, *colours: t.Union[discord.Colour, str]) -> None: """ This "Easterifies" the user's avatar. @@ -249,7 +249,11 @@ class PfpModify(commands.Cog): await ctx.send(file=file, embed=embed) - @pfp_modify.group(aliases=["avatarpride", "pridepfp", "prideprofile"], invoke_without_command=True) + @pfp_modify.group( + aliases=["avatarpride", "pridepfp", "prideprofile"], + root_aliases=("prideavatar", "avatarpride", "pridepfp", "prideprofile"), + invoke_without_command=True + ) async def prideavatar(self, ctx: commands.Context, option: str = "lgbt", pixels: int = 64) -> None: """ This surrounds an avatar with a border of a specified LGBT flag. @@ -310,6 +314,7 @@ class PfpModify(commands.Cog): @pfp_modify.command( name='savatar', aliases=('spookyavatar', 'spookify'), + root_aliases=('spookyavatar', 'spookify'), brief='Spookify an user\'s avatar.' ) async def spooky_avatar(self, ctx: commands.Context, user: discord.Member = None) -> None: diff --git a/bot/group.py b/bot/group.py new file mode 100644 index 00000000..77092adf --- /dev/null +++ b/bot/group.py @@ -0,0 +1,18 @@ +from discord.ext import commands + + +class Group(commands.Group): + """ + A `discord.ext.commands.Group` subclass which supports root aliases. + + A `root_aliases` keyword argument is added, which is a sequence of alias names that will act as + top-level commands rather than being aliases of the command's group. It's stored as an attribute + also named `root_aliases`. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.root_aliases = kwargs.get("root_aliases", []) + + if not isinstance(self.root_aliases, (list, tuple)): + raise TypeError("Root aliases of a command must be a list or a tuple of strings.") -- cgit v1.2.3 From a3a6550b80543c3ce7da28cf00b450b561e57ca9 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 19 Feb 2021 23:25:08 +0000 Subject: Share common code. --- bot/exts/evergreen/pfp_modify.py | 239 ++++++++++++++++++++------------------- 1 file changed, 122 insertions(+), 117 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/pfp_modify.py b/bot/exts/evergreen/pfp_modify.py index 8a3eb77c..5fb3b89d 100644 --- a/bot/exts/evergreen/pfp_modify.py +++ b/bot/exts/evergreen/pfp_modify.py @@ -1,16 +1,15 @@ import asyncio import json import logging -import os import typing as t from concurrent.futures import ThreadPoolExecutor from io import BytesIO from pathlib import Path import aiofiles -import aiohttp import discord -from PIL import Image, ImageDraw, ImageOps, UnidentifiedImageError +from PIL import Image, ImageDraw, ImageOps +from aiohttp import client_exceptions from discord.ext import commands from bot.constants import Colours @@ -69,7 +68,7 @@ class PfpModify(commands.Cog): return (r, g, b) @staticmethod - def crop_avatar(avatar: Image) -> Image: + def crop_avatar_circle(avatar: Image) -> Image: """This crops the avatar given into a circle.""" mask = Image.new("L", avatar.size, 0) draw = ImageDraw.Draw(mask) @@ -87,58 +86,71 @@ class PfpModify(commands.Cog): ring.putalpha(mask) return ring - def process_options(self, option: str, pixels: int) -> t.Tuple[str, int, str]: - """Does some shared preprocessing for the prideavatar commands.""" - return option.lower(), max(0, min(512, pixels)), self.GENDER_OPTIONS.get(option) + @staticmethod + def _apply_effect(image_bytes: bytes, effect: t.Callable, *args) -> discord.File: + im = Image.open(BytesIO(image_bytes)) + im = im.convert("RGBA") + im = effect(im, *args) - def process_image( - self, - image_bytes: bytes, + bufferedio = BytesIO() + im.save(bufferedio, format="PNG") + bufferedio.seek(0) + + return discord.File(bufferedio, filename="modified_avatar.png") + + @staticmethod + def pridify_effect( + image: Image, pixels: int, flag: str - ) -> discord.File: - """Constructs and returns the final image. Used by the pride commands.""" - # This line can raise UnidentifiedImageError and must be handled by the calling func. - avatar = Image.open(BytesIO(image_bytes)) - avatar = avatar.convert("RGBA").resize((1024, 1024)) - - avatar = self.crop_avatar(avatar) + ) -> Image: + """Applies the pride effect to the given image.""" + image = image.resize((1024, 1024)) + image = PfpModify.crop_avatar_circle(image) ring = Image.open(Path(f"bot/resources/pride/flags/{flag}.png")).resize((1024, 1024)) ring = ring.convert("RGBA") - ring = self.crop_ring(ring, pixels) - - avatar.alpha_composite(ring, (0, 0)) - bufferedio = BytesIO() - avatar.save(bufferedio, format="PNG") - bufferedio.seek(0) + ring = PfpModify.crop_ring(ring, pixels) - return discord.File(bufferedio, filename="pride_avatar.png") # Creates file to be used in embed + image.alpha_composite(ring, (0, 0)) + return image - async def send_image( - self, - ctx: commands.Context, - image_bytes: bytes, - pixels: int, - flag: str, - option: str - ) -> None: - """Gets and sends the image in an embed. Used by the pride commands.""" - try: - file = await in_thread(self.process_image, image_bytes, pixels, flag) - except UnidentifiedImageError: - ctx.send("Cannot identify image from provided URL") - return + @staticmethod + def eight_bitify_effect(image: Image) -> Image: + """Applies the 8bit effect to the given image.""" + image = image.resize((32, 32), resample=Image.NEAREST).resize((1024, 1024), resample=Image.NEAREST) + return image.quantize() - embed = discord.Embed( - name="Your Lovely Pride Avatar", - description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D" - ) - embed.set_image(url="attachment://pride_avatar.png") - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) - await ctx.send(file=file, embed=embed) + @staticmethod + def easterify_effect(image: Image, overlay_image: Image = None) -> Image: + """Applies the easter effect to the given image.""" + if overlay_image: + ratio = 64 / overlay_image.height + overlay_image = overlay_image.resize(( + round(overlay_image.width * ratio), + round(overlay_image.height * ratio) + )) + overlay_image = overlay_image.convert("RGBA") + else: + overlay_image = overlay_image = Image.open(Path("bot/resources/easter/chocolate_bunny.png")) + + alpha = image.getchannel("A").getdata() + image = image.convert("RGB") + image = ImageOps.posterize(image, 6) + + data = image.getdata() + setted_data = set(data) + new_d = {} + + for x in setted_data: + new_d[x] = PfpModify.closest(x) + new_data = [(*new_d[x], alpha[i]) if x in new_d else x for i, x in enumerate(data)] + + im = Image.new("RGBA", image.size) + im.putdata(new_data) + im.alpha_composite(overlay_image, (im.width - overlay_image.width, (im.height - overlay_image.height)//2)) + return im - @commands.max_concurrency(1, commands.BucketType.guild, wait=True) @commands.group() async def pfp_modify(self, ctx: commands.Context) -> None: """Groups all of the pfp modifing commands to allow a single concurrency limit.""" @@ -150,25 +162,18 @@ class PfpModify(commands.Cog): """Pixelates your avatar and changes the palette to an 8bit one.""" async with ctx.typing(): image_bytes = await ctx.author.avatar_url.read() - avatar = Image.open(BytesIO(image_bytes)) - avatar = avatar.convert("RGBA").resize((1024, 1024)) - - # Pixilate and quantize - eightbit = avatar.resize((32, 32), resample=Image.NEAREST).resize((1024, 1024), resample=Image.NEAREST) - eightbit = eightbit.quantize() - - bufferedio = BytesIO() - eightbit.save(bufferedio, format="PNG") - bufferedio.seek(0) - - file = discord.File(bufferedio, filename="8bitavatar.png") + file = await in_thread( + self._apply_effect, + image_bytes, + self.eight_bitify_effect + ) 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="attachment://8bitavatar.png") + embed.set_image(url="attachment://modified_avatar.png") embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) await ctx.send(file=file, embed=embed) @@ -194,61 +199,59 @@ class PfpModify(commands.Cog): return args[0] async with ctx.typing(): - - # Grabs image of avatar - image_bytes = await ctx.author.avatar_url_as(size=256).read() - - old = Image.open(BytesIO(image_bytes)) - old = old.convert("RGBA") - - # Grabs alpha channel since posterize can't be used with an RGBA image. - alpha = old.getchannel("A").getdata() - old = old.convert("RGB") - old = ImageOps.posterize(old, 6) - - data = old.getdata() - setted_data = set(data) - new_d = {} - - for x in setted_data: - new_d[x] = self.closest(x) - await asyncio.sleep(0) # Ensures discord doesn't break in the background. - new_data = [(*new_d[x], alpha[i]) if x in new_d else x for i, x in enumerate(data)] - - im = Image.new("RGBA", old.size) - im.putdata(new_data) - + 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. - return await send_message(egg) - - ratio = 64 / egg.height - egg = egg.resize((round(egg.width * ratio), round(egg.height * ratio))) - egg = egg.convert("RGBA") - im.alpha_composite(egg, (im.width - egg.width, (im.height - egg.height)//2)) # Right centre. + await send_message(egg) + return ctx.send = send_message # Reassigns ctx.send - else: - bunny = Image.open(Path("bot/resources/easter/chocolate_bunny.png")) - im.alpha_composite(bunny, (im.width - bunny.width, (im.height - bunny.height)//2)) # Right centre. - bufferedio = BytesIO() - im.save(bufferedio, format="PNG") - - bufferedio.seek(0) + image_bytes = await ctx.author.avatar_url_as(size=256).read() + file = await in_thread( + self._apply_effect, + image_bytes, + self.easterify_effect, + egg + ) - file = discord.File(bufferedio, filename="easterified_avatar.png") # Creates file to be used in embed embed = discord.Embed( name="Your Lovely Easterified Avatar", description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D" ) - embed.set_image(url="attachment://easterified_avatar.png") + embed.set_image(url="attachment://modified_avatar.png") embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) await ctx.send(file=file, embed=embed) + async def send_pride_image( + self, + ctx: commands.Context, + image_bytes: bytes, + pixels: int, + flag: str, + option: str + ) -> None: + """Gets and sends the image in an embed. Used by the pride commands.""" + async with ctx.typing(): + file = await in_thread( + self._apply_effect, + image_bytes, + self.pridify_effect, + pixels, + flag + ) + + embed = discord.Embed( + name="Your Lovely Pride Avatar", + description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D" + ) + embed.set_image(url="attachment://modified_avatar.png") + embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + await ctx.send(file=file, embed=embed) + @pfp_modify.group( aliases=["avatarpride", "pridepfp", "prideprofile"], root_aliases=("prideavatar", "avatarpride", "pridepfp", "prideprofile"), @@ -263,13 +266,15 @@ class PfpModify(commands.Cog): This has a maximum of 512px and defaults to a 64px border. The full image is 1024x1024. """ - option, pixels, flag = self.process_options(option, pixels) + option = option.lower() + pixels = max(0, min(512, pixels)) + flag = self.GENDER_OPTIONS.get(option) if flag is None: return await ctx.send("I don't have that flag!") async with ctx.typing(): image_bytes = await ctx.author.avatar_url.read() - await self.send_image(ctx, image_bytes, pixels, flag, option) + await self.send_pride_image(ctx, image_bytes, pixels, flag, option) @prideavatar.command() async def image(self, ctx: commands.Context, url: str, option: str = "lgbt", pixels: int = 64) -> None: @@ -281,22 +286,24 @@ class PfpModify(commands.Cog): This has a maximum of 512px and defaults to a 64px border. The full image is 1024x1024. """ - option, pixels, flag = self.process_options(option, pixels) + option = option.lower() + pixels = max(0, min(512, pixels)) + flag = self.GENDER_OPTIONS.get(option) if flag is None: return await ctx.send("I don't have that flag!") async with ctx.typing(): - async with aiohttp.ClientSession() as session: + async with self.bot.http_session as session: try: response = await session.get(url) - except aiohttp.client_exceptions.ClientConnectorError: + except client_exceptions.ClientConnectorError: return await ctx.send("Cannot connect to provided URL!") - except aiohttp.client_exceptions.InvalidURL: + except client_exceptions.InvalidURL: return await ctx.send("Invalid URL!") if response.status != 200: return await ctx.send("Bad response from provided URL!") image_bytes = await response.read() - await self.send_image(ctx, image_bytes, pixels, flag, option) + await self.send_pride_image(ctx, image_bytes, pixels, flag, option) @prideavatar.command() async def flags(self, ctx: commands.Context) -> None: @@ -308,7 +315,6 @@ class PfpModify(commands.Cog): description=options, colour=Colours.soft_red ) - await ctx.send(embed=embed) @pfp_modify.command( @@ -323,19 +329,18 @@ class PfpModify(commands.Cog): user = ctx.message.author async with ctx.typing(): - embed = discord.Embed(colour=0xFF0000) - embed.title = "Is this you or am I just really paranoid?" + image_bytes = await ctx.author.avatar_url.read() + file = await in_thread(self._apply_effect, image_bytes, spookifications.get_random_effect) + + embed = discord.Embed( + title="Is this you or am I just really paranoid?", + colour=0xFF0000 + ) embed.set_author(name=str(user.name), icon_url=user.avatar_url) + embed.set_image(url='attachment://modified_avatar.png') + embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) - image_bytes = await ctx.author.avatar_url.read() - im = Image.open(BytesIO(image_bytes)) - modified_im = spookifications.get_random_effect(im) - modified_im.save(str(ctx.message.id)+'.png') - f = discord.File(str(ctx.message.id)+'.png') - embed.set_image(url='attachment://'+str(ctx.message.id)+'.png') - - await ctx.send(file=f, embed=embed) - os.remove(str(ctx.message.id)+'.png') + await ctx.send(file=file, embed=embed) def setup(bot: commands.Bot) -> None: -- cgit v1.2.3 From 326d50cdc039556e88e0b210a7124a4cc47a9275 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 20 Feb 2021 15:44:47 +0000 Subject: Split effects from cog --- bot/exts/evergreen/pfp_modify.py | 348 --------------------- .../evergreen/profile_pic_modification/__init__.py | 0 .../evergreen/profile_pic_modification/_effects.py | 122 ++++++++ .../profile_pic_modification/pfp_modify.py | 234 ++++++++++++++ 4 files changed, 356 insertions(+), 348 deletions(-) delete mode 100644 bot/exts/evergreen/pfp_modify.py create mode 100644 bot/exts/evergreen/profile_pic_modification/__init__.py create mode 100644 bot/exts/evergreen/profile_pic_modification/_effects.py create mode 100644 bot/exts/evergreen/profile_pic_modification/pfp_modify.py (limited to 'bot') diff --git a/bot/exts/evergreen/pfp_modify.py b/bot/exts/evergreen/pfp_modify.py deleted file mode 100644 index 5fb3b89d..00000000 --- a/bot/exts/evergreen/pfp_modify.py +++ /dev/null @@ -1,348 +0,0 @@ -import asyncio -import json -import logging -import typing as t -from concurrent.futures import ThreadPoolExecutor -from io import BytesIO -from pathlib import Path - -import aiofiles -import discord -from PIL import Image, ImageDraw, ImageOps -from aiohttp import client_exceptions -from discord.ext import commands - -from bot.constants import Colours -from bot.utils.halloween import spookifications - -log = logging.getLogger(__name__) - -EASTER_COLOURS = [ - (255, 247, 0), (255, 255, 224), (0, 255, 127), (189, 252, 201), (255, 192, 203), - (255, 160, 122), (181, 115, 220), (221, 160, 221), (200, 162, 200), (238, 130, 238), - (135, 206, 235), (0, 204, 204), (64, 224, 208) -] # Pastel colours - Easter-like - -_EXECUTOR = ThreadPoolExecutor(10) - - -async def in_thread(func: t.Callable, *args) -> asyncio.Future: - """Allows non-async functions to work in async functions.""" - log.trace(f"Running {func.__name__} in an executor.") - loop = asyncio.get_event_loop() - return await loop.run_in_executor(_EXECUTOR, func, *args) - - -class PfpModify(commands.Cog): - """Various commands for users to change their own profile picture.""" - - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - self.bot.loop.create_task(self.init_cog()) - - async def init_cog(self) -> None: - """Initial load from resources asynchronously.""" - async with aiofiles.open('bot/resources/pride/gender_options.json') as f: - self.GENDER_OPTIONS = json.loads(await f.read()) - - @staticmethod - def closest(x: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: - """ - Finds the closest easter colour to a given pixel. - - Returns a merge between the original colour and the closest colour - """ - r1, g1, b1 = x - - def distance(point: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: - """Finds the difference between a pastel colour and the original pixel colour.""" - r2, g2, b2 = point - return ((r1 - r2)**2 + (g1 - g2)**2 + (b1 - b2)**2) - - closest_colours = sorted(EASTER_COLOURS, key=lambda point: distance(point)) - r2, g2, b2 = closest_colours[0] - r = (r1 + r2) // 2 - g = (g1 + g2) // 2 - b = (b1 + b2) // 2 - - return (r, g, b) - - @staticmethod - def crop_avatar_circle(avatar: Image) -> Image: - """This crops the avatar given into a circle.""" - mask = Image.new("L", avatar.size, 0) - draw = ImageDraw.Draw(mask) - draw.ellipse((0, 0) + avatar.size, fill=255) - avatar.putalpha(mask) - return avatar - - @staticmethod - def crop_ring(ring: Image, px: int) -> Image: - """This crops the given ring into a circle.""" - mask = Image.new("L", ring.size, 0) - draw = ImageDraw.Draw(mask) - draw.ellipse((0, 0) + ring.size, fill=255) - draw.ellipse((px, px, 1024-px, 1024-px), fill=0) - ring.putalpha(mask) - return ring - - @staticmethod - def _apply_effect(image_bytes: bytes, effect: t.Callable, *args) -> discord.File: - im = Image.open(BytesIO(image_bytes)) - im = im.convert("RGBA") - im = effect(im, *args) - - bufferedio = BytesIO() - im.save(bufferedio, format="PNG") - bufferedio.seek(0) - - return discord.File(bufferedio, filename="modified_avatar.png") - - @staticmethod - def pridify_effect( - image: Image, - pixels: int, - flag: str - ) -> Image: - """Applies the pride effect to the given image.""" - image = image.resize((1024, 1024)) - image = PfpModify.crop_avatar_circle(image) - - ring = Image.open(Path(f"bot/resources/pride/flags/{flag}.png")).resize((1024, 1024)) - ring = ring.convert("RGBA") - ring = PfpModify.crop_ring(ring, pixels) - - image.alpha_composite(ring, (0, 0)) - return image - - @staticmethod - def eight_bitify_effect(image: Image) -> Image: - """Applies the 8bit effect to the given image.""" - image = image.resize((32, 32), resample=Image.NEAREST).resize((1024, 1024), resample=Image.NEAREST) - return image.quantize() - - @staticmethod - def easterify_effect(image: Image, overlay_image: Image = None) -> Image: - """Applies the easter effect to the given image.""" - if overlay_image: - ratio = 64 / overlay_image.height - overlay_image = overlay_image.resize(( - round(overlay_image.width * ratio), - round(overlay_image.height * ratio) - )) - overlay_image = overlay_image.convert("RGBA") - else: - overlay_image = overlay_image = Image.open(Path("bot/resources/easter/chocolate_bunny.png")) - - alpha = image.getchannel("A").getdata() - image = image.convert("RGB") - image = ImageOps.posterize(image, 6) - - data = image.getdata() - setted_data = set(data) - new_d = {} - - for x in setted_data: - new_d[x] = PfpModify.closest(x) - new_data = [(*new_d[x], alpha[i]) if x in new_d else x for i, x in enumerate(data)] - - im = Image.new("RGBA", image.size) - im.putdata(new_data) - im.alpha_composite(overlay_image, (im.width - overlay_image.width, (im.height - overlay_image.height)//2)) - return im - - @commands.group() - async def pfp_modify(self, ctx: commands.Context) -> None: - """Groups all of the pfp modifing commands to allow a single concurrency limit.""" - if not ctx.invoked_subcommand: - await ctx.send_help(ctx.command) - - @pfp_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(): - image_bytes = await ctx.author.avatar_url.read() - file = await in_thread( - self._apply_effect, - image_bytes, - self.eight_bitify_effect - ) - - 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="attachment://modified_avatar.png") - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) - - await ctx.send(file=file, embed=embed) - - @pfp_modify.command(pass_context=True, aliases=["easterify"], root_aliases=("easterify", "avatareasterify")) - async def avatareasterify(self, ctx: commands.Context, *colours: t.Union[discord.Colour, str]) -> None: - """ - This "Easterifies" 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. - """ - async def send(*args, **kwargs) -> str: - """ - This replaces the original ctx.send. - - When invoking the egg decorating command, the egg itself doesn't print to to the channel. - Returns the message content so that if any errors occur, the error message can be output. - """ - if args: - return args[0] - - async with ctx.typing(): - 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 - - image_bytes = await ctx.author.avatar_url_as(size=256).read() - file = await in_thread( - self._apply_effect, - image_bytes, - self.easterify_effect, - egg - ) - - embed = discord.Embed( - name="Your Lovely Easterified Avatar", - description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D" - ) - embed.set_image(url="attachment://modified_avatar.png") - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) - - await ctx.send(file=file, embed=embed) - - async def send_pride_image( - self, - ctx: commands.Context, - image_bytes: bytes, - pixels: int, - flag: str, - option: str - ) -> None: - """Gets and sends the image in an embed. Used by the pride commands.""" - async with ctx.typing(): - file = await in_thread( - self._apply_effect, - image_bytes, - self.pridify_effect, - pixels, - flag - ) - - embed = discord.Embed( - name="Your Lovely Pride Avatar", - description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D" - ) - embed.set_image(url="attachment://modified_avatar.png") - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) - await ctx.send(file=file, embed=embed) - - @pfp_modify.group( - aliases=["avatarpride", "pridepfp", "prideprofile"], - root_aliases=("prideavatar", "avatarpride", "pridepfp", "prideprofile"), - invoke_without_command=True - ) - async def prideavatar(self, ctx: commands.Context, option: str = "lgbt", pixels: int = 64) -> None: - """ - This surrounds an avatar with a border of a specified LGBT flag. - - This defaults to the LGBT rainbow flag if none is given. - The amount of pixels can be given which determines the thickness of the flag border. - This has a maximum of 512px and defaults to a 64px border. - The full image is 1024x1024. - """ - option = option.lower() - pixels = max(0, min(512, pixels)) - flag = self.GENDER_OPTIONS.get(option) - if flag is None: - return await ctx.send("I don't have that flag!") - - async with ctx.typing(): - image_bytes = await ctx.author.avatar_url.read() - await self.send_pride_image(ctx, image_bytes, pixels, flag, option) - - @prideavatar.command() - async def image(self, ctx: commands.Context, url: str, option: str = "lgbt", pixels: int = 64) -> None: - """ - This surrounds the image specified by the URL with a border of a specified LGBT flag. - - This defaults to the LGBT rainbow flag if none is given. - The amount of pixels can be given which determines the thickness of the flag border. - This has a maximum of 512px and defaults to a 64px border. - The full image is 1024x1024. - """ - option = option.lower() - pixels = max(0, min(512, pixels)) - flag = self.GENDER_OPTIONS.get(option) - if flag is None: - return await ctx.send("I don't have that flag!") - - async with ctx.typing(): - async with self.bot.http_session as session: - try: - response = await session.get(url) - except client_exceptions.ClientConnectorError: - return await ctx.send("Cannot connect to provided URL!") - except client_exceptions.InvalidURL: - return await ctx.send("Invalid URL!") - if response.status != 200: - return await ctx.send("Bad response from provided URL!") - image_bytes = await response.read() - await self.send_pride_image(ctx, image_bytes, pixels, flag, option) - - @prideavatar.command() - async def flags(self, ctx: commands.Context) -> None: - """This lists the flags that can be used with the prideavatar command.""" - choices = sorted(set(self.GENDER_OPTIONS.values())) - options = "• " + "\n• ".join(choices) - embed = discord.Embed( - title="I have the following flags:", - description=options, - colour=Colours.soft_red - ) - await ctx.send(embed=embed) - - @pfp_modify.command( - name='savatar', - aliases=('spookyavatar', 'spookify'), - root_aliases=('spookyavatar', 'spookify'), - brief='Spookify an user\'s avatar.' - ) - async def spooky_avatar(self, ctx: commands.Context, user: discord.Member = None) -> None: - """A command to print the user's spookified avatar.""" - if user is None: - user = ctx.message.author - - async with ctx.typing(): - image_bytes = await ctx.author.avatar_url.read() - file = await in_thread(self._apply_effect, image_bytes, spookifications.get_random_effect) - - embed = discord.Embed( - title="Is this you or am I just really paranoid?", - colour=0xFF0000 - ) - embed.set_author(name=str(user.name), icon_url=user.avatar_url) - embed.set_image(url='attachment://modified_avatar.png') - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) - - await ctx.send(file=file, embed=embed) - - -def setup(bot: commands.Bot) -> None: - """Load the PfpModify cog.""" - bot.add_cog(PfpModify(bot)) diff --git a/bot/exts/evergreen/profile_pic_modification/__init__.py b/bot/exts/evergreen/profile_pic_modification/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py new file mode 100644 index 00000000..fbe5f706 --- /dev/null +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -0,0 +1,122 @@ +import typing as t +from io import BytesIO +from pathlib import Path + +import discord +from PIL import Image, ImageDraw, ImageOps + +EASTER_COLOURS = [ + (255, 247, 0), (255, 255, 224), (0, 255, 127), (189, 252, 201), (255, 192, 203), + (255, 160, 122), (181, 115, 220), (221, 160, 221), (200, 162, 200), (238, 130, 238), + (135, 206, 235), (0, 204, 204), (64, 224, 208) +] # Pastel colours - Easter-like + + +class PfpEffects(): + """Implements various image effects.""" + + @staticmethod + def closest(x: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: + """ + Finds the closest easter colour to a given pixel. + + Returns a merge between the original colour and the closest colour + """ + r1, g1, b1 = x + + def distance(point: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: + """Finds the difference between a pastel colour and the original pixel colour.""" + r2, g2, b2 = point + return ((r1 - r2)**2 + (g1 - g2)**2 + (b1 - b2)**2) + + closest_colours = sorted(EASTER_COLOURS, key=lambda point: distance(point)) + r2, g2, b2 = closest_colours[0] + r = (r1 + r2) // 2 + g = (g1 + g2) // 2 + b = (b1 + b2) // 2 + + return (r, g, b) + + @staticmethod + def crop_avatar_circle(avatar: Image) -> Image: + """This crops the avatar given into a circle.""" + mask = Image.new("L", avatar.size, 0) + draw = ImageDraw.Draw(mask) + draw.ellipse((0, 0) + avatar.size, fill=255) + avatar.putalpha(mask) + return avatar + + @staticmethod + def crop_ring(ring: Image, px: int) -> Image: + """This crops the given ring into a circle.""" + mask = Image.new("L", ring.size, 0) + draw = ImageDraw.Draw(mask) + draw.ellipse((0, 0) + ring.size, fill=255) + draw.ellipse((px, px, 1024-px, 1024-px), fill=0) + ring.putalpha(mask) + return ring + + @staticmethod + def _apply_effect(image_bytes: bytes, effect: t.Callable, *args) -> discord.File: + im = Image.open(BytesIO(image_bytes)) + im = im.convert("RGBA") + im = effect(im, *args) + + bufferedio = BytesIO() + im.save(bufferedio, format="PNG") + bufferedio.seek(0) + + return discord.File(bufferedio, filename="modified_avatar.png") + + @staticmethod + def pridify_effect( + image: Image, + pixels: int, + flag: str + ) -> Image: + """Applies the pride effect to the given image.""" + image = image.resize((1024, 1024)) + image = PfpEffects.crop_avatar_circle(image) + + ring = Image.open(Path(f"bot/resources/pride/flags/{flag}.png")).resize((1024, 1024)) + ring = ring.convert("RGBA") + ring = PfpEffects.crop_ring(ring, pixels) + + image.alpha_composite(ring, (0, 0)) + return image + + @staticmethod + def eight_bitify_effect(image: Image) -> Image: + """Applies the 8bit effect to the given image.""" + image = image.resize((32, 32), resample=Image.NEAREST).resize((1024, 1024), resample=Image.NEAREST) + return image.quantize() + + @staticmethod + def easterify_effect(image: Image, overlay_image: Image = None) -> Image: + """Applies the easter effect to the given image.""" + if overlay_image: + ratio = 64 / overlay_image.height + overlay_image = overlay_image.resize(( + round(overlay_image.width * ratio), + round(overlay_image.height * ratio) + )) + overlay_image = overlay_image.convert("RGBA") + else: + overlay_image = overlay_image = Image.open(Path("bot/resources/easter/chocolate_bunny.png")) + + alpha = image.getchannel("A").getdata() + image = image.convert("RGB") + image = ImageOps.posterize(image, 6) + + data = image.getdata() + setted_data = set(data) + new_d = {} + + for x in setted_data: + new_d[x] = PfpEffects.closest(x) + new_data = [(*new_d[x], alpha[i]) if x in new_d else x for i, x in enumerate(data)] + + im = Image.new("RGBA", image.size) + im.putdata(new_data) + im.alpha_composite(overlay_image, (im.width - overlay_image.width, (im.height - overlay_image.height)//2)) + return im diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py new file mode 100644 index 00000000..51742257 --- /dev/null +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -0,0 +1,234 @@ +import asyncio +import json +import logging +import typing as t +from concurrent.futures import ThreadPoolExecutor + +import aiofiles +import discord +from aiohttp import client_exceptions +from discord.ext import commands + +from bot.constants import Colours +from bot.exts.evergreen.profile_pic_modification._effects import PfpEffects +from bot.utils.halloween import spookifications + +log = logging.getLogger(__name__) + +_EXECUTOR = ThreadPoolExecutor(10) + + +async def in_thread(func: t.Callable, *args) -> asyncio.Future: + """Allows non-async functions to work in async functions.""" + log.trace(f"Running {func.__name__} in an executor.") + loop = asyncio.get_event_loop() + return await loop.run_in_executor(_EXECUTOR, func, *args) + + +class PfpModify(commands.Cog): + """Various commands for users to change their own profile picture.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + self.bot.loop.create_task(self.init_cog()) + + async def init_cog(self) -> None: + """Initial load from resources asynchronously.""" + async with aiofiles.open('bot/resources/pride/gender_options.json') as f: + self.GENDER_OPTIONS = json.loads(await f.read()) + + @commands.group() + async def pfp_modify(self, ctx: commands.Context) -> None: + """Groups all of the pfp modifing commands to allow a single concurrency limit.""" + if not ctx.invoked_subcommand: + await ctx.send_help(ctx.command) + + @pfp_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(): + image_bytes = await ctx.author.avatar_url.read() + file = await in_thread( + PfpEffects._apply_effect, + image_bytes, + PfpEffects.eight_bitify_effect + ) + + 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="attachment://modified_avatar.png") + embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + + await ctx.send(file=file, embed=embed) + + @pfp_modify.command(pass_context=True, aliases=["easterify"], root_aliases=("easterify", "avatareasterify")) + async def avatareasterify(self, ctx: commands.Context, *colours: t.Union[discord.Colour, str]) -> None: + """ + This "Easterifies" 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. + """ + async def send(*args, **kwargs) -> str: + """ + This replaces the original ctx.send. + + When invoking the egg decorating command, the egg itself doesn't print to to the channel. + Returns the message content so that if any errors occur, the error message can be output. + """ + if args: + return args[0] + + async with ctx.typing(): + 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 + + image_bytes = await ctx.author.avatar_url_as(size=256).read() + file = await in_thread( + PfpEffects._apply_effect, + image_bytes, + PfpEffects.easterify_effect, + egg + ) + + embed = discord.Embed( + name="Your Lovely Easterified Avatar", + description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D" + ) + embed.set_image(url="attachment://modified_avatar.png") + embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + + await ctx.send(file=file, embed=embed) + + async def send_pride_image( + self, + ctx: commands.Context, + image_bytes: bytes, + pixels: int, + flag: str, + option: str + ) -> None: + """Gets and sends the image in an embed. Used by the pride commands.""" + async with ctx.typing(): + file = await in_thread( + PfpEffects._apply_effect, + image_bytes, + PfpEffects.pridify_effect, + pixels, + flag + ) + + embed = discord.Embed( + name="Your Lovely Pride Avatar", + description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D" + ) + embed.set_image(url="attachment://modified_avatar.png") + embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + await ctx.send(file=file, embed=embed) + + @pfp_modify.group( + aliases=["avatarpride", "pridepfp", "prideprofile"], + root_aliases=("prideavatar", "avatarpride", "pridepfp", "prideprofile"), + invoke_without_command=True + ) + async def prideavatar(self, ctx: commands.Context, option: str = "lgbt", pixels: int = 64) -> None: + """ + This surrounds an avatar with a border of a specified LGBT flag. + + This defaults to the LGBT rainbow flag if none is given. + The amount of pixels can be given which determines the thickness of the flag border. + This has a maximum of 512px and defaults to a 64px border. + The full image is 1024x1024. + """ + option = option.lower() + pixels = max(0, min(512, pixels)) + flag = self.GENDER_OPTIONS.get(option) + if flag is None: + return await ctx.send("I don't have that flag!") + + async with ctx.typing(): + image_bytes = await ctx.author.avatar_url.read() + await self.send_pride_image(ctx, image_bytes, pixels, flag, option) + + @prideavatar.command() + async def image(self, ctx: commands.Context, url: str, option: str = "lgbt", pixels: int = 64) -> None: + """ + This surrounds the image specified by the URL with a border of a specified LGBT flag. + + This defaults to the LGBT rainbow flag if none is given. + The amount of pixels can be given which determines the thickness of the flag border. + This has a maximum of 512px and defaults to a 64px border. + The full image is 1024x1024. + """ + option = option.lower() + pixels = max(0, min(512, pixels)) + flag = self.GENDER_OPTIONS.get(option) + if flag is None: + return await ctx.send("I don't have that flag!") + + async with ctx.typing(): + async with self.bot.http_session as session: + try: + response = await session.get(url) + except client_exceptions.ClientConnectorError: + return await ctx.send("Cannot connect to provided URL!") + except client_exceptions.InvalidURL: + return await ctx.send("Invalid URL!") + if response.status != 200: + return await ctx.send("Bad response from provided URL!") + image_bytes = await response.read() + await self.send_pride_image(ctx, image_bytes, pixels, flag, option) + + @prideavatar.command() + async def flags(self, ctx: commands.Context) -> None: + """This lists the flags that can be used with the prideavatar command.""" + choices = sorted(set(self.GENDER_OPTIONS.values())) + options = "• " + "\n• ".join(choices) + embed = discord.Embed( + title="I have the following flags:", + description=options, + colour=Colours.soft_red + ) + await ctx.send(embed=embed) + + @pfp_modify.command( + name='spookyavatar', + aliases=('savatar', 'spookify'), + root_aliases=('spookyavatar', 'spookify', 'savatar'), + brief='Spookify an user\'s avatar.' + ) + async def spooky_avatar(self, ctx: commands.Context, user: discord.Member = None) -> None: + """A command to print the user's spookified avatar.""" + if user is None: + user = ctx.message.author + + async with ctx.typing(): + image_bytes = await ctx.author.avatar_url.read() + file = await in_thread(PfpEffects._apply_effect, image_bytes, spookifications.get_random_effect) + + embed = discord.Embed( + title="Is this you or am I just really paranoid?", + colour=0xFF0000 + ) + embed.set_author(name=str(user.name), icon_url=user.avatar_url) + embed.set_image(url='attachment://modified_avatar.png') + embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + + await ctx.send(file=file, embed=embed) + + +def setup(bot: commands.Bot) -> None: + """Load the PfpModify cog.""" + bot.add_cog(PfpModify(bot)) -- cgit v1.2.3 From e210354fc5d8e0970e1ea2e75bb28fa314fa72fa Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 20 Feb 2021 15:50:03 +0000 Subject: Hoist apply effect and remove private _. --- .../evergreen/profile_pic_modification/_effects.py | 25 +++++++++++----------- .../profile_pic_modification/pfp_modify.py | 8 +++---- 2 files changed, 17 insertions(+), 16 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py index fbe5f706..a290ed3d 100644 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -15,6 +15,19 @@ EASTER_COLOURS = [ class PfpEffects(): """Implements various image effects.""" + @staticmethod + def apply_effect(image_bytes: bytes, effect: t.Callable, *args) -> discord.File: + """Applies the given effect to the image passed to it.""" + im = Image.open(BytesIO(image_bytes)) + im = im.convert("RGBA") + im = effect(im, *args) + + bufferedio = BytesIO() + im.save(bufferedio, format="PNG") + bufferedio.seek(0) + + return discord.File(bufferedio, filename="modified_avatar.png") + @staticmethod def closest(x: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: """ @@ -56,18 +69,6 @@ class PfpEffects(): ring.putalpha(mask) return ring - @staticmethod - def _apply_effect(image_bytes: bytes, effect: t.Callable, *args) -> discord.File: - im = Image.open(BytesIO(image_bytes)) - im = im.convert("RGBA") - im = effect(im, *args) - - bufferedio = BytesIO() - im.save(bufferedio, format="PNG") - bufferedio.seek(0) - - return discord.File(bufferedio, filename="modified_avatar.png") - @staticmethod def pridify_effect( image: Image, diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 51742257..a58f44d2 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -49,7 +49,7 @@ class PfpModify(commands.Cog): async with ctx.typing(): image_bytes = await ctx.author.avatar_url.read() file = await in_thread( - PfpEffects._apply_effect, + PfpEffects.apply_effect, image_bytes, PfpEffects.eight_bitify_effect ) @@ -97,7 +97,7 @@ class PfpModify(commands.Cog): image_bytes = await ctx.author.avatar_url_as(size=256).read() file = await in_thread( - PfpEffects._apply_effect, + PfpEffects.apply_effect, image_bytes, PfpEffects.easterify_effect, egg @@ -123,7 +123,7 @@ class PfpModify(commands.Cog): """Gets and sends the image in an embed. Used by the pride commands.""" async with ctx.typing(): file = await in_thread( - PfpEffects._apply_effect, + PfpEffects.apply_effect, image_bytes, PfpEffects.pridify_effect, pixels, @@ -216,7 +216,7 @@ class PfpModify(commands.Cog): async with ctx.typing(): image_bytes = await ctx.author.avatar_url.read() - file = await in_thread(PfpEffects._apply_effect, image_bytes, spookifications.get_random_effect) + file = await in_thread(PfpEffects.apply_effect, image_bytes, spookifications.get_random_effect) embed = discord.Embed( title="Is this you or am I just really paranoid?", -- cgit v1.2.3 From a30f4448bfe411cf418e4de938870d89680d68df Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 20 Feb 2021 15:53:42 +0000 Subject: Don't return discord.Message from ctx.send calls --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index a58f44d2..f3e8d426 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -156,7 +156,8 @@ class PfpModify(commands.Cog): pixels = max(0, min(512, pixels)) flag = self.GENDER_OPTIONS.get(option) if flag is None: - return await ctx.send("I don't have that flag!") + await ctx.send("I don't have that flag!") + return async with ctx.typing(): image_bytes = await ctx.author.avatar_url.read() @@ -176,7 +177,8 @@ class PfpModify(commands.Cog): pixels = max(0, min(512, pixels)) flag = self.GENDER_OPTIONS.get(option) if flag is None: - return await ctx.send("I don't have that flag!") + await ctx.send("I don't have that flag!") + return async with ctx.typing(): async with self.bot.http_session as session: -- cgit v1.2.3 From 6e7521c74e9c34159d8ba4873f09f5277f47908e Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 22 Feb 2021 18:35:10 +0000 Subject: Improve doc string. --- bot/exts/evergreen/profile_pic_modification/_effects.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py index a290ed3d..1179100c 100644 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -13,7 +13,11 @@ EASTER_COLOURS = [ class PfpEffects(): - """Implements various image effects.""" + """ + Implements various image effects. + + All of these methods are blocking, so should be ran in threads. + """ @staticmethod def apply_effect(image_bytes: bytes, effect: t.Callable, *args) -> discord.File: -- cgit v1.2.3 From 64576f56a69ec26406d7eaa11956d93db642fc3a Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 5 Mar 2021 22:05:56 +0000 Subject: Rename in_thread fuction for clarity. --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index f3e8d426..df5005c9 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -18,7 +18,7 @@ log = logging.getLogger(__name__) _EXECUTOR = ThreadPoolExecutor(10) -async def in_thread(func: t.Callable, *args) -> asyncio.Future: +async def in_executor(func: t.Callable, *args) -> asyncio.Future: """Allows non-async functions to work in async functions.""" log.trace(f"Running {func.__name__} in an executor.") loop = asyncio.get_event_loop() @@ -48,7 +48,7 @@ class PfpModify(commands.Cog): """Pixelates your avatar and changes the palette to an 8bit one.""" async with ctx.typing(): image_bytes = await ctx.author.avatar_url.read() - file = await in_thread( + file = await in_executor( PfpEffects.apply_effect, image_bytes, PfpEffects.eight_bitify_effect @@ -96,7 +96,7 @@ class PfpModify(commands.Cog): ctx.send = send_message # Reassigns ctx.send image_bytes = await ctx.author.avatar_url_as(size=256).read() - file = await in_thread( + file = await in_executor( PfpEffects.apply_effect, image_bytes, PfpEffects.easterify_effect, @@ -122,7 +122,7 @@ class PfpModify(commands.Cog): ) -> None: """Gets and sends the image in an embed. Used by the pride commands.""" async with ctx.typing(): - file = await in_thread( + file = await in_executor( PfpEffects.apply_effect, image_bytes, PfpEffects.pridify_effect, @@ -218,7 +218,7 @@ class PfpModify(commands.Cog): async with ctx.typing(): image_bytes = await ctx.author.avatar_url.read() - file = await in_thread(PfpEffects.apply_effect, image_bytes, spookifications.get_random_effect) + file = await in_executor(PfpEffects.apply_effect, image_bytes, spookifications.get_random_effect) embed = discord.Embed( title="Is this you or am I just really paranoid?", -- cgit v1.2.3 From 013ce6da618ee9fdb7ec99d0c3a538f0aa8c745d Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 6 Mar 2021 12:12:22 +0000 Subject: Improve readibility of code, and fix grammar issues. --- .../evergreen/profile_pic_modification/_effects.py | 24 +++++++++++++--------- .../profile_pic_modification/pfp_modify.py | 18 ++++++++-------- 2 files changed, 23 insertions(+), 19 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py index 1179100c..99010931 100644 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -12,7 +12,7 @@ EASTER_COLOURS = [ ] # Pastel colours - Easter-like -class PfpEffects(): +class PfpEffects: """ Implements various image effects. @@ -37,14 +37,14 @@ class PfpEffects(): """ Finds the closest easter colour to a given pixel. - Returns a merge between the original colour and the closest colour + Returns a merge between the original colour and the closest colour. """ r1, g1, b1 = x def distance(point: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: """Finds the difference between a pastel colour and the original pixel colour.""" r2, g2, b2 = point - return ((r1 - r2)**2 + (g1 - g2)**2 + (b1 - b2)**2) + return (r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2 closest_colours = sorted(EASTER_COLOURS, key=lambda point: distance(point)) r2, g2, b2 = closest_colours[0] @@ -52,7 +52,7 @@ class PfpEffects(): g = (g1 + g2) // 2 b = (b1 + b2) // 2 - return (r, g, b) + return r, g, b @staticmethod def crop_avatar_circle(avatar: Image) -> Image: @@ -114,14 +114,18 @@ class PfpEffects(): image = ImageOps.posterize(image, 6) data = image.getdata() - setted_data = set(data) - new_d = {} + data_set = set(data) + easterified_data_set = {} - for x in setted_data: - new_d[x] = PfpEffects.closest(x) - new_data = [(*new_d[x], alpha[i]) if x in new_d else x for i, x in enumerate(data)] + for x in data_set: + easterified_data_set[x] = PfpEffects.closest(x) + new_pixel_data = [ + (*easterified_data_set[x], alpha[i]) + if x in easterified_data_set else x + for i, x in enumerate(data) + ] im = Image.new("RGBA", image.size) - im.putdata(new_data) + im.putdata(new_pixel_data) im.alpha_composite(overlay_image, (im.width - overlay_image.width, (im.height - overlay_image.height)//2)) return im diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index df5005c9..1d5b7208 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -39,7 +39,7 @@ class PfpModify(commands.Cog): @commands.group() async def pfp_modify(self, ctx: commands.Context) -> None: - """Groups all of the pfp modifing commands to allow a single concurrency limit.""" + """Groups all of the pfp modifying commands to allow a single concurrency limit.""" if not ctx.invoked_subcommand: await ctx.send_help(ctx.command) @@ -56,11 +56,11 @@ class PfpModify(commands.Cog): embed = discord.Embed( title="Your 8-bit avatar", - description='Here is your avatar. I think it looks all cool and "retro"' + description="Here is your avatar. I think it looks all cool and 'retro'." ) embed.set_image(url="attachment://modified_avatar.png") - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) await ctx.send(file=file, embed=embed) @@ -104,11 +104,11 @@ class PfpModify(commands.Cog): ) embed = discord.Embed( - name="Your Lovely Easterified Avatar", + name="Your Lovely Easterified Avatar!", description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D" ) embed.set_image(url="attachment://modified_avatar.png") - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) await ctx.send(file=file, embed=embed) @@ -131,11 +131,11 @@ class PfpModify(commands.Cog): ) embed = discord.Embed( - name="Your Lovely Pride Avatar", + name="Your Lovely Pride Avatar!", description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D" ) embed.set_image(url="attachment://modified_avatar.png") - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) await ctx.send(file=file, embed=embed) @pfp_modify.group( @@ -222,11 +222,11 @@ class PfpModify(commands.Cog): embed = discord.Embed( title="Is this you or am I just really paranoid?", - colour=0xFF0000 + colour=Colours.soft_red ) embed.set_author(name=str(user.name), icon_url=user.avatar_url) embed.set_image(url='attachment://modified_avatar.png') - embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url) + embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) await ctx.send(file=file, embed=embed) -- cgit v1.2.3 From 67db0d13d35cda2fcaa7d3bb20e22b38bb264487 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 6 Mar 2021 12:13:22 +0000 Subject: Convert method to static as it doesn't use class attributes. --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 1d5b7208..45a41e67 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -112,8 +112,8 @@ class PfpModify(commands.Cog): await ctx.send(file=file, embed=embed) + @staticmethod async def send_pride_image( - self, ctx: commands.Context, image_bytes: bytes, pixels: int, -- cgit v1.2.3 From 50a344d5caa006df1cbb033da4101b6cfa71529c Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 6 Mar 2021 12:14:23 +0000 Subject: Fix return type of in_executor. Its return type varies, based on the Callable given. --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 45a41e67..bba688aa 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -18,7 +18,7 @@ log = logging.getLogger(__name__) _EXECUTOR = ThreadPoolExecutor(10) -async def in_executor(func: t.Callable, *args) -> asyncio.Future: +async def in_executor(func: t.Callable, *args) -> t.Any: """Allows non-async functions to work in async functions.""" log.trace(f"Running {func.__name__} in an executor.") loop = asyncio.get_event_loop() -- cgit v1.2.3 From ea44e8ed8f5804cf5d80ab2318db6359c1e22c17 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 6 Mar 2021 12:15:28 +0000 Subject: Don't return discord.Messages. --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index bba688aa..7d4326a4 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -185,11 +185,16 @@ class PfpModify(commands.Cog): try: response = await session.get(url) except client_exceptions.ClientConnectorError: - return await ctx.send("Cannot connect to provided URL!") + await ctx.send("Cannot connect to provided URL!") + return except client_exceptions.InvalidURL: - return await ctx.send("Invalid URL!") + await ctx.send("Invalid URL!") + return + if response.status != 200: - return await ctx.send("Bad response from provided URL!") + await ctx.send("Bad response from provided URL!") + return + image_bytes = await response.read() await self.send_pride_image(ctx, image_bytes, pixels, flag, option) -- cgit v1.2.3 From 49ce88636124b826437c17a3c39c60c0840c1256 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 6 Mar 2021 12:16:00 +0000 Subject: Remove superfluous str cast. --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 7d4326a4..11222563 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -229,7 +229,7 @@ class PfpModify(commands.Cog): title="Is this you or am I just really paranoid?", colour=Colours.soft_red ) - embed.set_author(name=str(user.name), icon_url=user.avatar_url) + embed.set_author(name=user.name, icon_url=user.avatar_url) embed.set_image(url='attachment://modified_avatar.png') embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) -- cgit v1.2.3 From 365a272ce235706006a36acd0eac09637205a1ae Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 6 Mar 2021 12:20:25 +0000 Subject: Split up complex line. --- bot/exts/evergreen/profile_pic_modification/_effects.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py index 99010931..dda58006 100644 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -127,5 +127,8 @@ class PfpEffects: im = Image.new("RGBA", image.size) im.putdata(new_pixel_data) - im.alpha_composite(overlay_image, (im.width - overlay_image.width, (im.height - overlay_image.height)//2)) + im.alpha_composite( + overlay_image, + (im.width - overlay_image.width, (im.height - overlay_image.height) // 2) + ) return im -- cgit v1.2.3 From 2ff509601fb97466ba6d980d226f6c0e481a1961 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 10 Mar 2021 19:31:37 +0000 Subject: Move colours to constants --- bot/constants.py | 16 ++++++++++++++++ bot/exts/evergreen/profile_pic_modification/_effects.py | 8 ++------ 2 files changed, 18 insertions(+), 6 deletions(-) (limited to 'bot') diff --git a/bot/constants.py b/bot/constants.py index b8e30a7c..5c95d9c1 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -162,6 +162,22 @@ class Colours: python_yellow = 0xFFD43B grass_green = 0x66ff00 + easter_like_colours = [ + (255, 247, 0), + (255, 255, 224), + (0, 255, 127), + (189, 252, 201), + (255, 192, 203), + (255, 160, 122), + (181, 115, 220), + (221, 160, 221), + (200, 162, 200), + (238, 130, 238), + (135, 206, 235), + (0, 204, 204), + (64, 224, 208), + ] + class Emojis: star = "\u2B50" diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py index dda58006..b0d50f4b 100644 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -5,11 +5,7 @@ from pathlib import Path import discord from PIL import Image, ImageDraw, ImageOps -EASTER_COLOURS = [ - (255, 247, 0), (255, 255, 224), (0, 255, 127), (189, 252, 201), (255, 192, 203), - (255, 160, 122), (181, 115, 220), (221, 160, 221), (200, 162, 200), (238, 130, 238), - (135, 206, 235), (0, 204, 204), (64, 224, 208) -] # Pastel colours - Easter-like +from bot.constants import Colours class PfpEffects: @@ -46,7 +42,7 @@ class PfpEffects: r2, g2, b2 = point return (r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2 - closest_colours = sorted(EASTER_COLOURS, key=lambda point: distance(point)) + closest_colours = sorted(Colours.easter_like_colours, key=lambda point: distance(point)) r2, g2, b2 = closest_colours[0] r = (r1 + r2) // 2 g = (g1 + g2) // 2 -- cgit v1.2.3 From 3d54a88ee1d7fd5d9ece91ecea998e8da107ff22 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 10 Mar 2021 19:33:30 +0000 Subject: Improve docstring and split line for readibility --- bot/exts/evergreen/profile_pic_modification/_effects.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py index b0d50f4b..3f4e796d 100644 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -88,8 +88,14 @@ class PfpEffects: @staticmethod def eight_bitify_effect(image: Image) -> Image: - """Applies the 8bit effect to the given image.""" - image = image.resize((32, 32), resample=Image.NEAREST).resize((1024, 1024), resample=Image.NEAREST) + """ + Applies the 8bit effect to the given image. + + This is done by reducing the image to 32x32 and then back up to 1024x1024. + We then quantize the image before returning too. + """ + image = image.resize((32, 32), resample=Image.NEAREST) + image = image.resize((1024, 1024), resample=Image.NEAREST) return image.quantize() @staticmethod -- cgit v1.2.3 From 33141d9fa9d3ffe2120fc4cf2fcba824d047fe7e Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 10 Mar 2021 19:34:00 +0000 Subject: Remove double assignment of a variable --- bot/exts/evergreen/profile_pic_modification/_effects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py index 3f4e796d..ef0a3f37 100644 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -109,7 +109,7 @@ class PfpEffects: )) overlay_image = overlay_image.convert("RGBA") else: - overlay_image = overlay_image = Image.open(Path("bot/resources/easter/chocolate_bunny.png")) + overlay_image = Image.open(Path("bot/resources/easter/chocolate_bunny.png")) alpha = image.getchannel("A").getdata() image = image.convert("RGB") -- cgit v1.2.3 From d90f735913b087104a24aa718b6473fa0b39384c Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 11 Mar 2021 17:55:20 +0000 Subject: Use a more specific filename for avatar modifiying commands --- .../evergreen/profile_pic_modification/_effects.py | 4 +- .../profile_pic_modification/pfp_modify.py | 44 ++++++++++++++++++---- 2 files changed, 39 insertions(+), 9 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py index ef0a3f37..ae606dcb 100644 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -16,7 +16,7 @@ class PfpEffects: """ @staticmethod - def apply_effect(image_bytes: bytes, effect: t.Callable, *args) -> discord.File: + def apply_effect(image_bytes: bytes, effect: t.Callable, filename: str, *args) -> discord.File: """Applies the given effect to the image passed to it.""" im = Image.open(BytesIO(image_bytes)) im = im.convert("RGBA") @@ -26,7 +26,7 @@ class PfpEffects: im.save(bufferedio, format="PNG") bufferedio.seek(0) - return discord.File(bufferedio, filename="modified_avatar.png") + return discord.File(bufferedio, filename=filename) @staticmethod def closest(x: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 11222563..299ed107 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -17,6 +17,8 @@ log = logging.getLogger(__name__) _EXECUTOR = ThreadPoolExecutor(10) +FILENAME_STRING = "{effect}_{author}.png" + async def in_executor(func: t.Callable, *args) -> t.Any: """Allows non-async functions to work in async functions.""" @@ -48,10 +50,16 @@ class PfpModify(commands.Cog): """Pixelates your avatar and changes the palette to an 8bit one.""" async with ctx.typing(): image_bytes = await ctx.author.avatar_url.read() + file_name = FILENAME_STRING.format( + effect="eightbit_avatar", + author=ctx.author.display_name + ) + file = await in_executor( PfpEffects.apply_effect, image_bytes, - PfpEffects.eight_bitify_effect + PfpEffects.eight_bitify_effect, + file_name ) embed = discord.Embed( @@ -59,10 +67,10 @@ class PfpModify(commands.Cog): description="Here is your avatar. I think it looks all cool and 'retro'." ) - embed.set_image(url="attachment://modified_avatar.png") + embed.set_image(url=f"attachment://{file_name}") embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) - await ctx.send(file=file, embed=embed) + await ctx.send(embed=embed, file=file) @pfp_modify.command(pass_context=True, aliases=["easterify"], root_aliases=("easterify", "avatareasterify")) async def avatareasterify(self, ctx: commands.Context, *colours: t.Union[discord.Colour, str]) -> None: @@ -96,10 +104,16 @@ class PfpModify(commands.Cog): ctx.send = send_message # Reassigns ctx.send image_bytes = await ctx.author.avatar_url_as(size=256).read() + file_name = FILENAME_STRING.format( + effect="easterified_avatar", + author=ctx.author.display_name + ) + file = await in_executor( PfpEffects.apply_effect, image_bytes, PfpEffects.easterify_effect, + file_name, egg ) @@ -107,7 +121,7 @@ class PfpModify(commands.Cog): name="Your Lovely Easterified Avatar!", description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D" ) - embed.set_image(url="attachment://modified_avatar.png") + embed.set_image(url=f"attachment://{file_name}") embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) await ctx.send(file=file, embed=embed) @@ -122,10 +136,16 @@ class PfpModify(commands.Cog): ) -> None: """Gets and sends the image in an embed. Used by the pride commands.""" async with ctx.typing(): + file_name = FILENAME_STRING.format( + effect="pride_avatar", + author=ctx.author.display_name + ) + file = await in_executor( PfpEffects.apply_effect, image_bytes, PfpEffects.pridify_effect, + file_name, pixels, flag ) @@ -134,7 +154,7 @@ class PfpModify(commands.Cog): name="Your Lovely Pride Avatar!", description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D" ) - embed.set_image(url="attachment://modified_avatar.png") + embed.set_image(url=f"attachment://{file_name}") embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) await ctx.send(file=file, embed=embed) @@ -223,14 +243,24 @@ class PfpModify(commands.Cog): async with ctx.typing(): image_bytes = await ctx.author.avatar_url.read() - file = await in_executor(PfpEffects.apply_effect, image_bytes, spookifications.get_random_effect) + + file_name = FILENAME_STRING.format( + effect="pride_avatar", + author=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_author(name=user.name, icon_url=user.avatar_url) - embed.set_image(url='attachment://modified_avatar.png') + embed.set_image(url=f"attachment://{file_name}") embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) await ctx.send(file=file, embed=embed) -- cgit v1.2.3 From 856cffbbfa2411fa246a2ff988b1199e0dcb4030 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 11 Mar 2021 17:59:39 +0000 Subject: Defer errors in pride image to error handler --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 299ed107..68750fe2 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -8,6 +8,7 @@ import aiofiles import discord from aiohttp import client_exceptions from discord.ext import commands +from discord.ext.commands.errors import BadArgument from bot.constants import Colours from bot.exts.evergreen.profile_pic_modification._effects import PfpEffects @@ -205,11 +206,9 @@ class PfpModify(commands.Cog): try: response = await session.get(url) except client_exceptions.ClientConnectorError: - await ctx.send("Cannot connect to provided URL!") - return + raise BadArgument("Cannot connect to provided URL!") except client_exceptions.InvalidURL: - await ctx.send("Invalid URL!") - return + raise BadArgument("Invalid URL!") if response.status != 200: await ctx.send("Bad response from provided URL!") -- cgit v1.2.3 From 6c1b715bae96980309af3b38a5ff6cad6ce49f95 Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Thu, 11 Mar 2021 18:01:22 +0000 Subject: Update docstring to refer to groups rather than commands Co-authored-by: Shivansh-007 <69356296+Shivansh-007@users.noreply.github.com> --- bot/group.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/group.py b/bot/group.py index 77092adf..a7bc59b7 100644 --- a/bot/group.py +++ b/bot/group.py @@ -6,7 +6,7 @@ class Group(commands.Group): A `discord.ext.commands.Group` subclass which supports root aliases. A `root_aliases` keyword argument is added, which is a sequence of alias names that will act as - top-level commands rather than being aliases of the command's group. It's stored as an attribute + top-level groups rather than being aliases of the command's group. It's stored as an attribute also named `root_aliases`. """ @@ -15,4 +15,4 @@ class Group(commands.Group): self.root_aliases = kwargs.get("root_aliases", []) if not isinstance(self.root_aliases, (list, tuple)): - raise TypeError("Root aliases of a command must be a list or a tuple of strings.") + raise TypeError("Root aliases of a group must be a list or a tuple of strings.") -- cgit v1.2.3 From 335702f65429c6f1ebc9a9a50ffc528bcfd38581 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 11 Mar 2021 18:06:22 +0000 Subject: Fix filename for spooky effect avatars --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 68750fe2..9b874900 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -244,7 +244,7 @@ class PfpModify(commands.Cog): image_bytes = await ctx.author.avatar_url.read() file_name = FILENAME_STRING.format( - effect="pride_avatar", + effect="spooky_avatar", author=ctx.author.display_name ) file = await in_executor( -- cgit v1.2.3 From 88e7ab58ff6ad74a8323c828cc5411886bb57dbe Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Sat, 13 Mar 2021 14:57:54 +0000 Subject: Use user reference passed via command The command takes in user as argument but doesn't use it while converting the image into bytes. Co-authored-by: Shivansh-007 <69356296+Shivansh-007@users.noreply.github.com> --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 9b874900..aa0f0404 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -241,11 +241,11 @@ class PfpModify(commands.Cog): user = ctx.message.author async with ctx.typing(): - image_bytes = await ctx.author.avatar_url.read() + image_bytes = await user.avatar_url.read() file_name = FILENAME_STRING.format( effect="spooky_avatar", - author=ctx.author.display_name + author=user.display_name ) file = await in_executor( PfpEffects.apply_effect, -- cgit v1.2.3 From d8b30f2a1a99f1c301c4d1e675dc708b90d0e652 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 13 Mar 2021 15:54:15 +0000 Subject: Use new help command ext Since sir-lancebot#625 was merged, we now have bot.utils.extensions.invoke_help_command which sends a nicely formatted help embed. --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index aa0f0404..b13ba574 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -12,6 +12,7 @@ from discord.ext.commands.errors import BadArgument from bot.constants import Colours from bot.exts.evergreen.profile_pic_modification._effects import PfpEffects +from bot.utils.extensions import invoke_help_command from bot.utils.halloween import spookifications log = logging.getLogger(__name__) @@ -44,7 +45,7 @@ class PfpModify(commands.Cog): async def pfp_modify(self, ctx: commands.Context) -> None: """Groups all of the pfp modifying commands to allow a single concurrency limit.""" if not ctx.invoked_subcommand: - await ctx.send_help(ctx.command) + await invoke_help_command(ctx) @pfp_modify.command(name="8bitify", root_aliases=("8bitify",)) async def eightbit_command(self, ctx: commands.Context) -> None: -- cgit v1.2.3 From 8c09ba1db566a6c1051479e353ad025392e7e5bb Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 14 Mar 2021 13:34:27 +0000 Subject: Remove aiofiles dep --- Pipfile | 1 - Pipfile.lock | 28 ++++++++-------------- .../profile_pic_modification/pfp_modify.py | 16 +++++-------- 3 files changed, 16 insertions(+), 29 deletions(-) (limited to 'bot') diff --git a/Pipfile b/Pipfile index 79a5dfc8..e7e01a31 100644 --- a/Pipfile +++ b/Pipfile @@ -15,7 +15,6 @@ PyYAML = "~=5.3.1" "discord.py" = {extras = ["voice"], version = "~=1.5.1"} async-rediscache = {extras = ["fakeredis"], version = "~=0.1.4"} emojis = "~=0.6.0" -aiofiles = "~=0.6" [dev-packages] flake8 = "~=3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 9d0a988e..19bcb719 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "91281e9ed353fea748de3da19abd7bef402402b23fc78a1260dc8bf8bd2bd98c" + "sha256": "b4aaaacbab13179145e36d7b86c736db512286f6cce8e513cc30c48d68fe3810" }, "pipfile-spec": 6, "requires": { @@ -24,14 +24,6 @@ "index": "pypi", "version": "==2.0.0" }, - "aiofiles": { - "hashes": [ - "sha256:bd3019af67f83b739f8e4053c6c0512a7f545b9a8d91aaeab55e6e0f9d123c27", - "sha256:e0281b157d3d5d59d803e3f4557dcc9a3dff28a4dd4829a9ff478adae50ca092" - ], - "index": "pypi", - "version": "==0.6.0" - }, "aiohttp": { "hashes": [ "sha256:1a4160579ffbc1b69e88cb6ca8bb0fbd4947dfcbf9fb1e2a4fc4c7a4a986c1fe", @@ -531,11 +523,11 @@ }, "flake8-annotations": { "hashes": [ - "sha256:3a377140556aecf11fa9f3bb18c10db01f5ea56dc79a730e2ec9b4f1f49e2055", - "sha256:e17947a48a5b9f632fe0c72682fc797c385e451048e7dfb20139f448a074cb3e" + "sha256:8968ff12f296433028ad561c680ccc03a7cd62576d100c3f1475e058b3c11b43", + "sha256:bd0505616c0d85ebb45c6052d339c69f320d3f87fa079ab4e91a4f234a863d05" ], "index": "pypi", - "version": "==2.5.0" + "version": "==2.6.0" }, "flake8-bugbear": { "hashes": [ @@ -593,11 +585,11 @@ }, "identify": { "hashes": [ - "sha256:2179e7359471ab55729f201b3fdf7dc2778e221f868410fedcb0987b791ba552", - "sha256:2a5fdf2f5319cc357eda2550bea713a404392495961022cf2462624ce62f0f46" + "sha256:e3b7fd755b7ceee44fe22957005a92c2a085c863c2e65a6efdec35d0e06666db", + "sha256:fab0d3a3ab0d7d5f513985b0335ccccad9d61420c5216fb779237bf7edc3e5d1" ], "markers": "python_full_version >= '3.6.1'", - "version": "==2.1.0" + "version": "==2.1.2" }, "mccabe": { "hashes": [ @@ -623,11 +615,11 @@ }, "pre-commit": { "hashes": [ - "sha256:16212d1fde2bed88159287da88ff03796863854b04dc9f838a55979325a3d20e", - "sha256:399baf78f13f4de82a29b649afd74bef2c4e28eb4f021661fc7f29246e8c7a3a" + "sha256:94c82f1bf5899d56edb1d926732f4e75a7df29a0c8c092559c77420c9d62428b", + "sha256:de55c5c72ce80d79106e48beb1b54104d16495ce7f95b0c7b13d4784193a00af" ], "index": "pypi", - "version": "==2.10.1" + "version": "==2.11.1" }, "pycodestyle": { "hashes": [ diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index b13ba574..d41fcaad 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -4,7 +4,6 @@ import logging import typing as t from concurrent.futures import ThreadPoolExecutor -import aiofiles import discord from aiohttp import client_exceptions from discord.ext import commands @@ -21,6 +20,9 @@ _EXECUTOR = ThreadPoolExecutor(10) FILENAME_STRING = "{effect}_{author}.png" +with open('bot/resources/pride/gender_options.json') as f: + GENDER_OPTIONS = json.loads(f.read()) + async def in_executor(func: t.Callable, *args) -> t.Any: """Allows non-async functions to work in async functions.""" @@ -34,12 +36,6 @@ class PfpModify(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - self.bot.loop.create_task(self.init_cog()) - - async def init_cog(self) -> None: - """Initial load from resources asynchronously.""" - async with aiofiles.open('bot/resources/pride/gender_options.json') as f: - self.GENDER_OPTIONS = json.loads(await f.read()) @commands.group() async def pfp_modify(self, ctx: commands.Context) -> None: @@ -176,7 +172,7 @@ class PfpModify(commands.Cog): """ option = option.lower() pixels = max(0, min(512, pixels)) - flag = self.GENDER_OPTIONS.get(option) + flag = GENDER_OPTIONS.get(option) if flag is None: await ctx.send("I don't have that flag!") return @@ -197,7 +193,7 @@ class PfpModify(commands.Cog): """ option = option.lower() pixels = max(0, min(512, pixels)) - flag = self.GENDER_OPTIONS.get(option) + flag = GENDER_OPTIONS.get(option) if flag is None: await ctx.send("I don't have that flag!") return @@ -221,7 +217,7 @@ class PfpModify(commands.Cog): @prideavatar.command() async def flags(self, ctx: commands.Context) -> None: """This lists the flags that can be used with the prideavatar command.""" - choices = sorted(set(self.GENDER_OPTIONS.values())) + choices = sorted(set(GENDER_OPTIONS.values())) options = "• " + "\n• ".join(choices) embed = discord.Embed( title="I have the following flags:", -- cgit v1.2.3 From 4dbd106f3febeadcde162bcafa163ae4d07f1b12 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 14 Mar 2021 13:38:47 +0000 Subject: Remove unnecessary lambda in when getting closest colour --- bot/exts/evergreen/profile_pic_modification/_effects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py index ae606dcb..2ff92000 100644 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -42,7 +42,7 @@ class PfpEffects: r2, g2, b2 = point return (r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2 - closest_colours = sorted(Colours.easter_like_colours, key=lambda point: distance(point)) + closest_colours = sorted(Colours.easter_like_colours, key=distance) r2, g2, b2 = closest_colours[0] r = (r1 + r2) // 2 g = (g1 + g2) // 2 -- cgit v1.2.3 From 3eb23d5307bbf4abc03522e3ba9903e42131bb27 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 14 Mar 2021 13:39:26 +0000 Subject: Pull the function signature back onto one line for readibility --- bot/exts/evergreen/profile_pic_modification/_effects.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py index 2ff92000..9319a1b8 100644 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -70,11 +70,7 @@ class PfpEffects: return ring @staticmethod - def pridify_effect( - image: Image, - pixels: int, - flag: str - ) -> Image: + def pridify_effect(image: Image, pixels: int, flag: str) -> Image: """Applies the pride effect to the given image.""" image = image.resize((1024, 1024)) image = PfpEffects.crop_avatar_circle(image) -- cgit v1.2.3 From 63ec14c47ef34fe48a32ecf6b51718c277714b88 Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 14 Mar 2021 13:50:37 +0000 Subject: Imrpoves docstrings within the profile pic modification functions --- bot/exts/evergreen/profile_pic_modification/_effects.py | 17 ++++++++++++----- .../evergreen/profile_pic_modification/pfp_modify.py | 9 +++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py index 9319a1b8..a1069db7 100644 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -10,9 +10,9 @@ from bot.constants import Colours class PfpEffects: """ - Implements various image effects. + Implements various image modifying effects, for the PfpModify cog. - All of these methods are blocking, so should be ran in threads. + All of these fuctions are slow, and blocking, so should be ran in executors. """ @staticmethod @@ -31,7 +31,7 @@ class PfpEffects: @staticmethod def closest(x: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: """ - Finds the closest easter colour to a given pixel. + Finds the closest "easter" colour to a given pixel. Returns a merge between the original colour and the closest colour. """ @@ -71,7 +71,7 @@ class PfpEffects: @staticmethod def pridify_effect(image: Image, pixels: int, flag: str) -> Image: - """Applies the pride effect to the given image.""" + """Applies the given pride effect to the given image.""" image = image.resize((1024, 1024)) image = PfpEffects.crop_avatar_circle(image) @@ -96,7 +96,14 @@ class PfpEffects: @staticmethod def easterify_effect(image: Image, overlay_image: Image = None) -> Image: - """Applies the easter effect to the given image.""" + """ + Applies the easter effect to the given image. + + This is done by getting the closest "easter" colour to each pixel and changing the colour + to the half-way RGBvalue. + + We also then add an overlay image on top in middle right, a chocolate bunny by default. + """ if overlay_image: ratio = 64 / overlay_image.height overlay_image = overlay_image.resize(( diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index d41fcaad..19f0bdf0 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -25,7 +25,12 @@ with open('bot/resources/pride/gender_options.json') as f: async def in_executor(func: t.Callable, *args) -> t.Any: - """Allows non-async functions to work in async functions.""" + """ + Runs the given synchronus function `func` in an executor. + + This is useful for running slow, blocking code within async + functions, so that they don't block the bot. + """ log.trace(f"Running {func.__name__} in an executor.") loop = asyncio.get_event_loop() return await loop.run_in_executor(_EXECUTOR, func, *args) @@ -233,7 +238,7 @@ class PfpModify(commands.Cog): brief='Spookify an user\'s avatar.' ) async def spooky_avatar(self, ctx: commands.Context, user: discord.Member = None) -> None: - """A command to print the user's spookified avatar.""" + """This "spookifies" the given user's avatar, with a random *spooky* effect.""" if user is None: user = ctx.message.author -- cgit v1.2.3 From eac2b1d79275e3bdc08fc2a8de7a28913b8ebd5b Mon Sep 17 00:00:00 2001 From: Chris Date: Sun, 21 Mar 2021 13:52:56 +0000 Subject: Don't close bot's http session when getting image --- .../profile_pic_modification/pfp_modify.py | 24 ++++++++++------------ 1 file changed, 11 insertions(+), 13 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 19f0bdf0..9242ff0c 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -204,20 +204,18 @@ class PfpModify(commands.Cog): return async with ctx.typing(): - async with self.bot.http_session as session: - try: - response = await session.get(url) - except client_exceptions.ClientConnectorError: - raise BadArgument("Cannot connect to provided URL!") - except client_exceptions.InvalidURL: - raise BadArgument("Invalid URL!") - - if response.status != 200: - await ctx.send("Bad response from provided URL!") - return + try: + async with self.bot.http_session.get(url) as response: + if response.status != 200: + await ctx.send("Bad response from provided URL!") + return + image_bytes = await response.read() + except client_exceptions.ClientConnectorError: + raise BadArgument("Cannot connect to provided URL!") + except client_exceptions.InvalidURL: + raise BadArgument("Invalid URL!") - image_bytes = await response.read() - await self.send_pride_image(ctx, image_bytes, pixels, flag, option) + await self.send_pride_image(ctx, image_bytes, pixels, flag, option) @prideavatar.command() async def flags(self, ctx: commands.Context) -> None: -- cgit v1.2.3 From 2ad7b496c742d275d717b2de410caebdbeaa50d4 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 5 Apr 2021 17:16:52 +0100 Subject: Fetch member before modifying their pfp We do this as the member cache may have an outdated version of their pfp, which can lead to errors if it's removed from the Discord CDN. Co-authored-by: vcokltfre --- bot/constants.py | 1 + .../profile_pic_modification/pfp_modify.py | 65 +++++++++++++++++----- 2 files changed, 52 insertions(+), 14 deletions(-) (limited to 'bot') diff --git a/bot/constants.py b/bot/constants.py index 3ca2cda9..853ea340 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -165,6 +165,7 @@ class Colours: class Emojis: + cross_mark = "\u274C" star = "\u2B50" christmas_tree = "\U0001F384" check = "\u2611" diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 9242ff0c..c4b74d04 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -9,7 +9,7 @@ from aiohttp import client_exceptions from discord.ext import commands from discord.ext.commands.errors import BadArgument -from bot.constants import Colours +from bot.constants import Client, Colours, Emojis from bot.exts.evergreen.profile_pic_modification._effects import PfpEffects from bot.utils.extensions import invoke_help_command from bot.utils.halloween import spookifications @@ -42,6 +42,24 @@ class PfpModify(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot + async def _fetch_member(self, member_id: int) -> t.Optional[discord.Member]: + """ + Fetches a member and handles errors. + + This helper funciton is required as the member cache doesn't always have the most up to date + profile picture. THis can lead to errors if the image is delted from the Discord CDN. + """ + try: + member = await self.bot.get_guild(Client.guild).fetch_member(member_id) + except discord.errors.NotFound: + log.debug(f"Member {member_id} left the guild before we could get their pfp.") + return None + except discord.HTTPException: + log.exception(f"Exception while trying to retrieve member {member_id} from Discord.") + return None + + return member + @commands.group() async def pfp_modify(self, ctx: commands.Context) -> None: """Groups all of the pfp modifying commands to allow a single concurrency limit.""" @@ -52,10 +70,15 @@ class PfpModify(commands.Cog): async def eightbit_command(self, ctx: commands.Context) -> None: """Pixelates your avatar and changes the palette to an 8bit one.""" async with ctx.typing(): - image_bytes = await ctx.author.avatar_url.read() + member = await self._fetch_member(ctx.author.id) + if not member: + ctx.send(f"{Emojis.cross_mark} Could not get member info.") + return + + image_bytes = await member.avatar_url.read() file_name = FILENAME_STRING.format( effect="eightbit_avatar", - author=ctx.author.display_name + author=member.display_name ) file = await in_executor( @@ -71,7 +94,7 @@ class PfpModify(commands.Cog): ) embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) + embed.set_footer(text=f"Made by {member.display_name}.", icon_url=member.avatar_url) await ctx.send(embed=embed, file=file) @@ -96,6 +119,11 @@ class PfpModify(commands.Cog): return args[0] async with ctx.typing(): + member = await self._fetch_member(ctx.author.id) + if not member: + ctx.send(f"{Emojis.cross_mark} Could not get member info.") + return + egg = None if colours: send_message = ctx.send @@ -106,10 +134,10 @@ class PfpModify(commands.Cog): return ctx.send = send_message # Reassigns ctx.send - image_bytes = await ctx.author.avatar_url_as(size=256).read() + image_bytes = await member.avatar_url_as(size=256).read() file_name = FILENAME_STRING.format( effect="easterified_avatar", - author=ctx.author.display_name + author=member.display_name ) file = await in_executor( @@ -125,7 +153,7 @@ class PfpModify(commands.Cog): 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=ctx.author.avatar_url) + embed.set_footer(text=f"Made by {member.display_name}.", icon_url=member.avatar_url) await ctx.send(file=file, embed=embed) @@ -183,7 +211,11 @@ class PfpModify(commands.Cog): return async with ctx.typing(): - image_bytes = await ctx.author.avatar_url.read() + member = await self._fetch_member(ctx.author.id) + if not member: + ctx.send(f"{Emojis.cross_mark} Could not get member info.") + return + image_bytes = await member.avatar_url.read() await self.send_pride_image(ctx, image_bytes, pixels, flag, option) @prideavatar.command() @@ -235,17 +267,22 @@ class PfpModify(commands.Cog): root_aliases=('spookyavatar', 'spookify', 'savatar'), brief='Spookify an user\'s avatar.' ) - async def spooky_avatar(self, ctx: commands.Context, user: discord.Member = None) -> None: + async def spooky_avatar(self, ctx: commands.Context, member: discord.Member = None) -> None: """This "spookifies" the given user's avatar, with a random *spooky* effect.""" - if user is None: - user = ctx.message.author + if member is None: + member = ctx.author + + member = await self._fetch_member(member.id) + if not member: + ctx.send(f"{Emojis.cross_mark} Could not get member info.") + return async with ctx.typing(): - image_bytes = await user.avatar_url.read() + image_bytes = await member.avatar_url.read() file_name = FILENAME_STRING.format( effect="spooky_avatar", - author=user.display_name + author=member.display_name ) file = await in_executor( PfpEffects.apply_effect, @@ -258,7 +295,7 @@ class PfpModify(commands.Cog): title="Is this you or am I just really paranoid?", colour=Colours.soft_red ) - embed.set_author(name=user.name, icon_url=user.avatar_url) + embed.set_author(name=member.name, icon_url=member.avatar_url) embed.set_image(url=f"attachment://{file_name}") embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) -- cgit v1.2.3 From 422f6d50b8eeafdf2118f1d0e96c420c0b1391ce Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Thu, 8 Apr 2021 10:04:14 +0100 Subject: Refactor pfp cog to remove unnecessary params and calls Co-authored-by: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> --- bot/exts/evergreen/profile_pic_modification/_effects.py | 2 +- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py index a1069db7..e415d700 100644 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ b/bot/exts/evergreen/profile_pic_modification/_effects.py @@ -12,7 +12,7 @@ class PfpEffects: """ Implements various image modifying effects, for the PfpModify cog. - All of these fuctions are slow, and blocking, so should be ran in executors. + All of these fuctions are slow, and blocking, so they should be ran in executors. """ @staticmethod diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index c4b74d04..712631d1 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -21,7 +21,7 @@ _EXECUTOR = ThreadPoolExecutor(10) FILENAME_STRING = "{effect}_{author}.png" with open('bot/resources/pride/gender_options.json') as f: - GENDER_OPTIONS = json.loads(f.read()) + GENDER_OPTIONS = json.load(f) async def in_executor(func: t.Callable, *args) -> t.Any: @@ -98,7 +98,7 @@ class PfpModify(commands.Cog): await ctx.send(embed=embed, file=file) - @pfp_modify.command(pass_context=True, aliases=["easterify"], root_aliases=("easterify", "avatareasterify")) + @pfp_modify.command(aliases=["easterify"], root_aliases=("easterify", "avatareasterify")) async def avatareasterify(self, ctx: commands.Context, *colours: t.Union[discord.Colour, str]) -> None: """ This "Easterifies" the user's avatar. -- cgit v1.2.3 From c9e4916cb1cad8d43148030660575f5084710d38 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 8 Apr 2021 10:05:53 +0100 Subject: Ensure to await ctx.send() calls --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 712631d1..68d68927 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -72,7 +72,7 @@ class PfpModify(commands.Cog): async with ctx.typing(): member = await self._fetch_member(ctx.author.id) if not member: - ctx.send(f"{Emojis.cross_mark} Could not get member info.") + await ctx.send(f"{Emojis.cross_mark} Could not get member info.") return image_bytes = await member.avatar_url.read() @@ -121,7 +121,7 @@ class PfpModify(commands.Cog): async with ctx.typing(): member = await self._fetch_member(ctx.author.id) if not member: - ctx.send(f"{Emojis.cross_mark} Could not get member info.") + await ctx.send(f"{Emojis.cross_mark} Could not get member info.") return egg = None @@ -213,7 +213,7 @@ class PfpModify(commands.Cog): async with ctx.typing(): member = await self._fetch_member(ctx.author.id) if not member: - ctx.send(f"{Emojis.cross_mark} Could not get member info.") + await ctx.send(f"{Emojis.cross_mark} Could not get member info.") return image_bytes = await member.avatar_url.read() await self.send_pride_image(ctx, image_bytes, pixels, flag, option) @@ -274,7 +274,7 @@ class PfpModify(commands.Cog): member = await self._fetch_member(member.id) if not member: - ctx.send(f"{Emojis.cross_mark} Could not get member info.") + await ctx.send(f"{Emojis.cross_mark} Could not get member info.") return async with ctx.typing(): -- cgit v1.2.3 From 442391dfbafa3b3848a030301702031f01320fdd Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 12 Apr 2021 21:40:05 +0100 Subject: Request the pfp as the right size from discord --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 68d68927..51999fb0 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -215,7 +215,7 @@ class PfpModify(commands.Cog): if not member: await ctx.send(f"{Emojis.cross_mark} Could not get member info.") return - image_bytes = await member.avatar_url.read() + image_bytes = await member.avatar_url_as(size=1024).read() await self.send_pride_image(ctx, image_bytes, pixels, flag, option) @prideavatar.command() -- cgit v1.2.3 From b411a4522422879eee3e821f149f21636dfb56bf Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 12 Apr 2021 22:21:44 +0100 Subject: Silence matplotlib's logger --- bot/__init__.py | 1 + 1 file changed, 1 insertion(+) (limited to 'bot') diff --git a/bot/__init__.py b/bot/__init__.py index d0992912..71b7c8a3 100644 --- a/bot/__init__.py +++ b/bot/__init__.py @@ -60,6 +60,7 @@ if root.handlers: logging.getLogger("discord").setLevel(logging.ERROR) logging.getLogger("websockets").setLevel(logging.ERROR) logging.getLogger("PIL").setLevel(logging.ERROR) +logging.getLogger("matplotlib").setLevel(logging.ERROR) # Setup new logging configuration logging.basicConfig( -- cgit v1.2.3 From 2d3e1cd56eaf4902ca49f0a47a6c933484f0f1b5 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 17 Apr 2021 12:24:17 +0100 Subject: Fix captitilization in docstring Co-authored-by: LXNN --- bot/exts/evergreen/profile_pic_modification/pfp_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 51999fb0..77c937de 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -47,7 +47,7 @@ class PfpModify(commands.Cog): Fetches a member and handles errors. This helper funciton is required as the member cache doesn't always have the most up to date - profile picture. THis can lead to errors if the image is delted from the Discord CDN. + profile picture. This can lead to errors if the image is delted from the Discord CDN. """ try: member = await self.bot.get_guild(Client.guild).fetch_member(member_id) -- cgit v1.2.3 From 9524620ddbb7d3e707258e1ba3b84b23b5e3b54a Mon Sep 17 00:00:00 2001 From: Dillon Runke <44979306+Kronifer@users.noreply.github.com> Date: Tue, 20 Apr 2021 11:12:17 -0500 Subject: Add Catify command (#694) Co-authored-by: Joe Banks Co-authored-by: hypergamer80 <79152594+hypergamer80@users.noreply.github.com> --- bot/constants.py | 5 +++ bot/exts/evergreen/catify.py | 78 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 bot/exts/evergreen/catify.py (limited to 'bot') diff --git a/bot/constants.py b/bot/constants.py index a64882db..bcbdcba0 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -8,6 +8,7 @@ from typing import Dict, NamedTuple __all__ = ( "AdventOfCode", "Branding", + "Cats", "Channels", "Categories", "Client", @@ -93,6 +94,10 @@ class Branding: cycle_frequency = int(environ.get("CYCLE_FREQUENCY", 3)) # 0: never, 1: every day, 2: every other day, ... +class Cats: + cats = ["ᓚᘏᗢ", "ᘡᘏᗢ", "🐈", "ᓕᘏᗢ", "ᓇᘏᗢ", "ᓂᘏᗢ", "ᘣᘏᗢ", "ᕦᘏᗢ", "ᕂᘏᗢ"] + + class Channels(NamedTuple): advent_of_code = int(environ.get("AOC_CHANNEL_ID", 782715290437943306)) advent_of_code_commands = int(environ.get("AOC_COMMANDS_CHANNEL_ID", 607247579608121354)) diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py new file mode 100644 index 00000000..a0121403 --- /dev/null +++ b/bot/exts/evergreen/catify.py @@ -0,0 +1,78 @@ +import random +from typing import Optional + +from discord import AllowedMentions, Embed +from discord.ext import commands + +from bot.constants import Cats, Colours, NEGATIVE_REPLIES + + +class Catify(commands.Cog): + """Cog for the catify command.""" + + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.command(aliases=["ᓚᘏᗢify", "ᓚᘏᗢ"]) + async def catify(self, ctx: commands.Context, *, text: Optional[str]) -> None: + """ + Convert the provided text into a cat themed sentence by interspercing cats throughout text. + + If no text is given then the users nickname is edited. + """ + if not text: + display_name = ctx.author.display_name + + if len(display_name) > 26: + embed = Embed( + title=random.choice(NEGATIVE_REPLIES), + description="Your nickname is too long to be catified! Please change it to be under 26 characters.", + color=Colours.soft_red + ) + await ctx.send(embed=embed) + return + + else: + display_name += f" | {random.choice(Cats.cats)}" + await ctx.send(f"Your catified username is: `{display_name}`") + await ctx.author.edit(nick=display_name) + else: + if len(text) >= 1500: + embed = Embed( + title=random.choice(NEGATIVE_REPLIES), + description="Submitted text was too large! Please submit something under 1500 characters.", + color=Colours.soft_red + ) + await ctx.send(embed=embed) + return + + string_list = text.split() + for index, name in enumerate(string_list): + if "cat" in name: + if random.randint(0, 5) == 5: + string_list[index] = string_list[index].replace("cat", f"**{random.choice(Cats.cats)}**") + else: + string_list[index] = string_list[index].replace("cat", random.choice(Cats.cats)) + for element in Cats.cats: + if element in name: + string_list[index] = string_list[index].replace(element, "cat") + + string_len = len(string_list) // 3 or len(string_list) + + for _ in range(random.randint(1, string_len)): + # insert cat at random index + if random.randint(0, 5) == 5: + string_list.insert(random.randint(0, len(string_list)), f"**{random.choice(Cats.cats)}**") + else: + string_list.insert(random.randint(0, len(string_list)), random.choice(Cats.cats)) + + text = " ".join(string_list) + await ctx.send( + f">>> {text}", + allowed_mentions=AllowedMentions.none() + ) + + +def setup(bot: commands.Bot) -> None: + """Loads the catify cog.""" + bot.add_cog(Catify(bot)) -- cgit v1.2.3 From dc7923a78a4020a4020c42a392ff38bf3f5c35f3 Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Tue, 20 Apr 2021 12:30:27 -0400 Subject: chore: Fix UnboundLocalError and discord.ForbiddenErrors in the catify command --- bot/exts/evergreen/catify.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py index a0121403..c409ce6c 100644 --- a/bot/exts/evergreen/catify.py +++ b/bot/exts/evergreen/catify.py @@ -1,7 +1,8 @@ import random +from contextlib import suppress from typing import Optional -from discord import AllowedMentions, Embed +from discord import AllowedMentions, Embed, Forbidden from discord.ext import commands from bot.constants import Cats, Colours, NEGATIVE_REPLIES @@ -34,8 +35,11 @@ class Catify(commands.Cog): else: display_name += f" | {random.choice(Cats.cats)}" + await ctx.send(f"Your catified username is: `{display_name}`") - await ctx.author.edit(nick=display_name) + + with suppress(Forbidden): + await ctx.author.edit(nick=display_name) else: if len(text) >= 1500: embed = Embed( @@ -50,27 +54,27 @@ class Catify(commands.Cog): for index, name in enumerate(string_list): if "cat" in name: if random.randint(0, 5) == 5: - string_list[index] = string_list[index].replace("cat", f"**{random.choice(Cats.cats)}**") + string_list[index] = name.replace("cat", f"**{random.choice(Cats.cats)}**") else: - string_list[index] = string_list[index].replace("cat", random.choice(Cats.cats)) + string_list[index] = name.replace("cat", random.choice(Cats.cats)) for element in Cats.cats: if element in name: - string_list[index] = string_list[index].replace(element, "cat") + string_list[index] = name.replace(element, "cat") - string_len = len(string_list) // 3 or len(string_list) + string_len = len(string_list) // 3 or len(string_list) - for _ in range(random.randint(1, string_len)): - # insert cat at random index - if random.randint(0, 5) == 5: - string_list.insert(random.randint(0, len(string_list)), f"**{random.choice(Cats.cats)}**") - else: - string_list.insert(random.randint(0, len(string_list)), random.choice(Cats.cats)) + for _ in range(random.randint(1, string_len)): + # insert cat at random index + if random.randint(0, 5) == 5: + string_list.insert(random.randint(0, len(string_list)), f"**{random.choice(Cats.cats)}**") + else: + string_list.insert(random.randint(0, len(string_list)), random.choice(Cats.cats)) - text = " ".join(string_list) - await ctx.send( - f">>> {text}", - allowed_mentions=AllowedMentions.none() - ) + text = " ".join(string_list) + await ctx.send( + f">>> {text}", + allowed_mentions=AllowedMentions.none() + ) def setup(bot: commands.Bot) -> None: -- cgit v1.2.3 From 3477069a3a2e0b5e4030602afa4a4c0e7411a7e1 Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Tue, 20 Apr 2021 12:59:46 -0400 Subject: chore: lower the input to fine more cats --- bot/exts/evergreen/catify.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py index c409ce6c..262c75bd 100644 --- a/bot/exts/evergreen/catify.py +++ b/bot/exts/evergreen/catify.py @@ -52,11 +52,12 @@ class Catify(commands.Cog): string_list = text.split() for index, name in enumerate(string_list): - if "cat" in name: + name = name.lower() + if "cat" in text: if random.randint(0, 5) == 5: - string_list[index] = name.replace("cat", f"**{random.choice(Cats.cats)}**") + string_list[index] = text.replace("cat", f"**{random.choice(Cats.cats)}**") else: - string_list[index] = name.replace("cat", random.choice(Cats.cats)) + string_list[index] = text.replace("cat", random.choice(Cats.cats)) for element in Cats.cats: if element in name: string_list[index] = name.replace(element, "cat") -- cgit v1.2.3 From 5f889e4d3f0712c0005dbbc7c3ee820cc786ec30 Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Tue, 20 Apr 2021 13:05:03 -0400 Subject: fix: Use name.replace not text.replace --- bot/exts/evergreen/catify.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py index 262c75bd..88c63202 100644 --- a/bot/exts/evergreen/catify.py +++ b/bot/exts/evergreen/catify.py @@ -53,11 +53,11 @@ class Catify(commands.Cog): string_list = text.split() for index, name in enumerate(string_list): name = name.lower() - if "cat" in text: + if "cat" in name: if random.randint(0, 5) == 5: - string_list[index] = text.replace("cat", f"**{random.choice(Cats.cats)}**") + string_list[index] = name.replace("cat", f"**{random.choice(Cats.cats)}**") else: - string_list[index] = text.replace("cat", random.choice(Cats.cats)) + string_list[index] = name.replace("cat", random.choice(Cats.cats)) for element in Cats.cats: if element in name: string_list[index] = name.replace(element, "cat") -- cgit v1.2.3 From daf7dc79753d0d4482f58ddcad0ee2a7f7a244c1 Mon Sep 17 00:00:00 2001 From: ToxicKidz <78174417+ToxicKidz@users.noreply.github.com> Date: Tue, 20 Apr 2021 16:13:57 -0400 Subject: chore: use 'nickname' and 'display name' in the right places and use allowed_mentions --- bot/exts/evergreen/catify.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py index 88c63202..ae8d54b6 100644 --- a/bot/exts/evergreen/catify.py +++ b/bot/exts/evergreen/catify.py @@ -27,7 +27,10 @@ class Catify(commands.Cog): if len(display_name) > 26: embed = Embed( title=random.choice(NEGATIVE_REPLIES), - description="Your nickname is too long to be catified! Please change it to be under 26 characters.", + description=( + "Your display name is too long to be catified! " + "Please change it to be under 26 characters." + ), color=Colours.soft_red ) await ctx.send(embed=embed) @@ -36,7 +39,7 @@ class Catify(commands.Cog): else: display_name += f" | {random.choice(Cats.cats)}" - await ctx.send(f"Your catified username is: `{display_name}`") + await ctx.send(f"Your catified nickname is: `{display_name}`", allowed_mentions=AllowedMentions.none()) with suppress(Forbidden): await ctx.author.edit(nick=display_name) -- cgit v1.2.3 From 29084d1576e1435a5a4a32071a79b6af28acd362 Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 22 Apr 2021 20:25:38 +0100 Subject: Fix errors when a subreddit has <5 posts. If a subreddit has <2 posts, the posts[1] check would fail with an IndexError. If the subreddit had less that 5 posts, then the k=5 check would also error. These changes harden the command for these edge cases. --- bot/exts/evergreen/reddit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/reddit.py b/bot/exts/evergreen/reddit.py index 49127bea..4fdb6fca 100644 --- a/bot/exts/evergreen/reddit.py +++ b/bot/exts/evergreen/reddit.py @@ -54,7 +54,7 @@ class Reddit(commands.Cog): if not posts: return await ctx.send('No posts available!') - if posts[1]["data"]["over_18"] is True: + if posts[0]["data"]["over_18"] is True: return await ctx.send( "You cannot access this Subreddit as it is ment for those who " "are 18 years or older." @@ -63,7 +63,7 @@ class Reddit(commands.Cog): embed_titles = "" # Chooses k unique random elements from a population sequence or set. - random_posts = random.sample(posts, k=5) + random_posts = random.sample(posts, k=min(len(posts), 5)) # ----------------------------------------------------------- # This code below is bound of change when the emojis are added. -- cgit v1.2.3 From 9b9ab546090dc2513ac00f6b476b8b494eb5428a Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 22 Apr 2021 21:02:31 +0100 Subject: Escape invalid filename chars before saving. This would cause an issue with the embed not embedding the image due to a filename mismatch. This has been implemented by escaping all invalid filename characters before saving the file. --- .../profile_pic_modification/pfp_modify.py | 40 +++++++++++++--------- 1 file changed, 24 insertions(+), 16 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py index 77c937de..f6b1d394 100644 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py @@ -1,7 +1,9 @@ import asyncio import json import logging +import string import typing as t +import unicodedata from concurrent.futures import ThreadPoolExecutor import discord @@ -36,6 +38,23 @@ async def in_executor(func: t.Callable, *args) -> t.Any: return await loop.run_in_executor(_EXECUTOR, func, *args) +def file_safe_name(effect: str, display_name: str) -> str: + """Returns a file safe filename based on the given effect and display name.""" + valid_filename_chars = f"-_.() {string.ascii_letters}{string.digits}" + + file_name = FILENAME_STRING.format(effect=effect, author=display_name) + + # Replace spaces + file_name = file_name.replace(" ", "_") + + # Normalize unicode characters + cleaned_filename = unicodedata.normalize('NFKD', file_name).encode('ASCII', 'ignore').decode() + + # Remove invalid filename characters + cleaned_filename = ''.join(c for c in cleaned_filename if c in valid_filename_chars) + return cleaned_filename + + class PfpModify(commands.Cog): """Various commands for users to change their own profile picture.""" @@ -76,10 +95,7 @@ class PfpModify(commands.Cog): return image_bytes = await member.avatar_url.read() - file_name = FILENAME_STRING.format( - effect="eightbit_avatar", - author=member.display_name - ) + file_name = file_safe_name("eightbit_avatar", member.display_name) file = await in_executor( PfpEffects.apply_effect, @@ -135,10 +151,7 @@ class PfpModify(commands.Cog): ctx.send = send_message # Reassigns ctx.send image_bytes = await member.avatar_url_as(size=256).read() - file_name = FILENAME_STRING.format( - effect="easterified_avatar", - author=member.display_name - ) + file_name = file_safe_name("easterified_avatar", member.display_name) file = await in_executor( PfpEffects.apply_effect, @@ -167,10 +180,7 @@ class PfpModify(commands.Cog): ) -> None: """Gets and sends the image in an embed. Used by the pride commands.""" async with ctx.typing(): - file_name = FILENAME_STRING.format( - effect="pride_avatar", - author=ctx.author.display_name - ) + file_name = file_safe_name("pride_avatar", ctx.author.display_name) file = await in_executor( PfpEffects.apply_effect, @@ -280,10 +290,8 @@ class PfpModify(commands.Cog): async with ctx.typing(): image_bytes = await member.avatar_url.read() - file_name = FILENAME_STRING.format( - effect="spooky_avatar", - author=member.display_name - ) + file_name = file_safe_name("spooky_avatar", member.display_name) + file = await in_executor( PfpEffects.apply_effect, image_bytes, -- cgit v1.2.3 From e858a3682ffe36675570508802f633046b39ebd8 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Fri, 23 Apr 2021 02:55:03 +0300 Subject: Adds Link Suppressing Helper Adds a helper to find and escape links in a message. Signed-off-by: Hassan Abouelela --- bot/utils/helpers.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 bot/utils/helpers.py (limited to 'bot') diff --git a/bot/utils/helpers.py b/bot/utils/helpers.py new file mode 100644 index 00000000..74c2ccd0 --- /dev/null +++ b/bot/utils/helpers.py @@ -0,0 +1,8 @@ +import re + + +def suppress_links(message: str) -> str: + """Accepts a message that may contain links, suppresses them, and returns them.""" + for link in set(re.findall(r"https?://[^\s]+", message, re.IGNORECASE)): + message = message.replace(link, f"<{link}>") + return message -- cgit v1.2.3 From 9e03064e9c116b0dd2bcc65b149b7ac9ee389ff3 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela Date: Fri, 23 Apr 2021 02:59:30 +0300 Subject: Suppresses Links In Commands Suppresses links in certain commands that can echo back user input. Signed-off-by: Hassan Abouelela --- bot/exts/easter/egg_decorating.py | 4 +++- bot/exts/evergreen/catify.py | 3 ++- bot/exts/evergreen/fun.py | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) (limited to 'bot') diff --git a/bot/exts/easter/egg_decorating.py b/bot/exts/easter/egg_decorating.py index b18e6636..a847388d 100644 --- a/bot/exts/easter/egg_decorating.py +++ b/bot/exts/easter/egg_decorating.py @@ -10,6 +10,8 @@ import discord from PIL import Image from discord.ext import commands +from bot.utils import helpers + log = logging.getLogger(__name__) with open(Path("bot/resources/evergreen/html_colours.json"), encoding="utf8") as f: @@ -65,7 +67,7 @@ class EggDecorating(commands.Cog): if value: colours[idx] = discord.Colour(value) else: - invalid.append(colour) + invalid.append(helpers.suppress_links(colour)) if len(invalid) > 1: return await ctx.send(f"Sorry, I don't know these colours: {' '.join(invalid)}") diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py index ae8d54b6..d8a7442d 100644 --- a/bot/exts/evergreen/catify.py +++ b/bot/exts/evergreen/catify.py @@ -6,6 +6,7 @@ from discord import AllowedMentions, Embed, Forbidden from discord.ext import commands from bot.constants import Cats, Colours, NEGATIVE_REPLIES +from bot.utils import helpers class Catify(commands.Cog): @@ -74,7 +75,7 @@ class Catify(commands.Cog): else: string_list.insert(random.randint(0, len(string_list)), random.choice(Cats.cats)) - text = " ".join(string_list) + text = helpers.suppress_links(" ".join(string_list)) await ctx.send( f">>> {text}", allowed_mentions=AllowedMentions.none() diff --git a/bot/exts/evergreen/fun.py b/bot/exts/evergreen/fun.py index 101725da..7152d0cb 100644 --- a/bot/exts/evergreen/fun.py +++ b/bot/exts/evergreen/fun.py @@ -11,6 +11,7 @@ from discord.ext.commands import BadArgument, Bot, Cog, Context, MessageConverte from bot import utils from bot.constants import Client, Colours, Emojis +from bot.utils import helpers log = logging.getLogger(__name__) @@ -83,6 +84,7 @@ class Fun(Cog): if embed is not None: embed = Fun._convert_embed(conversion_func, embed) converted_text = conversion_func(text) + converted_text = helpers.suppress_links(converted_text) # Don't put >>> if only embed present if converted_text: converted_text = f">>> {converted_text.lstrip('> ')}" @@ -101,6 +103,7 @@ class Fun(Cog): if embed is not None: embed = Fun._convert_embed(conversion_func, embed) converted_text = conversion_func(text) + converted_text = helpers.suppress_links(converted_text) # Don't put >>> if only embed present if converted_text: converted_text = f">>> {converted_text.lstrip('> ')}" -- cgit v1.2.3 From d77986e528b977170c30efb6afc41e53425ad6df Mon Sep 17 00:00:00 2001 From: ChrisJL Date: Fri, 23 Apr 2021 12:28:37 +0100 Subject: Fix spelling of a user facing message in reddit cog Co-authored-by: Xithrius <15021300+Xithrius@users.noreply.github.com> --- bot/exts/evergreen/reddit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/reddit.py b/bot/exts/evergreen/reddit.py index 4fdb6fca..2be511c8 100644 --- a/bot/exts/evergreen/reddit.py +++ b/bot/exts/evergreen/reddit.py @@ -56,7 +56,7 @@ class Reddit(commands.Cog): if posts[0]["data"]["over_18"] is True: return await ctx.send( - "You cannot access this Subreddit as it is ment for those who " + "You cannot access this Subreddit as it is meant for those who " "are 18 years or older." ) -- cgit v1.2.3 From e666fb7b80a9f590257d1426c7604ff180b35e6e Mon Sep 17 00:00:00 2001 From: Salil Chincholikar Date: Tue, 27 Apr 2021 14:39:58 +0530 Subject: Reworded/fixed grammatical error --- bot/utils/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/utils/exceptions.py b/bot/utils/exceptions.py index 2b1c1b31..5e4d6330 100644 --- a/bot/utils/exceptions.py +++ b/bot/utils/exceptions.py @@ -1,4 +1,4 @@ class UserNotPlayingError(Exception): - """Will raised when user try to use game commands when not playing.""" + """Will be raised when the users try to use game commands when they are not playing.""" pass -- cgit v1.2.3 From 4e0210922ef8d25eeb18f080c9baab022d70d0d2 Mon Sep 17 00:00:00 2001 From: Salil Chincholikar <31334826+chincholikarsalil@users.noreply.github.com> Date: Tue, 27 Apr 2021 19:12:28 +0530 Subject: Updated bot/utils/exceptions.py Co-authored-by: Joe Banks --- bot/utils/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/utils/exceptions.py b/bot/utils/exceptions.py index 5e4d6330..9e080759 100644 --- a/bot/utils/exceptions.py +++ b/bot/utils/exceptions.py @@ -1,4 +1,4 @@ class UserNotPlayingError(Exception): - """Will be raised when the users try to use game commands when they are not playing.""" + """Raised when users try to use game commands when they are not playing.""" pass -- cgit v1.2.3 From dade71a609d298e8bfb16fef8c8ecbd2faffe51d Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 30 Apr 2021 10:00:51 +0100 Subject: Rename pfp_modify cog and group to avatar_mod for clarity Added in aliases for ease of use too. --- bot/exts/evergreen/avatar_modification/__init__.py | 0 bot/exts/evergreen/avatar_modification/_effects.py | 139 +++++++++ .../evergreen/avatar_modification/avatar_modify.py | 315 +++++++++++++++++++++ .../evergreen/profile_pic_modification/__init__.py | 0 .../evergreen/profile_pic_modification/_effects.py | 139 --------- .../profile_pic_modification/pfp_modify.py | 315 --------------------- 6 files changed, 454 insertions(+), 454 deletions(-) create mode 100644 bot/exts/evergreen/avatar_modification/__init__.py create mode 100644 bot/exts/evergreen/avatar_modification/_effects.py create mode 100644 bot/exts/evergreen/avatar_modification/avatar_modify.py delete mode 100644 bot/exts/evergreen/profile_pic_modification/__init__.py delete mode 100644 bot/exts/evergreen/profile_pic_modification/_effects.py delete mode 100644 bot/exts/evergreen/profile_pic_modification/pfp_modify.py (limited to 'bot') diff --git a/bot/exts/evergreen/avatar_modification/__init__.py b/bot/exts/evergreen/avatar_modification/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bot/exts/evergreen/avatar_modification/_effects.py b/bot/exts/evergreen/avatar_modification/_effects.py new file mode 100644 index 00000000..e415d700 --- /dev/null +++ b/bot/exts/evergreen/avatar_modification/_effects.py @@ -0,0 +1,139 @@ +import typing as t +from io import BytesIO +from pathlib import Path + +import discord +from PIL import Image, ImageDraw, ImageOps + +from bot.constants import Colours + + +class PfpEffects: + """ + Implements various image modifying effects, for the PfpModify cog. + + All of these fuctions are slow, and blocking, so they should be ran in executors. + """ + + @staticmethod + def apply_effect(image_bytes: bytes, effect: t.Callable, filename: str, *args) -> discord.File: + """Applies the given effect to the image passed to it.""" + im = Image.open(BytesIO(image_bytes)) + im = im.convert("RGBA") + im = effect(im, *args) + + bufferedio = BytesIO() + im.save(bufferedio, format="PNG") + bufferedio.seek(0) + + return discord.File(bufferedio, filename=filename) + + @staticmethod + def closest(x: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: + """ + Finds the closest "easter" colour to a given pixel. + + Returns a merge between the original colour and the closest colour. + """ + r1, g1, b1 = x + + def distance(point: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: + """Finds the difference between a pastel colour and the original pixel colour.""" + r2, g2, b2 = point + return (r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2 + + closest_colours = sorted(Colours.easter_like_colours, key=distance) + r2, g2, b2 = closest_colours[0] + r = (r1 + r2) // 2 + g = (g1 + g2) // 2 + b = (b1 + b2) // 2 + + return r, g, b + + @staticmethod + def crop_avatar_circle(avatar: Image) -> Image: + """This crops the avatar given into a circle.""" + mask = Image.new("L", avatar.size, 0) + draw = ImageDraw.Draw(mask) + draw.ellipse((0, 0) + avatar.size, fill=255) + avatar.putalpha(mask) + return avatar + + @staticmethod + def crop_ring(ring: Image, px: int) -> Image: + """This crops the given ring into a circle.""" + mask = Image.new("L", ring.size, 0) + draw = ImageDraw.Draw(mask) + draw.ellipse((0, 0) + ring.size, fill=255) + draw.ellipse((px, px, 1024-px, 1024-px), fill=0) + ring.putalpha(mask) + return ring + + @staticmethod + def pridify_effect(image: Image, pixels: int, flag: str) -> Image: + """Applies the given pride effect to the given image.""" + image = image.resize((1024, 1024)) + image = PfpEffects.crop_avatar_circle(image) + + ring = Image.open(Path(f"bot/resources/pride/flags/{flag}.png")).resize((1024, 1024)) + ring = ring.convert("RGBA") + ring = PfpEffects.crop_ring(ring, pixels) + + image.alpha_composite(ring, (0, 0)) + return image + + @staticmethod + def eight_bitify_effect(image: Image) -> Image: + """ + Applies the 8bit effect to the given image. + + This is done by reducing the image to 32x32 and then back up to 1024x1024. + We then quantize the image before returning too. + """ + image = image.resize((32, 32), resample=Image.NEAREST) + image = image.resize((1024, 1024), resample=Image.NEAREST) + return image.quantize() + + @staticmethod + def easterify_effect(image: Image, overlay_image: Image = None) -> Image: + """ + Applies the easter effect to the given image. + + This is done by getting the closest "easter" colour to each pixel and changing the colour + to the half-way RGBvalue. + + We also then add an overlay image on top in middle right, a chocolate bunny by default. + """ + if overlay_image: + ratio = 64 / overlay_image.height + overlay_image = overlay_image.resize(( + round(overlay_image.width * ratio), + round(overlay_image.height * ratio) + )) + overlay_image = overlay_image.convert("RGBA") + else: + overlay_image = Image.open(Path("bot/resources/easter/chocolate_bunny.png")) + + alpha = image.getchannel("A").getdata() + image = image.convert("RGB") + image = ImageOps.posterize(image, 6) + + data = image.getdata() + data_set = set(data) + easterified_data_set = {} + + for x in data_set: + easterified_data_set[x] = PfpEffects.closest(x) + new_pixel_data = [ + (*easterified_data_set[x], alpha[i]) + if x in easterified_data_set else x + for i, x in enumerate(data) + ] + + im = Image.new("RGBA", image.size) + im.putdata(new_pixel_data) + im.alpha_composite( + overlay_image, + (im.width - overlay_image.width, (im.height - overlay_image.height) // 2) + ) + return im diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py new file mode 100644 index 00000000..e1078fa0 --- /dev/null +++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py @@ -0,0 +1,315 @@ +import asyncio +import json +import logging +import string +import typing as t +import unicodedata +from concurrent.futures import ThreadPoolExecutor + +import discord +from aiohttp import client_exceptions +from discord.ext import commands +from discord.ext.commands.errors import BadArgument + +from bot.constants import Client, Colours, Emojis +from bot.exts.evergreen.avatar_modification._effects import PfpEffects +from bot.utils.extensions import invoke_help_command +from bot.utils.halloween import spookifications + +log = logging.getLogger(__name__) + +_EXECUTOR = ThreadPoolExecutor(10) + +FILENAME_STRING = "{effect}_{author}.png" + +with open('bot/resources/pride/gender_options.json') as f: + GENDER_OPTIONS = json.load(f) + + +async def in_executor(func: t.Callable, *args) -> t.Any: + """ + Runs the given synchronus function `func` in an executor. + + This is useful for running slow, blocking code within async + functions, so that they don't block the bot. + """ + log.trace(f"Running {func.__name__} in an executor.") + loop = asyncio.get_event_loop() + return await loop.run_in_executor(_EXECUTOR, func, *args) + + +def file_safe_name(effect: str, display_name: str) -> str: + """Returns a file safe filename based on the given effect and display name.""" + valid_filename_chars = f"-_.() {string.ascii_letters}{string.digits}" + + file_name = FILENAME_STRING.format(effect=effect, author=display_name) + + # Replace spaces + file_name = file_name.replace(" ", "_") + + # Normalize unicode characters + cleaned_filename = unicodedata.normalize('NFKD', file_name).encode('ASCII', 'ignore').decode() + + # Remove invalid filename characters + cleaned_filename = ''.join(c for c in cleaned_filename if c in valid_filename_chars) + return cleaned_filename + + +class AvatarModify(commands.Cog): + """Various commands for users to apply affects to their own avatars.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + async def _fetch_member(self, member_id: int) -> t.Optional[discord.Member]: + """ + Fetches a member and handles errors. + + This helper funciton is required as the member cache doesn't always have the most up to date + profile picture. This can lead to errors if the image is delted from the Discord CDN. + """ + try: + member = await self.bot.get_guild(Client.guild).fetch_member(member_id) + except discord.errors.NotFound: + log.debug(f"Member {member_id} left the guild before we could get their pfp.") + return None + except discord.HTTPException: + log.exception(f"Exception while trying to retrieve member {member_id} from Discord.") + return None + + return member + + @commands.group(aliases=('avatar_mod', 'pfp_mod')) + 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: + await invoke_help_command(ctx) + + @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(): + member = await self._fetch_member(ctx.author.id) + if not member: + await ctx.send(f"{Emojis.cross_mark} Could not get member info.") + return + + image_bytes = await member.avatar_url.read() + file_name = file_safe_name("eightbit_avatar", member.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 {member.display_name}.", icon_url=member.avatar_url) + + await ctx.send(embed=embed, file=file) + + @avatar_modify.command(aliases=["easterify"], root_aliases=("easterify", "avatareasterify")) + async def avatareasterify(self, ctx: commands.Context, *colours: t.Union[discord.Colour, str]) -> None: + """ + This "Easterifies" 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. + """ + async def send(*args, **kwargs) -> str: + """ + This replaces the original ctx.send. + + When invoking the egg decorating command, the egg itself doesn't print to to the channel. + Returns the message content so that if any errors occur, the error message can be output. + """ + if args: + return args[0] + + async with ctx.typing(): + member = await self._fetch_member(ctx.author.id) + if not member: + await ctx.send(f"{Emojis.cross_mark} Could not get member 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 + + image_bytes = await member.avatar_url_as(size=256).read() + file_name = file_safe_name("easterified_avatar", member.display_name) + + file = await in_executor( + PfpEffects.apply_effect, + image_bytes, + PfpEffects.easterify_effect, + file_name, + egg + ) + + embed = discord.Embed( + name="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 {member.display_name}.", icon_url=member.avatar_url) + + await ctx.send(file=file, embed=embed) + + @staticmethod + async def send_pride_image( + ctx: commands.Context, + image_bytes: bytes, + pixels: int, + flag: str, + option: str + ) -> None: + """Gets and sends the image in an embed. Used by the pride commands.""" + async with ctx.typing(): + file_name = file_safe_name("pride_avatar", ctx.author.display_name) + + file = await in_executor( + PfpEffects.apply_effect, + image_bytes, + PfpEffects.pridify_effect, + file_name, + pixels, + flag + ) + + embed = discord.Embed( + name="Your Lovely Pride Avatar!", + description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D" + ) + embed.set_image(url=f"attachment://{file_name}") + embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) + await ctx.send(file=file, embed=embed) + + @avatar_modify.group( + aliases=["avatarpride", "pridepfp", "prideprofile"], + root_aliases=("prideavatar", "avatarpride", "pridepfp", "prideprofile"), + invoke_without_command=True + ) + async def prideavatar(self, ctx: commands.Context, option: str = "lgbt", pixels: int = 64) -> None: + """ + This surrounds an avatar with a border of a specified LGBT flag. + + This defaults to the LGBT rainbow flag if none is given. + The amount of pixels can be given which determines the thickness of the flag border. + This has a maximum of 512px and defaults to a 64px border. + The full image is 1024x1024. + """ + option = option.lower() + pixels = max(0, min(512, pixels)) + flag = GENDER_OPTIONS.get(option) + if flag is None: + await ctx.send("I don't have that flag!") + return + + async with ctx.typing(): + member = await self._fetch_member(ctx.author.id) + if not member: + await ctx.send(f"{Emojis.cross_mark} Could not get member info.") + return + image_bytes = await member.avatar_url_as(size=1024).read() + await self.send_pride_image(ctx, image_bytes, pixels, flag, option) + + @prideavatar.command() + async def image(self, ctx: commands.Context, url: str, option: str = "lgbt", pixels: int = 64) -> None: + """ + This surrounds the image specified by the URL with a border of a specified LGBT flag. + + This defaults to the LGBT rainbow flag if none is given. + The amount of pixels can be given which determines the thickness of the flag border. + This has a maximum of 512px and defaults to a 64px border. + The full image is 1024x1024. + """ + option = option.lower() + pixels = max(0, min(512, pixels)) + flag = GENDER_OPTIONS.get(option) + if flag is None: + await ctx.send("I don't have that flag!") + return + + async with ctx.typing(): + try: + async with self.bot.http_session.get(url) as response: + if response.status != 200: + await ctx.send("Bad response from provided URL!") + return + image_bytes = await response.read() + except client_exceptions.ClientConnectorError: + raise BadArgument("Cannot connect to provided URL!") + except client_exceptions.InvalidURL: + raise BadArgument("Invalid URL!") + + await self.send_pride_image(ctx, image_bytes, pixels, flag, option) + + @prideavatar.command() + async def flags(self, ctx: commands.Context) -> None: + """This lists the flags that can be used with the prideavatar command.""" + choices = sorted(set(GENDER_OPTIONS.values())) + options = "• " + "\n• ".join(choices) + embed = discord.Embed( + title="I have the following flags:", + description=options, + colour=Colours.soft_red + ) + await ctx.send(embed=embed) + + @avatar_modify.command( + name='spookyavatar', + aliases=('savatar', 'spookify'), + root_aliases=('spookyavatar', 'spookify', 'savatar'), + brief='Spookify an user\'s avatar.' + ) + async def spooky_avatar(self, ctx: commands.Context, member: discord.Member = None) -> None: + """This "spookifies" the given user's avatar, with a random *spooky* effect.""" + if member is None: + member = ctx.author + + member = await self._fetch_member(member.id) + if not member: + await ctx.send(f"{Emojis.cross_mark} Could not get member info.") + return + + async with ctx.typing(): + image_bytes = await member.avatar_url.read() + + file_name = file_safe_name("spooky_avatar", member.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_author(name=member.name, icon_url=member.avatar_url) + embed.set_image(url=f"attachment://{file_name}") + embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) + + await ctx.send(file=file, embed=embed) + + +def setup(bot: commands.Bot) -> None: + """Load the PfpModify cog.""" + bot.add_cog(AvatarModify(bot)) diff --git a/bot/exts/evergreen/profile_pic_modification/__init__.py b/bot/exts/evergreen/profile_pic_modification/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/bot/exts/evergreen/profile_pic_modification/_effects.py b/bot/exts/evergreen/profile_pic_modification/_effects.py deleted file mode 100644 index e415d700..00000000 --- a/bot/exts/evergreen/profile_pic_modification/_effects.py +++ /dev/null @@ -1,139 +0,0 @@ -import typing as t -from io import BytesIO -from pathlib import Path - -import discord -from PIL import Image, ImageDraw, ImageOps - -from bot.constants import Colours - - -class PfpEffects: - """ - Implements various image modifying effects, for the PfpModify cog. - - All of these fuctions are slow, and blocking, so they should be ran in executors. - """ - - @staticmethod - def apply_effect(image_bytes: bytes, effect: t.Callable, filename: str, *args) -> discord.File: - """Applies the given effect to the image passed to it.""" - im = Image.open(BytesIO(image_bytes)) - im = im.convert("RGBA") - im = effect(im, *args) - - bufferedio = BytesIO() - im.save(bufferedio, format="PNG") - bufferedio.seek(0) - - return discord.File(bufferedio, filename=filename) - - @staticmethod - def closest(x: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: - """ - Finds the closest "easter" colour to a given pixel. - - Returns a merge between the original colour and the closest colour. - """ - r1, g1, b1 = x - - def distance(point: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]: - """Finds the difference between a pastel colour and the original pixel colour.""" - r2, g2, b2 = point - return (r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2 - - closest_colours = sorted(Colours.easter_like_colours, key=distance) - r2, g2, b2 = closest_colours[0] - r = (r1 + r2) // 2 - g = (g1 + g2) // 2 - b = (b1 + b2) // 2 - - return r, g, b - - @staticmethod - def crop_avatar_circle(avatar: Image) -> Image: - """This crops the avatar given into a circle.""" - mask = Image.new("L", avatar.size, 0) - draw = ImageDraw.Draw(mask) - draw.ellipse((0, 0) + avatar.size, fill=255) - avatar.putalpha(mask) - return avatar - - @staticmethod - def crop_ring(ring: Image, px: int) -> Image: - """This crops the given ring into a circle.""" - mask = Image.new("L", ring.size, 0) - draw = ImageDraw.Draw(mask) - draw.ellipse((0, 0) + ring.size, fill=255) - draw.ellipse((px, px, 1024-px, 1024-px), fill=0) - ring.putalpha(mask) - return ring - - @staticmethod - def pridify_effect(image: Image, pixels: int, flag: str) -> Image: - """Applies the given pride effect to the given image.""" - image = image.resize((1024, 1024)) - image = PfpEffects.crop_avatar_circle(image) - - ring = Image.open(Path(f"bot/resources/pride/flags/{flag}.png")).resize((1024, 1024)) - ring = ring.convert("RGBA") - ring = PfpEffects.crop_ring(ring, pixels) - - image.alpha_composite(ring, (0, 0)) - return image - - @staticmethod - def eight_bitify_effect(image: Image) -> Image: - """ - Applies the 8bit effect to the given image. - - This is done by reducing the image to 32x32 and then back up to 1024x1024. - We then quantize the image before returning too. - """ - image = image.resize((32, 32), resample=Image.NEAREST) - image = image.resize((1024, 1024), resample=Image.NEAREST) - return image.quantize() - - @staticmethod - def easterify_effect(image: Image, overlay_image: Image = None) -> Image: - """ - Applies the easter effect to the given image. - - This is done by getting the closest "easter" colour to each pixel and changing the colour - to the half-way RGBvalue. - - We also then add an overlay image on top in middle right, a chocolate bunny by default. - """ - if overlay_image: - ratio = 64 / overlay_image.height - overlay_image = overlay_image.resize(( - round(overlay_image.width * ratio), - round(overlay_image.height * ratio) - )) - overlay_image = overlay_image.convert("RGBA") - else: - overlay_image = Image.open(Path("bot/resources/easter/chocolate_bunny.png")) - - alpha = image.getchannel("A").getdata() - image = image.convert("RGB") - image = ImageOps.posterize(image, 6) - - data = image.getdata() - data_set = set(data) - easterified_data_set = {} - - for x in data_set: - easterified_data_set[x] = PfpEffects.closest(x) - new_pixel_data = [ - (*easterified_data_set[x], alpha[i]) - if x in easterified_data_set else x - for i, x in enumerate(data) - ] - - im = Image.new("RGBA", image.size) - im.putdata(new_pixel_data) - im.alpha_composite( - overlay_image, - (im.width - overlay_image.width, (im.height - overlay_image.height) // 2) - ) - return im diff --git a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py b/bot/exts/evergreen/profile_pic_modification/pfp_modify.py deleted file mode 100644 index f6b1d394..00000000 --- a/bot/exts/evergreen/profile_pic_modification/pfp_modify.py +++ /dev/null @@ -1,315 +0,0 @@ -import asyncio -import json -import logging -import string -import typing as t -import unicodedata -from concurrent.futures import ThreadPoolExecutor - -import discord -from aiohttp import client_exceptions -from discord.ext import commands -from discord.ext.commands.errors import BadArgument - -from bot.constants import Client, Colours, Emojis -from bot.exts.evergreen.profile_pic_modification._effects import PfpEffects -from bot.utils.extensions import invoke_help_command -from bot.utils.halloween import spookifications - -log = logging.getLogger(__name__) - -_EXECUTOR = ThreadPoolExecutor(10) - -FILENAME_STRING = "{effect}_{author}.png" - -with open('bot/resources/pride/gender_options.json') as f: - GENDER_OPTIONS = json.load(f) - - -async def in_executor(func: t.Callable, *args) -> t.Any: - """ - Runs the given synchronus function `func` in an executor. - - This is useful for running slow, blocking code within async - functions, so that they don't block the bot. - """ - log.trace(f"Running {func.__name__} in an executor.") - loop = asyncio.get_event_loop() - return await loop.run_in_executor(_EXECUTOR, func, *args) - - -def file_safe_name(effect: str, display_name: str) -> str: - """Returns a file safe filename based on the given effect and display name.""" - valid_filename_chars = f"-_.() {string.ascii_letters}{string.digits}" - - file_name = FILENAME_STRING.format(effect=effect, author=display_name) - - # Replace spaces - file_name = file_name.replace(" ", "_") - - # Normalize unicode characters - cleaned_filename = unicodedata.normalize('NFKD', file_name).encode('ASCII', 'ignore').decode() - - # Remove invalid filename characters - cleaned_filename = ''.join(c for c in cleaned_filename if c in valid_filename_chars) - return cleaned_filename - - -class PfpModify(commands.Cog): - """Various commands for users to change their own profile picture.""" - - def __init__(self, bot: commands.Bot) -> None: - self.bot = bot - - async def _fetch_member(self, member_id: int) -> t.Optional[discord.Member]: - """ - Fetches a member and handles errors. - - This helper funciton is required as the member cache doesn't always have the most up to date - profile picture. This can lead to errors if the image is delted from the Discord CDN. - """ - try: - member = await self.bot.get_guild(Client.guild).fetch_member(member_id) - except discord.errors.NotFound: - log.debug(f"Member {member_id} left the guild before we could get their pfp.") - return None - except discord.HTTPException: - log.exception(f"Exception while trying to retrieve member {member_id} from Discord.") - return None - - return member - - @commands.group() - async def pfp_modify(self, ctx: commands.Context) -> None: - """Groups all of the pfp modifying commands to allow a single concurrency limit.""" - if not ctx.invoked_subcommand: - await invoke_help_command(ctx) - - @pfp_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(): - member = await self._fetch_member(ctx.author.id) - if not member: - await ctx.send(f"{Emojis.cross_mark} Could not get member info.") - return - - image_bytes = await member.avatar_url.read() - file_name = file_safe_name("eightbit_avatar", member.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 {member.display_name}.", icon_url=member.avatar_url) - - await ctx.send(embed=embed, file=file) - - @pfp_modify.command(aliases=["easterify"], root_aliases=("easterify", "avatareasterify")) - async def avatareasterify(self, ctx: commands.Context, *colours: t.Union[discord.Colour, str]) -> None: - """ - This "Easterifies" 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. - """ - async def send(*args, **kwargs) -> str: - """ - This replaces the original ctx.send. - - When invoking the egg decorating command, the egg itself doesn't print to to the channel. - Returns the message content so that if any errors occur, the error message can be output. - """ - if args: - return args[0] - - async with ctx.typing(): - member = await self._fetch_member(ctx.author.id) - if not member: - await ctx.send(f"{Emojis.cross_mark} Could not get member 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 - - image_bytes = await member.avatar_url_as(size=256).read() - file_name = file_safe_name("easterified_avatar", member.display_name) - - file = await in_executor( - PfpEffects.apply_effect, - image_bytes, - PfpEffects.easterify_effect, - file_name, - egg - ) - - embed = discord.Embed( - name="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 {member.display_name}.", icon_url=member.avatar_url) - - await ctx.send(file=file, embed=embed) - - @staticmethod - async def send_pride_image( - ctx: commands.Context, - image_bytes: bytes, - pixels: int, - flag: str, - option: str - ) -> None: - """Gets and sends the image in an embed. Used by the pride commands.""" - async with ctx.typing(): - file_name = file_safe_name("pride_avatar", ctx.author.display_name) - - file = await in_executor( - PfpEffects.apply_effect, - image_bytes, - PfpEffects.pridify_effect, - file_name, - pixels, - flag - ) - - embed = discord.Embed( - name="Your Lovely Pride Avatar!", - description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D" - ) - embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) - await ctx.send(file=file, embed=embed) - - @pfp_modify.group( - aliases=["avatarpride", "pridepfp", "prideprofile"], - root_aliases=("prideavatar", "avatarpride", "pridepfp", "prideprofile"), - invoke_without_command=True - ) - async def prideavatar(self, ctx: commands.Context, option: str = "lgbt", pixels: int = 64) -> None: - """ - This surrounds an avatar with a border of a specified LGBT flag. - - This defaults to the LGBT rainbow flag if none is given. - The amount of pixels can be given which determines the thickness of the flag border. - This has a maximum of 512px and defaults to a 64px border. - The full image is 1024x1024. - """ - option = option.lower() - pixels = max(0, min(512, pixels)) - flag = GENDER_OPTIONS.get(option) - if flag is None: - await ctx.send("I don't have that flag!") - return - - async with ctx.typing(): - member = await self._fetch_member(ctx.author.id) - if not member: - await ctx.send(f"{Emojis.cross_mark} Could not get member info.") - return - image_bytes = await member.avatar_url_as(size=1024).read() - await self.send_pride_image(ctx, image_bytes, pixels, flag, option) - - @prideavatar.command() - async def image(self, ctx: commands.Context, url: str, option: str = "lgbt", pixels: int = 64) -> None: - """ - This surrounds the image specified by the URL with a border of a specified LGBT flag. - - This defaults to the LGBT rainbow flag if none is given. - The amount of pixels can be given which determines the thickness of the flag border. - This has a maximum of 512px and defaults to a 64px border. - The full image is 1024x1024. - """ - option = option.lower() - pixels = max(0, min(512, pixels)) - flag = GENDER_OPTIONS.get(option) - if flag is None: - await ctx.send("I don't have that flag!") - return - - async with ctx.typing(): - try: - async with self.bot.http_session.get(url) as response: - if response.status != 200: - await ctx.send("Bad response from provided URL!") - return - image_bytes = await response.read() - except client_exceptions.ClientConnectorError: - raise BadArgument("Cannot connect to provided URL!") - except client_exceptions.InvalidURL: - raise BadArgument("Invalid URL!") - - await self.send_pride_image(ctx, image_bytes, pixels, flag, option) - - @prideavatar.command() - async def flags(self, ctx: commands.Context) -> None: - """This lists the flags that can be used with the prideavatar command.""" - choices = sorted(set(GENDER_OPTIONS.values())) - options = "• " + "\n• ".join(choices) - embed = discord.Embed( - title="I have the following flags:", - description=options, - colour=Colours.soft_red - ) - await ctx.send(embed=embed) - - @pfp_modify.command( - name='spookyavatar', - aliases=('savatar', 'spookify'), - root_aliases=('spookyavatar', 'spookify', 'savatar'), - brief='Spookify an user\'s avatar.' - ) - async def spooky_avatar(self, ctx: commands.Context, member: discord.Member = None) -> None: - """This "spookifies" the given user's avatar, with a random *spooky* effect.""" - if member is None: - member = ctx.author - - member = await self._fetch_member(member.id) - if not member: - await ctx.send(f"{Emojis.cross_mark} Could not get member info.") - return - - async with ctx.typing(): - image_bytes = await member.avatar_url.read() - - file_name = file_safe_name("spooky_avatar", member.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_author(name=member.name, icon_url=member.avatar_url) - embed.set_image(url=f"attachment://{file_name}") - embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url) - - await ctx.send(file=file, embed=embed) - - -def setup(bot: commands.Bot) -> None: - """Load the PfpModify cog.""" - bot.add_cog(PfpModify(bot)) -- cgit v1.2.3 From 1e1e8aec0d943cecda46fa22300bab3cae108263 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 30 Apr 2021 10:04:07 +0100 Subject: Consistant use of double quotes and tuples in avatar_mod cog --- .../evergreen/avatar_modification/avatar_modify.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) (limited to 'bot') diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py index e1078fa0..095de306 100644 --- a/bot/exts/evergreen/avatar_modification/avatar_modify.py +++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py @@ -22,7 +22,7 @@ _EXECUTOR = ThreadPoolExecutor(10) FILENAME_STRING = "{effect}_{author}.png" -with open('bot/resources/pride/gender_options.json') as f: +with open("bot/resources/pride/gender_options.json") as f: GENDER_OPTIONS = json.load(f) @@ -48,10 +48,10 @@ def file_safe_name(effect: str, display_name: str) -> str: file_name = file_name.replace(" ", "_") # Normalize unicode characters - cleaned_filename = unicodedata.normalize('NFKD', file_name).encode('ASCII', 'ignore').decode() + cleaned_filename = unicodedata.normalize("NFKD", file_name).encode("ASCII", "ignore").decode() # Remove invalid filename characters - cleaned_filename = ''.join(c for c in cleaned_filename if c in valid_filename_chars) + cleaned_filename = "".join(c for c in cleaned_filename if c in valid_filename_chars) return cleaned_filename @@ -79,7 +79,7 @@ class AvatarModify(commands.Cog): return member - @commands.group(aliases=('avatar_mod', 'pfp_mod')) + @commands.group(aliases=("avatar_mod", "pfp_mod")) 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: @@ -114,7 +114,7 @@ class AvatarModify(commands.Cog): await ctx.send(embed=embed, file=file) - @avatar_modify.command(aliases=["easterify"], root_aliases=("easterify", "avatareasterify")) + @avatar_modify.command(aliases=("easterify",), root_aliases=("easterify", "avatareasterify")) async def avatareasterify(self, ctx: commands.Context, *colours: t.Union[discord.Colour, str]) -> None: """ This "Easterifies" the user's avatar. @@ -200,7 +200,7 @@ class AvatarModify(commands.Cog): await ctx.send(file=file, embed=embed) @avatar_modify.group( - aliases=["avatarpride", "pridepfp", "prideprofile"], + aliases=("avatarpride", "pridepfp", "prideprofile"), root_aliases=("prideavatar", "avatarpride", "pridepfp", "prideprofile"), invoke_without_command=True ) @@ -272,12 +272,11 @@ class AvatarModify(commands.Cog): await ctx.send(embed=embed) @avatar_modify.command( - name='spookyavatar', - aliases=('savatar', 'spookify'), - root_aliases=('spookyavatar', 'spookify', 'savatar'), - brief='Spookify an user\'s avatar.' + aliases=("savatar", "spookify"), + root_aliases=("spookyavatar", "spookify", "savatar"), + brief="Spookify an user's avatar." ) - async def spooky_avatar(self, ctx: commands.Context, member: discord.Member = None) -> None: + async def spookyavatar(self, ctx: commands.Context, member: discord.Member = None) -> None: """This "spookifies" the given user's avatar, with a random *spooky* effect.""" if member is None: member = ctx.author -- cgit v1.2.3 From 2bbbef35ebbb01e7671250f58c4a8e7543942b08 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 30 Apr 2021 10:09:37 +0100 Subject: Add non-underscore aliases for lazy people like me :) --- bot/exts/evergreen/avatar_modification/avatar_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py index 095de306..a111c480 100644 --- a/bot/exts/evergreen/avatar_modification/avatar_modify.py +++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py @@ -79,7 +79,7 @@ class AvatarModify(commands.Cog): return member - @commands.group(aliases=("avatar_mod", "pfp_mod")) + @commands.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: -- cgit v1.2.3 From 477a4d7e095c2cb5ca51339b3e75b287338e3e92 Mon Sep 17 00:00:00 2001 From: vcokltfre Date: Fri, 30 Apr 2021 17:32:53 +0100 Subject: fix: remove () from list of safe filename chars --- bot/exts/evergreen/avatar_modification/avatar_modify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'bot') diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py index a111c480..0baee8b2 100644 --- a/bot/exts/evergreen/avatar_modification/avatar_modify.py +++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py @@ -40,7 +40,7 @@ async def in_executor(func: t.Callable, *args) -> t.Any: def file_safe_name(effect: str, display_name: str) -> str: """Returns a file safe filename based on the given effect and display name.""" - valid_filename_chars = f"-_.() {string.ascii_letters}{string.digits}" + valid_filename_chars = f"-_. {string.ascii_letters}{string.digits}" file_name = FILENAME_STRING.format(effect=effect, author=display_name) -- cgit v1.2.3