diff options
author | 2021-10-24 18:26:45 -0400 | |
---|---|---|
committer | 2021-10-24 18:26:45 -0400 | |
commit | 46620dd929b8359af1c086b3e9b6e23633398f6e (patch) | |
tree | 84a2b78c23709b5a03d10a72e9e2f244a82e5dbb | |
parent | temp: add restructured template as comments (diff) | |
parent | chore: remove single-use constant for json path (diff) |
fix: merge conflicts
Merge branch 'color-677' of github.com:brad90four/sir-lancebot into color-677
-rw-r--r-- | .gitpod.yml | 5 | ||||
-rwxr-xr-x | README.md | 1 | ||||
-rw-r--r-- | bot/constants.py | 3 | ||||
-rw-r--r-- | bot/exts/core/extensions.py | 2 | ||||
-rw-r--r-- | bot/exts/events/hacktoberfest/hacktober-issue-finder.py | 8 | ||||
-rw-r--r-- | bot/exts/fun/hangman.py | 182 | ||||
-rw-r--r-- | bot/exts/fun/quack.py | 75 | ||||
-rw-r--r-- | bot/exts/holidays/halloween/spookyreact.py | 8 | ||||
-rw-r--r-- | bot/exts/holidays/valentines/lovecalculator.py | 3 | ||||
-rw-r--r-- | bot/exts/utilities/bookmark.py | 13 | ||||
-rw-r--r-- | bot/exts/utilities/color.py | 471 | ||||
-rw-r--r-- | bot/exts/utilities/emoji.py | 4 | ||||
-rw-r--r-- | bot/resources/fun/hangman_words.txt | 877 |
13 files changed, 1164 insertions, 488 deletions
diff --git a/.gitpod.yml b/.gitpod.yml new file mode 100644 index 00000000..a10e6e26 --- /dev/null +++ b/.gitpod.yml @@ -0,0 +1,5 @@ +tasks: + - name: "Python Environment" + before: "pyenv install 3.9.6 && pyenv global 3.9.6" + init: "pip install poetry && export PIP_USER=false" + command: "poetry install && poetry run pre-commit install" @@ -4,6 +4,7 @@ [![Lint Badge][1]][2] [![Build Badge][3]][4] [](LICENSE) +[](https://gitpod.io/#/github.com/python-discord/sir-lancebot)  diff --git a/bot/constants.py b/bot/constants.py index 2313bfdb..6e45632f 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -280,11 +280,12 @@ if Client.month_override is not None: class Roles(NamedTuple): + owner = 267627879762755584 admin = int(environ.get("BOT_ADMIN_ROLE_ID", 267628507062992896)) moderator = 267629731250176001 - owner = 267627879762755584 helpers = int(environ.get("ROLE_HELPERS", 267630620367257601)) core_developers = 587606783669829632 + everyone = int(environ.get("BOT_GUILD", 267624335836053506)) class Tokens(NamedTuple): 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(), diff --git a/bot/resources/fun/hangman_words.txt b/bot/resources/fun/hangman_words.txt new file mode 100644 index 00000000..5e20bfde --- /dev/null +++ b/bot/resources/fun/hangman_words.txt @@ -0,0 +1,877 @@ +abandon +ability +able +about +above +accept +according +account +across +action +activity +actually +address +administration +admit +adult +affect +after +again +against +agency +agent +agree +agreement +ahead +allow +almost +alone +along +already +also +although +always +among +amount +analysis +animal +another +answer +anyone +anything +appear +apply +approach +area +argue +around +arrive +article +artist +assume +attack +attention +attorney +audience +author +authority +available +avoid +away +baby +back +ball +bank +base +beat +beautiful +because +become +before +begin +behavior +behind +believe +benefit +best +better +between +beyond +bill +billion +black +blood +blue +board +body +book +born +both +break +bring +brother +budget +build +building +business +bytecode +call +camera +campaign +cancer +candidate +capital +card +care +career +carry +case +catch +cause +cell +center +central +century +certain +certainly +chair +challenge +chance +change +character +charge +check +child +choice +choose +church +citizen +city +civil +claim +class +clear +clearly +close +coach +cold +collection +college +color +come +commercial +common +community +company +compare +computer +concern +condition +conference +consider +consumer +contain +continue +control +cost +could +country +couple +course +court +cover +create +crime +cultural +culture +current +customer +dark +data +daughter +dead +deal +death +debate +decade +decide +decision +deep +defense +degree +describe +design +despite +detail +determine +develop +development +dictionary +difference +different +difficult +dinner +direction +director +discover +discuss +discussion +disease +doctor +door +down +draw +dream +drive +drop +during +each +early +east +easy +economic +economy +edge +education +effect +effort +eight +either +election +else +employee +energy +enjoy +enough +enter +entire +environment +environmental +especially +establish +even +evening +event +ever +every +everybody +everyone +everything +evidence +exactly +example +executive +exist +expect +experience +expert +explain +face +fact +factor +fall +false +family +fast +father +fear +federal +feel +feeling +field +fight +figure +fill +film +final +finally +financial +find +fine +finger +finish +fire +firm +first +fish +five +floor +focus +follow +food +foot +force +foreign +forget +form +former +forward +four +free +friend +from +front +full +function +fund +future +game +garden +general +generation +girl +give +glass +goal +good +government +great +green +ground +group +grow +growth +guess +guido +hair +half +hand +hang +happen +happy +hard +have +head +health +hear +heart +heat +heavy +help +here +herself +high +himself +history +hold +home +hope +hospital +hotel +hour +house +however +huge +human +hundred +husband +idea +identify +image +imagine +impact +import +important +improve +include +including +increase +indeed +indicate +individual +industry +information +inside +instead +institution +interest +interesting +international +interpreter +interview +into +investment +involve +issue +item +itself +join +just +keep +kill +kind +kitchen +know +knowledge +land +lambda +language +large +last +late +later +laugh +lawyer +lead +leader +learn +least +leave +left +legal +less +letter +level +life +light +like +likely +line +list +listen +little +live +local +long +look +loop +lose +loss +love +machine +magazine +main +maintain +major +majority +make +manage +management +manager +many +market +marriage +material +matter +maybe +mean +measure +media +medical +meet +meeting +member +memory +mention +message +method +middle +might +military +million +mind +minute +miss +mission +model +modern +moment +money +month +more +morning +most +mother +mouth +move +movement +movie +much +music +must +myself +name +nation +national +natural +nature +near +nearly +necessary +need +network +never +news +newspaper +next +nice +nightnone +north +note +nothing +notice +number +object +occur +offer +office +officer +official +often +once +only +onto +open +operation +opportunity +option +order +organization +other +others +outside +over +owner +page +pain +painting +paper +parameters +parent +part +participant +particular +particularly +partner +party +pass +past +patient +pattern +peace +people +perform +performance +perhaps +period +person +personal +phone +physical +pick +picture +piece +place +plan +plant +play +player +point +police +policy +political +politics +poor +popular +population +position +positive +possible +power +practice +prepare +present +president +pressure +pretty +prevent +price +print +private +probably +problem +process +produce +product +production +professional +professor +program +project +property +protect +prove +provide +public +pull +purpose +push +pydis +pygame +python +quality +question +quickly +quite +race +radio +raise +range +rate +rather +reach +read +ready +real +reality +realize +really +reason +receive +recent +recently +recognize +record +reduce +reflect +region +relate +relationship +remain +remember +remove +report +represent +require +research +resource +respond +response +responsibility +rest +result +return +reveal +rich +right +rise +risk +road +rock +role +room +rule +safe +same +save +scene +school +science +scientist +score +season +seat +second +section +security +seek +seem +sell +send +senior +sense +series +serious +serve +service +seven +several +shake +share +shoot +short +shot +should +shoulder +show +side +sign +significant +similar +simple +simply +since +sing +single +sister +site +situation +size +skill +skin +small +smile +social +society +soldier +some +somebody +someone +something +sometimes +song +soon +sort +sound +source +south +southern +space +speak +special +specific +speech +spend +sport +spring +staff +stage +stand +standard +star +start +state +statement +station +stay +step +still +stock +stop +store +story +strategy +street +strong +structure +student +study +stuff +style +subject +success +successful +such +suddenly +suffer +suggest +summer +support +sure +surface +system +table +take +talk +task +teach +teacher +team +technology +television +tell +tend +term +test +than +thank +that +their +them +themselves +then +theory +there +these +they +thing +think +third +this +those +though +thought +thousand +threat +three +through +throughout +throw +thus +time +today +together +tonight +total +tough +toward +town +trade +traditional +training +travel +treat +treatment +tree +trial +trip +trouble +true +truth +turn +type +under +understand +unit +until +upon +usually +value +variable +various +very +victim +view +visit +voice +vote +wait +walk +wall +want +watch +water +wear +week +weight +well +west +western +what +whatever +when +where +whether +which +while +white +whole +whom +whose +wide +wife +will +wind +window +wish +with +within +without +woman +wonder +word +work +worker +world +worry +would +write +writer +wrong +yard +yield +yeah +year +young +your +yourself |