diff options
Diffstat (limited to 'bot/exts')
| -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/emoji.py | 4 |
8 files changed, 279 insertions, 16 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/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(), |