aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts
diff options
context:
space:
mode:
Diffstat (limited to 'bot/exts')
-rw-r--r--bot/exts/core/extensions.py2
-rw-r--r--bot/exts/events/hacktoberfest/hacktober-issue-finder.py8
-rw-r--r--bot/exts/fun/hangman.py182
-rw-r--r--bot/exts/fun/quack.py75
-rw-r--r--bot/exts/holidays/halloween/spookyreact.py8
-rw-r--r--bot/exts/holidays/valentines/lovecalculator.py3
-rw-r--r--bot/exts/utilities/bookmark.py13
-rw-r--r--bot/exts/utilities/color.py471
-rw-r--r--bot/exts/utilities/emoji.py4
9 files changed, 279 insertions, 487 deletions
diff --git a/bot/exts/core/extensions.py b/bot/exts/core/extensions.py
index 424bacac..dbb9e069 100644
--- a/bot/exts/core/extensions.py
+++ b/bot/exts/core/extensions.py
@@ -18,7 +18,7 @@ from bot.utils.pagination import LinePaginator
log = logging.getLogger(__name__)
-UNLOAD_BLACKLIST = {f"{exts.__name__}.utils.extensions"}
+UNLOAD_BLACKLIST = {f"{exts.__name__}.core.extensions"}
BASE_PATH_LEN = len(exts.__name__.split("."))
diff --git a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py b/bot/exts/events/hacktoberfest/hacktober-issue-finder.py
index e3053851..088e7e43 100644
--- a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py
+++ b/bot/exts/events/hacktoberfest/hacktober-issue-finder.py
@@ -52,10 +52,10 @@ class HacktoberIssues(commands.Cog):
async def get_issues(self, ctx: commands.Context, option: str) -> Optional[dict]:
"""Get a list of the python issues with the label 'hacktoberfest' from the Github api."""
if option == "beginner":
- if (ctx.message.created_at - self.cache_timer_beginner).seconds <= 60:
+ if (ctx.message.created_at.replace(tzinfo=None) - self.cache_timer_beginner).seconds <= 60:
log.debug("using cache")
return self.cache_beginner
- elif (ctx.message.created_at - self.cache_timer_normal).seconds <= 60:
+ elif (ctx.message.created_at.replace(tzinfo=None) - self.cache_timer_normal).seconds <= 60:
log.debug("using cache")
return self.cache_normal
@@ -88,10 +88,10 @@ class HacktoberIssues(commands.Cog):
if option == "beginner":
self.cache_beginner = data
- self.cache_timer_beginner = ctx.message.created_at
+ self.cache_timer_beginner = ctx.message.created_at.replace(tzinfo=None)
else:
self.cache_normal = data
- self.cache_timer_normal = ctx.message.created_at
+ self.cache_timer_normal = ctx.message.created_at.replace(tzinfo=None)
return data
diff --git a/bot/exts/fun/hangman.py b/bot/exts/fun/hangman.py
new file mode 100644
index 00000000..a2c8c735
--- /dev/null
+++ b/bot/exts/fun/hangman.py
@@ -0,0 +1,182 @@
+from asyncio import TimeoutError
+from pathlib import Path
+from random import choice
+
+from discord import Embed, Message
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.constants import Colours, NEGATIVE_REPLIES
+
+# Defining all words in the list of words as a global variable
+ALL_WORDS = Path("bot/resources/fun/hangman_words.txt").read_text().splitlines()
+
+# Defining a dictionary of images that will be used for the game to represent the hangman person
+IMAGES = {
+ 6: "https://cdn.discordapp.com/attachments/859123972884922418/888133201497837598/hangman0.png",
+ 5: "https://cdn.discordapp.com/attachments/859123972884922418/888133595259084800/hangman1.png",
+ 4: "https://cdn.discordapp.com/attachments/859123972884922418/888134194474139688/hangman2.png",
+ 3: "https://cdn.discordapp.com/attachments/859123972884922418/888133758069395466/hangman3.png",
+ 2: "https://cdn.discordapp.com/attachments/859123972884922418/888133786724859924/hangman4.png",
+ 1: "https://cdn.discordapp.com/attachments/859123972884922418/888133828831477791/hangman5.png",
+ 0: "https://cdn.discordapp.com/attachments/859123972884922418/888133845449338910/hangman6.png",
+}
+
+
+class Hangman(commands.Cog):
+ """
+ Cog for the Hangman game.
+
+ Hangman is a classic game where the user tries to guess a word, with a limited amount of tries.
+ """
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+
+ @staticmethod
+ def create_embed(tries: int, user_guess: str) -> Embed:
+ """
+ Helper method that creates the embed where the game information is shown.
+
+ This includes how many letters the user has guessed so far, and the hangman photo itself.
+ """
+ hangman_embed = Embed(
+ title="Hangman",
+ color=Colours.python_blue,
+ )
+ hangman_embed.set_image(url=IMAGES[tries])
+ hangman_embed.add_field(
+ name=f"You've guessed `{user_guess}` so far.",
+ value="Guess the word by sending a message with a letter!"
+ )
+ hangman_embed.set_footer(text=f"Tries remaining: {tries}")
+ return hangman_embed
+
+ @commands.command()
+ async def hangman(
+ self,
+ ctx: commands.Context,
+ min_length: int = 0,
+ max_length: int = 25,
+ min_unique_letters: int = 0,
+ max_unique_letters: int = 25,
+ ) -> None:
+ """
+ Play hangman against the bot, where you have to guess the word it has provided!
+
+ The arguments for this command mean:
+ - min_length: the minimum length you want the word to be (i.e. 2)
+ - max_length: the maximum length you want the word to be (i.e. 5)
+ - min_unique_letters: the minimum unique letters you want the word to have (i.e. 4)
+ - max_unique_letters: the maximum unique letters you want the word to have (i.e. 7)
+ """
+ # Filtering the list of all words depending on the configuration
+ filtered_words = [
+ word for word in ALL_WORDS
+ if min_length < len(word) < max_length
+ and min_unique_letters < len(set(word)) < max_unique_letters
+ ]
+
+ if not filtered_words:
+ filter_not_found_embed = Embed(
+ title=choice(NEGATIVE_REPLIES),
+ description="No words could be found that fit all filters specified.",
+ color=Colours.soft_red,
+ )
+ await ctx.send(embed=filter_not_found_embed)
+ return
+
+ word = choice(filtered_words)
+ # `pretty_word` is used for comparing the indices where the guess of the user is similar to the word
+ # The `user_guess` variable is prettified by adding spaces between every dash, and so is the `pretty_word`
+ pretty_word = ''.join([f"{letter} " for letter in word])[:-1]
+ user_guess = ("_ " * len(word))[:-1]
+ tries = 6
+ guessed_letters = set()
+
+ def check(msg: Message) -> bool:
+ return msg.author == ctx.author and msg.channel == ctx.channel
+
+ original_message = await ctx.send(embed=Embed(
+ title="Hangman",
+ description="Loading game...",
+ color=Colours.soft_green
+ ))
+
+ # Game loop
+ while user_guess.replace(' ', '') != word:
+ # Edit the message to the current state of the game
+ await original_message.edit(embed=self.create_embed(tries, user_guess))
+
+ try:
+ message = await self.bot.wait_for(
+ event="message",
+ timeout=60.0,
+ check=check
+ )
+ except TimeoutError:
+ timeout_embed = Embed(
+ title=choice(NEGATIVE_REPLIES),
+ description="Looks like the bot timed out! You must send a letter within 60 seconds.",
+ color=Colours.soft_red,
+ )
+ await original_message.edit(embed=timeout_embed)
+ return
+
+ # If the user enters a capital letter as their guess, it is automatically converted to a lowercase letter
+ normalized_content = message.content.lower()
+ # The user should only guess one letter per message
+ if len(normalized_content) > 1:
+ letter_embed = Embed(
+ title=choice(NEGATIVE_REPLIES),
+ description="You can only send one letter at a time, try again!",
+ color=Colours.dark_green,
+ )
+ await ctx.send(embed=letter_embed, delete_after=4)
+ continue
+
+ # Checks for repeated guesses
+ elif normalized_content in guessed_letters:
+ already_guessed_embed = Embed(
+ title=choice(NEGATIVE_REPLIES),
+ description=f"You have already guessed `{normalized_content}`, try again!",
+ color=Colours.dark_green,
+ )
+ await ctx.send(embed=already_guessed_embed, delete_after=4)
+ continue
+
+ # Checks for correct guesses from the user
+ elif normalized_content in word:
+ positions = {idx for idx, letter in enumerate(pretty_word) if letter == normalized_content}
+ user_guess = "".join(
+ [normalized_content if index in positions else dash for index, dash in enumerate(user_guess)]
+ )
+
+ else:
+ tries -= 1
+
+ if tries <= 0:
+ losing_embed = Embed(
+ title="You lost.",
+ description=f"The word was `{word}`.",
+ color=Colours.soft_red,
+ )
+ await original_message.edit(embed=self.create_embed(tries, user_guess))
+ await ctx.send(embed=losing_embed)
+ return
+
+ guessed_letters.add(normalized_content)
+
+ # The loop exited meaning that the user has guessed the word
+ await original_message.edit(embed=self.create_embed(tries, user_guess))
+ win_embed = Embed(
+ title="You won!",
+ description=f"The word was `{word}`.",
+ color=Colours.grass_green
+ )
+ await ctx.send(embed=win_embed)
+
+
+def setup(bot: Bot) -> None:
+ """Load the Hangman cog."""
+ bot.add_cog(Hangman(bot))
diff --git a/bot/exts/fun/quack.py b/bot/exts/fun/quack.py
new file mode 100644
index 00000000..0c228aed
--- /dev/null
+++ b/bot/exts/fun/quack.py
@@ -0,0 +1,75 @@
+import logging
+import random
+from typing import Literal, Optional
+
+import discord
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.constants import Colours, NEGATIVE_REPLIES
+
+API_URL = 'https://quackstack.pythondiscord.com'
+
+log = logging.getLogger(__name__)
+
+
+class Quackstack(commands.Cog):
+ """Cog used for wrapping Quackstack."""
+
+ def __init__(self, bot: Bot):
+ self.bot = bot
+
+ @commands.command()
+ async def quack(
+ self,
+ ctx: commands.Context,
+ ducktype: Literal["duck", "manduck"] = "duck",
+ *,
+ seed: Optional[str] = None
+ ) -> None:
+ """
+ Use the Quackstack API to generate a random duck.
+
+ If a seed is provided, a duck is generated based on the given seed.
+ Either "duck" or "manduck" can be provided to change the duck type generated.
+ """
+ ducktype = ducktype.lower()
+ quackstack_url = f"{API_URL}/{ducktype}"
+ params = {}
+ if seed is not None:
+ try:
+ seed = int(seed)
+ except ValueError:
+ # We just need to turn the string into an integer any way possible
+ seed = int.from_bytes(seed.encode(), "big")
+ params["seed"] = seed
+
+ async with self.bot.http_session.get(quackstack_url, params=params) as response:
+ error_embed = discord.Embed(
+ title=random.choice(NEGATIVE_REPLIES),
+ description="The request failed. Please try again later.",
+ color=Colours.soft_red,
+ )
+ if response.status != 200:
+ log.error(f"Response to Quackstack returned code {response.status}")
+ await ctx.send(embed=error_embed)
+ return
+
+ data = await response.json()
+ file = data["file"]
+
+ embed = discord.Embed(
+ title=f"Quack! Here's a {ducktype} for you.",
+ description=f"A {ducktype} from Quackstack.",
+ color=Colours.grass_green,
+ url=f"{API_URL}/docs"
+ )
+
+ embed.set_image(url=API_URL + file)
+
+ await ctx.send(embed=embed)
+
+
+def setup(bot: Bot) -> None:
+ """Loads the Quack cog."""
+ bot.add_cog(Quackstack(bot))
diff --git a/bot/exts/holidays/halloween/spookyreact.py b/bot/exts/holidays/halloween/spookyreact.py
index 25e783f4..e228b91d 100644
--- a/bot/exts/holidays/halloween/spookyreact.py
+++ b/bot/exts/holidays/halloween/spookyreact.py
@@ -47,12 +47,12 @@ class SpookyReact(Cog):
Short-circuit helper check.
Return True if:
- * author is the bot
+ * author is a bot
* prefix is not None
"""
- # Check for self reaction
- if message.author == self.bot.user:
- log.debug(f"Ignoring reactions on self message. Message ID: {message.id}")
+ # Check if message author is a bot
+ if message.author.bot:
+ log.debug(f"Ignoring reactions on bot message. Message ID: {message.id}")
return True
# Check for command invocation
diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py
index 3999db2b..a53014e5 100644
--- a/bot/exts/holidays/valentines/lovecalculator.py
+++ b/bot/exts/holidays/valentines/lovecalculator.py
@@ -74,7 +74,8 @@ class LoveCalculator(Cog):
# We need the -1 due to how bisect returns the point
# see the documentation for further detail
# https://docs.python.org/3/library/bisect.html#bisect.bisect
- index = bisect.bisect(LOVE_DATA, (love_percent,)) - 1
+ love_threshold = [threshold for threshold, _ in LOVE_DATA]
+ index = bisect.bisect(love_threshold, love_percent) - 1
# We already have the nearest "fit" love level
# We only need the dict, so we can ditch the first element
_, data = LOVE_DATA[index]
diff --git a/bot/exts/utilities/bookmark.py b/bot/exts/utilities/bookmark.py
index a91ef1c0..a11c366b 100644
--- a/bot/exts/utilities/bookmark.py
+++ b/bot/exts/utilities/bookmark.py
@@ -7,7 +7,7 @@ import discord
from discord.ext import commands
from bot.bot import Bot
-from bot.constants import Categories, Colours, ERROR_REPLIES, Icons, WHITELISTED_CHANNELS
+from bot.constants import Colours, ERROR_REPLIES, Icons, Roles
from bot.utils.converters import WrappedMessageConverter
from bot.utils.decorators import whitelist_override
@@ -16,7 +16,6 @@ log = logging.getLogger(__name__)
# Number of seconds to wait for other users to bookmark the same message
TIMEOUT = 120
BOOKMARK_EMOJI = "📌"
-WHITELISTED_CATEGORIES = (Categories.help_in_use,)
class Bookmark(commands.Cog):
@@ -87,8 +86,8 @@ class Bookmark(commands.Cog):
await message.add_reaction(BOOKMARK_EMOJI)
return message
- @whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES)
@commands.command(name="bookmark", aliases=("bm", "pin"))
+ @whitelist_override(roles=(Roles.everyone,))
async def bookmark(
self,
ctx: commands.Context,
@@ -99,7 +98,13 @@ class Bookmark(commands.Cog):
"""Send the author a link to `target_message` via DMs."""
if not target_message:
if not ctx.message.reference:
- raise commands.UserInputError("You must either provide a valid message to bookmark, or reply to one.")
+ raise commands.UserInputError(
+ "You must either provide a valid message to bookmark, or reply to one."
+ "\n\nThe lookup strategy for a message is as follows (in order):"
+ "\n1. Lookup by '{channel ID}-{message ID}' (retrieved by shift-clicking on 'Copy ID')"
+ "\n2. Lookup by message ID (the message **must** have been sent after the bot last started)"
+ "\n3. Lookup by message URL"
+ )
target_message = ctx.message.reference.resolved
# Prevent users from bookmarking a message in a channel they don't have access to
diff --git a/bot/exts/utilities/color.py b/bot/exts/utilities/color.py
deleted file mode 100644
index c1523281..00000000
--- a/bot/exts/utilities/color.py
+++ /dev/null
@@ -1,471 +0,0 @@
-import colorsys
-import json
-import logging
-import re
-from io import BytesIO
-
-from discord import Embed, File
-from discord.ext import commands
-from PIL import Image, ImageColor
-from rapidfuzz import process
-
-from bot.bot import Bot
-from bot.constants import Colours
-
-# from bot.exts.core.extension import invoke_help_command
-
-
-logger = logging.getLogger(__name__)
-
-
-ERROR_MSG = """The color code {user_color} is not a possible color combination.
-The range of possible values are:
-RGB & HSV: 0-255
-CMYK: 0-100%
-HSL: 0-360 degrees
-Hex: #000000-#FFFFFF
-"""
-
-with open("bot/resources/utilities/ryanzec_colours.json") as f:
- COLOR_MAPPING = json.load(f)
-
-
-THUMBNAIL_SIZE = 80
-
-"""
-class Colour(commands.Cog):
-
- def __init__(self, bot: Bot) -> None:
- self.bot = bot
-
- @commands.group(aliases=["color"])
- async def colour(self, ctx: commands.Context) -> None:
- if ctx.invoked_subcommand is None:
- await invoke_help_command(ctx)
-
- @colour.command()
- async def rgb(self, ctx: commands.Context, red: int, green: int, blue: int) -> None:
- rgb_tuple = ImageColor.getrgb(f"rgb({red}, {green}, {blue})")
- await Colour.send_colour_response(ctx, list(rgb_tuple))
-
- @colour.command()
- async def hsv(self, ctx: commands.Context, hue: int, saturation: int, value: int) -> None:
- hsv_tuple = ImageColor.getrgb(f"hsv({hue}, {saturation}%, {value}%)")
- await Colour.send_colour_response(ctx, list(hsv_tuple))
-
- @colour.command()
- async def hsl(self, ctx: commands.Context, hue: int, saturation: int, lightness: int) -> None:
- hsl_tuple = ImageColor.getrgb(f"hsl({hue}, {saturation}%, {lightness}%)")
- await Colour.send_colour_response(ctx, list(hsl_tuple))
-
- @colour.command()
- async def cmyk(self, ctx: commands.Context, cyan: int, yellow: int, magenta: int, key: int) -> None:
- ...
-
- @colour.command()
- async def hex(self, ctx: commands.Context, hex_code: str) -> None:
- hex_tuple = ImageColor.getrgb(hex_code)
- await Colour.send_colour_response(ctx, list(hex_tuple))
-
- @colour.command()
- async def yiq(
- self,
- ctx: commands.Context,
- perceived_luminesence: int,
- in_phase: int,
- quadrature: int
- ) -> None:
- yiq_list = list(colorsys.yiq_to_rgb(perceived_luminesence, in_phase, quadrature))
- yiq_tuple = [int(val * 255.0) for val in yiq_list]
- await Colour.send_colour_response(ctx, list(yiq_tuple))
-
- @staticmethod
- async def send_colour_response(ctx: commands.Context, rgb: list[int]) -> Message:
- r, g, b = rgb[0], rgb[1], rgb[2]
- colour_embed = Embed(
- title="Colour",
- description="Here lies thy colour",
- colour=int(f"{r:02x}{g:02x}{b:02x}", 16)
- )
- colour_conversions = Colour.get_colour_conversions(rgb)
- for colour_space, value in colour_conversions.items():
- colour_embed.add_field(
- name=colour_space.upper(),
- value=f"`{value}`",
- inline=True
- )
-
- thumbnail = Image.new("RGB", (THUMBNAIL_SIZE, THUMBNAIL_SIZE), color=tuple(rgb))
- buffer = BytesIO()
- thumbnail.save(buffer, "PNG")
- buffer.seek(0)
- thumbnail_file = File(buffer, filename="colour.png")
-
- colour_embed.set_thumbnail(url="attachment://colour.png")
-
- await ctx.send(file=thumbnail_file, embed=colour_embed)
-
- @staticmethod
- def get_colour_conversions(rgb: list[int]) -> dict[str, str]:
- return {
- "rgb": tuple(rgb),
- "hsv": Colour._rgb_to_hsv(rgb),
- "hsl": Colour._rgb_to_hsl(rgb),
- "cmyk": Colour._rgb_to_cmyk(rgb),
- "hex": Colour._rgb_to_hex(rgb),
- "yiq": Colour._rgb_to_yiq(rgb)
- }
-
- @staticmethod
- def _rgb_to_hsv(rgb: list[int]) -> tuple[int, int, int]:
- rgb = [val / 255.0 for val in rgb]
- h, v, s = colorsys.rgb_to_hsv(*rgb)
- hsv = (round(h * 360), round(s * 100), round(v * 100))
- return hsv
-
- @staticmethod
- def _rgb_to_hsl(rgb: list[int]) -> tuple[int, int, int]:
- rgb = [val / 255.0 for val in rgb]
- h, l, s = colorsys.rgb_to_hls(*rgb)
- hsl = (round(h * 360), round(s * 100), round(l * 100))
- return hsl
-
- @staticmethod
- def _rgb_to_cmyk(rgb: list[int]) -> tuple[int, int, int, int]:
- rgb = [val / 255.0 for val in rgb]
-
- if all(val == 0 for val in rgb):
- return 0, 0, 0, 100
-
- cmy = [1 - val / 255 for val in rgb]
- min_cmy = min(cmy)
-
- cmyk = [(val - min_cmy) / (1 - min_cmy) for val in cmy] + [min_cmy]
- cmyk = [round(val * 100) for val in cmyk]
-
- return tuple(cmyk)
-
- @staticmethod
- def _rgb_to_hex(rgb: list[int]) -> str:
- hex_ = ''.join([hex(val)[2:].zfill(2) for val in rgb])
- hex_code = f"#{hex_}".upper()
- return hex_code
-
- @staticmethod
- def _rgb_to_yiq(rgb: list[int]) -> tuple[int, int, int, int]:
- rgb = [val / 255.0 for val in rgb]
- y, i, q = colorsys.rgb_to_yiq(*rgb)
- yiq = (round(y), round(i), round(q))
- return yiq
-
-def setup(bot: commands.Bot) -> None:
- bot.add_cog(Colour(bot))
-"""
-
-
-class Color(commands.Cog):
- """User initiated commands to receive color information."""
-
- def __init__(self, bot: Bot):
- self.bot = bot
-
- @commands.command(aliases=["colour"])
- async def color(self, ctx: commands.Context, mode: str, *, user_color: str) -> None:
- """
- Send information on input color code or color name.
-
- Possible modes are: "hex", "rgb", "hsv", "hsl", "cmyk" or "name".
- """
- logger.debug(f"{mode = }")
- logger.debug(f"{user_color = }")
- if mode.lower() == "hex":
- await self.hex_to_rgb(ctx, user_color)
- elif mode.lower() == "rgb":
- rgb_color = self.tuple_create(user_color)
- await self.color_embed(ctx, rgb_color)
- elif mode.lower() == "hsv":
- await self.hsv_to_rgb(ctx, user_color)
- elif mode.lower() == "hsl":
- await self.hsl_to_rgb(ctx, user_color)
- elif mode.lower() == "cmyk":
- await self.cmyk_to_rgb(ctx, user_color)
- elif mode.lower() == "name":
- color_name, hex_color = self.match_color_name(user_color)
- if "#" in hex_color:
- rgb_color = ImageColor.getcolor(hex_color, "RGB")
- else:
- rgb_color = ImageColor.getcolor("#" + hex_color, "RGB")
- await self.color_embed(ctx, rgb_color, color_name)
- else:
- # mode is either None or an invalid code
- if mode is None:
- no_mode_embed = Embed(
- title="No mode was passed, please define a color code.",
- description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.",
- color=Colours.soft_red,
- )
- await ctx.send(embed=no_mode_embed)
- return
- wrong_mode_embed = Embed(
- title=f"The color code {mode} is not a valid option",
- description="Possible modes are: Name, Hex, RGB, HSV, HSL and CMYK.",
- color=Colours.soft_red,
- )
- await ctx.send(embed=wrong_mode_embed)
- return
-
- @staticmethod
- def tuple_create(input_color: str) -> tuple[int, int, int]:
- """
- Create a tuple of integers based on user's input.
-
- Can handle inputs of the types:
- (100, 100, 100)
- 100, 100, 100
- 100 100 100
- """
- if "(" in input_color:
- remove = "[() ]"
- color_tuple = re.sub(remove, "", input_color)
- color_tuple = tuple(map(int, color_tuple.split(",")))
- elif "," in input_color:
- color_tuple = tuple(map(int, input_color.split(",")))
- else:
- color_tuple = tuple(map(int, input_color.split(" ")))
- return color_tuple
-
- async def hex_to_rgb(self, ctx: commands.Context, hex_string: str) -> None:
- """Function to convert hex color to rgb color and send main embed."""
- hex_match = re.fullmatch(r"(#?[0x]?)((?:[0-9a-fA-F]{3}){1,2})", hex_string)
- if hex_match:
- if "#" in hex_string:
- rgb_color = ImageColor.getcolor(hex_string, "RGB")
- elif "0x" in hex_string:
- hex_ = hex_string.replace("0x", "#")
- rgb_color = ImageColor.getcolor(hex_, "RGB")
- else:
- hex_ = "#" + hex_string
- rgb_color = ImageColor.getcolor(hex_, "RGB")
- await self.color_embed(ctx, rgb_color)
- else:
- await ctx.send(
- embed=Embed(
- title="There was an issue converting the hex color code.",
- description=ERROR_MSG.format(user_color=hex_string),
- )
- )
-
- async def hsv_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int]) -> tuple[int, int, int]:
- """Function to convert hsv color to rgb color and send main embed."""
- input_color = self.tuple_create(input_color)
- (h, v, s) = input_color # the function hsv_to_rgb expects v and s to be swapped
- h = h / 360
- s = s / 100
- v = v / 100
- rgb_color = colorsys.hsv_to_rgb(h, s, v)
- (r, g, b) = rgb_color
- r = int(r * 255)
- g = int(g * 255)
- b = int(b * 255)
- await self.color_embed(ctx, (r, g, b))
-
- async def hsl_to_rgb(self, ctx: commands.Context, input_color: tuple[int, int, int]) -> tuple[int, int, int]:
- """Function to convert hsl color to rgb color and send main embed."""
- input_color = self.tuple_create(input_color)
- (h, s, l) = input_color
- h = h / 360
- s = s / 100
- l = l / 100 # noqa: E741 It's little `L`, Reason: To maintain consistency.
- rgb_color = colorsys.hls_to_rgb(h, l, s)
- (r, g, b) = rgb_color
- r = int(r * 255)
- g = int(g * 255)
- b = int(b * 255)
- await self.color_embed(ctx, (r, g, b))
-
- async def cmyk_to_rgb(
- self,
- ctx: commands.Context,
- input_color: tuple[int, int, int, int]
- ) -> tuple[int, int, int]:
- """Function to convert cmyk color to rgb color and send main embed."""
- input_color = self.tuple_create(input_color)
- c = input_color[0]
- m = input_color[1]
- y = input_color[2]
- k = input_color[3]
- r = int(255 * (1.0 - c / float(100)) * (1.0 - k / float(100)))
- g = int(255 * (1.0 - m / float(100)) * (1.0 - k / float(100)))
- b = int(255 * (1.0 - y / float(100)) * (1.0 - k / float(100)))
- await self.color_embed(ctx, (r, g, b))
-
- @staticmethod
- async def create_thumbnail_attachment(color: tuple[int, int, int]) -> File:
- """
- Generate a thumbnail from `color`.
-
- Assumes that color is an rgb tuple.
- """
- thumbnail = Image.new("RGB", (80, 80), color=color)
- bufferedio = BytesIO()
- thumbnail.save(bufferedio, format="PNG")
- bufferedio.seek(0)
-
- file = File(bufferedio, filename="color.png")
-
- return file
-
- @staticmethod
- def get_color_fields(rgb_color: tuple[int, int, int]) -> list[dict]:
- """Converts from `RGB` to `CMYK`, `HSV`, `HSL` and returns a list of fields."""
-
- def _rgb_to_hex(rgb_color: tuple[int, int, int]) -> str:
- """To convert from `RGB` to `Hex` notation."""
- return '#' + ''.join(hex(int(color))[2:].zfill(2) for color in rgb_color).upper()
-
- def _rgb_to_cmyk(rgb_color: tuple[int, int, int]) -> tuple[int, int, int, int]:
- """To convert from `RGB` to `CMYK` color space."""
- r, g, b = rgb_color
-
- # RGB_SCALE -> 255
- # CMYK_SCALE -> 100
-
- if (r == g == b == 0):
- return 0, 0, 0, 100 # Representing Black
-
- # rgb [0,RGB_SCALE] -> cmy [0,1]
- c = 1 - r / 255
- m = 1 - g / 255
- y = 1 - b / 255
-
- # extract out k [0, 1]
- min_cmy = min(c, m, y)
- c = (c - min_cmy) / (1 - min_cmy)
- m = (m - min_cmy) / (1 - min_cmy)
- y = (y - min_cmy) / (1 - min_cmy)
- k = min_cmy
-
- # rescale to the range [0,CMYK_SCALE] and round off
- c = round(c * 100)
- m = round(m * 100)
- y = round(y * 100)
- k = round(k * 100)
-
- return c, m, y, k
-
- def _rgb_to_hsv(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]:
- """To convert from `RGB` to `HSV` color space."""
- r, g, b = rgb_color
- h, v, s = colorsys.rgb_to_hsv(r / float(255), g / float(255), b / float(255))
- h = round(h * 360)
- s = round(s * 100)
- v = round(v * 100)
- return h, s, v
-
- def _rgb_to_hsl(rgb_color: tuple[int, int, int]) -> tuple[int, int, int]:
- """To convert from `RGB` to `HSL` color space."""
- r, g, b = rgb_color
- h, l, s = colorsys.rgb_to_hls(r / float(255), g / float(255), b / float(255))
- h = round(h * 360)
- s = round(s * 100)
- l = round(l * 100) # noqa: E741 It's little `L`, Reason: To maintain consistency.
- return h, s, l
-
- all_fields = [
- {
- "name": "RGB",
- "value": f"» rgb {rgb_color}"
- },
- {
- "name": "HEX",
- "value": f"» hex {_rgb_to_hex(rgb_color)}"
- },
- {
- "name": "CMYK",
- "value": f"» cmyk {_rgb_to_cmyk(rgb_color)}"
- },
- {
- "name": "HSV",
- "value": f"» hsv {_rgb_to_hsv(rgb_color)}"
- },
- {
- "name": "HSL",
- "value": f"» hsl {_rgb_to_hsl(rgb_color)}"
- },
- ]
-
- return all_fields
-
- @staticmethod
- def match_color_name(input_color_name: str) -> str:
- """Use fuzzy matching to return a hex color code based on the user's input."""
- try:
- match, certainty, _ = process.extractOne(
- query=input_color_name,
- choices=COLOR_MAPPING.keys(),
- score_cutoff=50
- )
- logger.debug(f"{match = }, {certainty = }")
- hex_match = COLOR_MAPPING[match]
- logger.debug(f"{hex_match = }")
- except TypeError:
- match = "No color name match found."
- hex_match = input_color_name
-
- return match, hex_match
-
- @staticmethod
- def match_color_hex(input_hex_color: str) -> str:
- """Use fuzzy matching to return a hex color code based on the user's input."""
- try:
- match, certainty, _ = process.extractOne(
- query=input_hex_color,
- choices=COLOR_MAPPING.values(),
- score_cutoff=80
- )
- logger.debug(f"{match = }, {certainty = }")
- color_name = [name for name, _ in COLOR_MAPPING.items() if _ == match][0]
- logger.debug(f"{color_name = }")
- except TypeError:
- color_name = "No color name match found."
-
- return color_name
-
- async def color_embed(
- self,
- ctx: commands.Context,
- rgb_color: tuple[int, int, int],
- color_name: str = None
- ) -> None:
- """Take a RGB color tuple, create embed, and send."""
- (r, g, b) = rgb_color
- discord_rgb_int = int(f"{r:02x}{g:02x}{b:02x}", 16)
- all_colors = self.get_color_fields(rgb_color)
- hex_color = all_colors[1]["value"].replace("» hex ", "")
- if color_name is None:
- logger.debug(f"Find color name from hex color: {hex_color}")
- color_name = self.match_color_hex(hex_color)
-
- async with ctx.typing():
- main_embed = Embed(
- title=color_name,
- description='(Approx..)',
- color=discord_rgb_int,
- )
-
- file = await self.create_thumbnail_attachment(rgb_color)
- main_embed.set_thumbnail(url="attachment://color.png")
-
- for field in all_colors:
- main_embed.add_field(
- name=field['name'],
- value=field['value'],
- inline=False,
- )
-
- await ctx.send(file=file, embed=main_embed)
-
-
-def setup(bot: Bot) -> None:
- """Load the Color Cog."""
- bot.add_cog(Color(bot))
diff --git a/bot/exts/utilities/emoji.py b/bot/exts/utilities/emoji.py
index 55d6b8e9..83df39cc 100644
--- a/bot/exts/utilities/emoji.py
+++ b/bot/exts/utilities/emoji.py
@@ -107,8 +107,8 @@ class Emojis(commands.Cog):
title=f"Emoji Information: {emoji.name}",
description=textwrap.dedent(f"""
**Name:** {emoji.name}
- **Created:** {time_since(emoji.created_at, precision="hours")}
- **Date:** {datetime.strftime(emoji.created_at, "%d/%m/%Y")}
+ **Created:** {time_since(emoji.created_at.replace(tzinfo=None), precision="hours")}
+ **Date:** {datetime.strftime(emoji.created_at.replace(tzinfo=None), "%d/%m/%Y")}
**ID:** {emoji.id}
"""),
color=Color.blurple(),