diff options
| author | 2021-10-05 11:48:17 -0400 | |
|---|---|---|
| committer | 2021-10-05 11:48:17 -0400 | |
| commit | 878423ff5cf6699bc29d49a887d8e7ca50933a47 (patch) | |
| tree | 7efb0a49bf0a9ed3a95209f122b29c2178a46f54 /bot/exts | |
| parent | chore: remove single-use constant for json path (diff) | |
| parent | chore: remove single-use constant for json path (diff) | |
Merge branch 'color-677' of https://github.com/brad90four/sir-lancebot into color-677
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(), | 
