aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar bradtimmis <[email protected]>2021-10-24 18:26:45 -0400
committerGravatar bradtimmis <[email protected]>2021-10-24 18:26:45 -0400
commit46620dd929b8359af1c086b3e9b6e23633398f6e (patch)
tree84a2b78c23709b5a03d10a72e9e2f244a82e5dbb
parenttemp: add restructured template as comments (diff)
parentchore: 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.yml5
-rwxr-xr-xREADME.md1
-rw-r--r--bot/constants.py3
-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
-rw-r--r--bot/resources/fun/hangman_words.txt877
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"
diff --git a/README.md b/README.md
index dd8301dc..2e2b7aec 100755
--- a/README.md
+++ b/README.md
@@ -4,6 +4,7 @@
[![Lint Badge][1]][2]
[![Build Badge][3]][4]
[![License](https://img.shields.io/badge/license-MIT-green)](LICENSE)
+[![Open in Gitpod](https://img.shields.io/badge/Gitpod-ready--to--code-908a85?logo=gitpod)](https://gitpod.io/#/github.com/python-discord/sir-lancebot)
![Header](sir-lancebot-logo.png)
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