diff options
| author | 2021-10-13 17:23:30 -0400 | |
|---|---|---|
| committer | 2021-10-13 21:23:30 +0000 | |
| commit | a0c86e72c0a418f1265a1aa035b45048d8e921ec (patch) | |
| tree | aa64807399a4ccece4458dfba06f2186c7f45d7f /bot | |
| parent | Merge pull request #904 from python-discord/update-advent-of-code-channel-ids (diff) | |
Challenges (#860)
* beginning commit
creating the base of the hangman, code needs to be linted in the future
* updated words list
* adding images to show the hangman person
* added images, though it is a bit laggy
* replacing images with discord attachment urls
* adding error if filters aren't found
* fixing typo in ``filter_not_found_embed``
* final lints + removing `mode` parameter as it renders useless
* linting flake8 errors
* adding newline at the end of `top_1000_used_words.txt`
* minor change to filter message
* beginning commit -- trying to add bs4 to pyproject.toml, though it is currently failing
* kata information section done, ready for issue
* fixing bugs with the query not being fully picked up, also allowing query only with no kyu
* fixing bug where user cannot leave all arguments blank
* typo - forgot unary before the level within the `language and not query` if statement
* changing to random kata chosen
* ensuring that if the user provides a query that won't work, that it won't error out
* limiting choice to smaller numbers if a query was provided, so the user gets what they want
* improving hangman docstring
* removing `bot/resources/evergreen/hangman` directory as file attachments are used
* replacing single quotes with double quotes, to adhere to the style guide.
* fixing style inconsistencies and other problems with how the code looks - as per requested by Objectivix
* fixing `IMAGES` style inconsistency
* adding trailing commas and switching to `Colours` for consistency
* adding trailing commas and switching to `Colours` for consistency
* fixing the remnants of non-trailing commas and allowing specification for single player vs mulitplayer
* removing all 2 letter words from the hangman word choosing and removing words that @Objectivix found that shouldn't be in the list of words
* removing some inappropriate words from the txt file
* Adding space for grammatical errors
Co-authored-by: ChrisJL <[email protected]>
* changing two periods to a full stop & wrapping try and except block to only the part that can raise it
* using negative replies instead along with fixing grammatical errors in the sentence
* removing words that could be considered inappropirate
* removing `TOP_WORDS_FILE_PATH` and making `ALL_WORDS` a global variable.
* error handling
* fixing the overcomplication of the bs4 portion
* adding button and dropdowns to the challenges command
* more specific docstring
* more specific docstring
* finishing dropdowns/buttons
* putting the dropdown on top of the link button
* replacing ' with a double quote for some strings
* Removing more words
The words removed shouldn't really belong here
* Update bot/exts/utilities/challenges.py
Co-authored-by: Bluenix <[email protected]>
* replacing mapping_of_images with IMAGES and other fixes
* Dedenting
Co-authored-by: Bluenix <[email protected]>
* Improving tries logic
Co-authored-by: Bluenix <[email protected]>
* Updating `positions` list to set
Co-authored-by: Bluenix <[email protected]>
* Updating setup docstring
Co-authored-by: Bluenix <[email protected]>
* Updating comment in callback function of the dropdown
Co-authored-by: Bluenix <[email protected]>
* fixing too many blank lines
* Hardcode dictionary
Co-authored-by: Bluenix <[email protected]>
* restructuring
* fixing errors
* Remove unnecessary comments
Co-authored-by: Bluenix <[email protected]>
* Remove unnecessary comments
Co-authored-by: Bluenix <[email protected]>
* Improve comment explanation
Co-authored-by: Bluenix <[email protected]>
* Remove redundant extra membership test
Co-authored-by: Bluenix <[email protected]>
* Removing verbose variable definition
Co-authored-by: Bluenix <[email protected]>
* Redundant list
Co-authored-by: Bluenix <[email protected]>
* Shorten 'social distancing' (too many separations) between related lines
Co-authored-by: Bluenix <[email protected]>
* improving docstring in `kata_id`
* sending embed if error occurs with api or bs4, also hardcoding params dictionary
* Better comments
Co-authored-by: Bluenix <[email protected]>
* better docstring
Co-authored-by: Bluenix <[email protected]>
* Removing f-string inception and replacing it with more readable code
Co-authored-by: Bluenix <[email protected]>
* More specific docstring
Co-authored-by: Bluenix <[email protected]>
* Removing redundant comments
Co-authored-by: Bluenix <[email protected]>
* Fixing linting errors
* mapping of kyu -> constant
* adding trailing comma
* specific comment regarding where colors are from for `MAPPING_OF_KYU`
* changing name to link too along with link button
* adding ellipsis to make it more clear for `Read more`
* removing redundant sentences from all docstrings of embed creator functions
* fixing unboundlocalerror due to kata_url only being defined under a certain condition
* only allowing supported languages on codewars.com
* fixing url glitch with embed
* Delete hangman.py
* Delete top_1000_used_words.txt
* hangman dependencies leaked into this PR, removing them
* add bs4 and lxml back to lock file
* Capitalize comments
Co-authored-by: Bluenix <[email protected]>
* Improving comments (capitalization)
Co-authored-by: Bluenix <[email protected]>
* polishing
* explaining that self.original_message will be set later in the callback function of the dropdown
* fixing nitpicks
* cast to integer from hex
* removing unnecessary trailing commas
* Simplifying L274-L276
Co-authored-by: Bluenix <[email protected]>
* Add ellipsis to end of description if it's too long
Co-authored-by: Bluenix <[email protected]>
* Changing to hex
Co-authored-by: Bluenix <[email protected]>
* Running blocking function (BeautifulSoup.find_all) to thread
Co-authored-by: Bluenix <[email protected]>
* logger.error errors
* Fixing error with to_thread
* Fixing errors with MAPPING_OF_KYU
Co-authored-by: Bluenix <[email protected]>
* changing `query` to `-query` if the query is a kata level
* changing embed names to add the kata name
* Mimicking mailing list's behavior
Co-authored-by: Bluenix <[email protected]>
* url attribute for all embeds & title for all embeds
* remove view after a certain amount of tikme
* disabling view after waiting instead of just editing it out
* styling
* remove view to avoid spamming errors
* changing `logger` to `log`
Co-authored-by: Xithrius <[email protected]>
* Change `logger` to `log` for logging errors
Co-authored-by: ChrisJL <[email protected]>
Co-authored-by: Bluenix <[email protected]>
Co-authored-by: Xithrius <[email protected]>
Diffstat (limited to 'bot')
| -rw-r--r-- | bot/exts/utilities/challenges.py | 335 | 
1 files changed, 335 insertions, 0 deletions
| diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py new file mode 100644 index 00000000..234eb0be --- /dev/null +++ b/bot/exts/utilities/challenges.py @@ -0,0 +1,335 @@ +import logging +from asyncio import to_thread +from random import choice +from typing import Union + +from bs4 import BeautifulSoup +from discord import Embed, Interaction, SelectOption, ui +from discord.ext import commands + +from bot.bot import Bot +from bot.constants import Colours, Emojis, NEGATIVE_REPLIES + +log = logging.getLogger(__name__) +API_ROOT = "https://www.codewars.com/api/v1/code-challenges/{kata_id}" + +# Map difficulty for the kata to color we want to display in the embed. +# These colors are representative of the colors that each kyu's level represents on codewars.com +MAPPING_OF_KYU = { +    8: 0xdddbda, 7: 0xdddbda, 6: 0xecb613, 5: 0xecb613, +    4: 0x3c7ebb, 3: 0x3c7ebb, 2: 0x866cc7, 1: 0x866cc7 +} + +# Supported languages for a kata on codewars.com +SUPPORTED_LANGUAGES = { +    "stable": [ +        "c", "c#", "c++", "clojure", "coffeescript", "coq", "crystal", "dart", "elixir", +        "f#", "go", "groovy", "haskell", "java", "javascript", "kotlin", "lean", "lua", "nasm", +        "php", "python", "racket", "ruby", "rust", "scala", "shell", "sql", "swift", "typescript" +    ], +    "beta": [ +        "agda", "bf", "cfml", "cobol", "commonlisp", "elm", "erlang", "factor", +        "forth", "fortran", "haxe", "idris", "julia", "nim", "objective-c", "ocaml", +        "pascal", "perl", "powershell", "prolog", "purescript", "r", "raku", "reason", "solidity", "vb.net" +    ] +} + + +class InformationDropdown(ui.Select): +    """A dropdown inheriting from ui.Select that allows finding out other information about the kata.""" + +    def __init__(self, language_embed: Embed, tags_embed: Embed, other_info_embed: Embed, main_embed: Embed): +        options = [ +            SelectOption( +                label="Main Information", +                description="See the kata's difficulty, description, etc.", +                emoji="🌎" +            ), +            SelectOption( +                label="Languages", +                description="See what languages this kata supports!", +                emoji=Emojis.reddit_post_text +            ), +            SelectOption( +                label="Tags", +                description="See what categories this kata falls under!", +                emoji=Emojis.stackoverflow_tag +            ), +            SelectOption( +                label="Other Information", +                description="See how other people performed on this kata and more!", +                emoji="ℹ" +            ) +        ] + +        # We map the option label to the embed instance so that it can be easily looked up later in O(1) +        self.mapping_of_embeds = { +            "Main Information": main_embed, +            "Languages": language_embed, +            "Tags": tags_embed, +            "Other Information": other_info_embed, +        } + +        super().__init__( +            placeholder="See more information regarding this kata", +            min_values=1, +            max_values=1, +            options=options +        ) + +    async def callback(self, interaction: Interaction) -> None: +        """Callback for when someone clicks on a dropdown.""" +        # Edit the message to the embed selected in the option +        # The `original_message` attribute is set just after the message is sent with the view. +        # The attribute is not set during initialization. +        result_embed = self.mapping_of_embeds[self.values[0]] +        await self.original_message.edit(embed=result_embed) + + +class Challenges(commands.Cog): +    """ +    Cog for the challenge command. + +    The challenge command pulls a random kata from codewars.com. +    A kata is the name for a challenge, specific to codewars.com. + +    The challenge command also has filters to customize the kata that is given. +    You can specify the language the kata should be from, difficulty and topic of the kata. +    """ + +    def __init__(self, bot: Bot): +        self.bot = bot + +    async def kata_id(self, search_link: str, params: dict) -> Union[str, Embed]: +        """ +        Uses bs4 to get the HTML code for the page of katas, where the page is the link of the formatted `search_link`. + +        This will webscrape the search page with `search_link` and then get the ID of a kata for the +        codewars.com API to use. +        """ +        async with self.bot.http_session.get(search_link, params=params) as response: +            if response.status != 200: +                error_embed = Embed( +                    title=choice(NEGATIVE_REPLIES), +                    description="We ran into an error when getting the kata from codewars.com, try again later.", +                    color=Colours.soft_red +                ) +                log.error(f"Unexpected response from codewars.com, status code: {response.status}") +                return error_embed + +            soup = BeautifulSoup(await response.text(), features="lxml") +            first_kata_div = await to_thread(soup.find_all, "div", class_="item-title px-0") + +            if not first_kata_div: +                raise commands.BadArgument("No katas could be found with the filters provided.") +            elif len(first_kata_div) >= 3: +                first_kata_div = choice(first_kata_div[:3]) +            elif "q=" not in search_link: +                first_kata_div = choice(first_kata_div) +            else: +                first_kata_div = first_kata_div[0] + +            # There are numerous divs before arriving at the id of the kata, which can be used for the link. +            first_kata_id = first_kata_div.a["href"].split("/")[-1] +            return first_kata_id + +    async def kata_information(self, kata_id: str) -> Union[dict, Embed]: +        """ +        Returns the information about the Kata. + +        Uses the codewars.com API to get information about the kata using `kata_id`. +        """ +        async with self.bot.http_session.get(API_ROOT.format(kata_id=kata_id)) as response: +            if response.status != 200: +                error_embed = Embed( +                    title=choice(NEGATIVE_REPLIES), +                    description="We ran into an error when getting the kata information, try again later.", +                    color=Colours.soft_red +                ) +                log.error(f"Unexpected response from codewars.com/api/v1, status code: {response.status}") +                return error_embed + +            return await response.json() + +    @staticmethod +    def main_embed(kata_information: dict) -> Embed: +        """Creates the main embed which displays the name, difficulty and description of the kata.""" +        kata_description = kata_information["description"] +        kata_url = f"https://codewars.com/kata/{kata_information['id']}" + +        # Ensuring it isn't over the length 1024 +        if len(kata_description) > 1024: +            kata_description = "\n".join(kata_description[:1000].split("\n")[:-1]) + "..." +            kata_description += f" [continue reading]({kata_url})" + +        kata_embed = Embed( +            title=kata_information["name"], +            description=kata_description, +            color=MAPPING_OF_KYU[int(kata_information["rank"]["name"].replace(" kyu", ""))], +            url=kata_url +        ) +        kata_embed.add_field(name="Difficulty", value=kata_information["rank"]["name"], inline=False) +        return kata_embed + +    @staticmethod +    def language_embed(kata_information: dict) -> Embed: +        """Creates the 'language embed' which displays all languages the kata supports.""" +        kata_url = f"https://codewars.com/kata/{kata_information['id']}" + +        languages = "\n".join(map(str.title, kata_information["languages"])) +        language_embed = Embed( +            title=kata_information["name"], +            description=f"```yaml\nSupported Languages:\n{languages}\n```", +            color=Colours.python_blue, +            url=kata_url +        ) +        return language_embed + +    @staticmethod +    def tags_embed(kata_information: dict) -> Embed: +        """ +        Creates the 'tags embed' which displays all the tags of the Kata. + +        Tags explain what the kata is about, this is what codewars.com calls categories. +        """ +        kata_url = f"https://codewars.com/kata/{kata_information['id']}" + +        tags = "\n".join(kata_information["tags"]) +        tags_embed = Embed( +            title=kata_information["name"], +            description=f"```yaml\nTags:\n{tags}\n```", +            color=Colours.grass_green, +            url=kata_url +        ) +        return tags_embed + +    @staticmethod +    def miscellaneous_embed(kata_information: dict) -> Embed: +        """ +        Creates the 'other information embed' which displays miscellaneous information about the kata. + +        This embed shows statistics such as the total number of people who completed the kata, +        the total number of stars of the kata, etc. +        """ +        kata_url = f"https://codewars.com/kata/{kata_information['id']}" + +        embed = Embed( +            title=kata_information["name"], +            description="```nim\nOther Information\n```", +            color=Colours.grass_green, +            url=kata_url +        ) +        embed.add_field( +            name="`Total Score`", +            value=f"```css\n{kata_information['voteScore']}\n```", +            inline=False +        ) +        embed.add_field( +            name="`Total Stars`", +            value=f"```css\n{kata_information['totalStars']}\n```", +            inline=False +        ) +        embed.add_field( +            name="`Total Completed`", +            value=f"```css\n{kata_information['totalCompleted']}\n```", +            inline=False +        ) +        embed.add_field( +            name="`Total Attempts`", +            value=f"```css\n{kata_information['totalAttempts']}\n```", +            inline=False +        ) +        return embed + +    @staticmethod +    def create_view(dropdown: InformationDropdown, link: str) -> ui.View: +        """ +        Creates the discord.py View for the Discord message components (dropdowns and buttons). + +        The discord UI is implemented onto the embed, where the user can choose what information about the kata they +        want, along with a link button to the kata itself. +        """ +        view = ui.View() +        view.add_item(dropdown) +        view.add_item(ui.Button(label="View the Kata", url=link)) +        return view + +    @commands.command(aliases=["kata"]) +    @commands.cooldown(1, 5, commands.BucketType.user) +    async def challenge(self, ctx: commands.Context, language: str = "python", *, query: str = None) -> None: +        """ +        The challenge command pulls a random kata (challenge) from codewars.com. + +        The different ways to use this command are: +        `.challenge <language>` - Pulls a random challenge within that language's scope. +        `.challenge <language> <difficulty>` - The difficulty can be from 1-8, +        1 being the hardest, 8 being the easiest. This pulls a random challenge within that difficulty & language. +        `.challenge <language> <query>` - Pulls a random challenge with the query provided under the language +        `.challenge <language> <query>, <difficulty>` - Pulls a random challenge with the query provided, +        under that difficulty within the language's scope. +        """ +        if language.lower() not in SUPPORTED_LANGUAGES["stable"] + SUPPORTED_LANGUAGES["beta"]: +            raise commands.BadArgument("This is not a recognized language on codewars.com!") + +        get_kata_link = f"https://codewars.com/kata/search/{language}" +        params = {} + +        if language and not query: +            level = f"-{choice([1, 2, 3, 4, 5, 6, 7, 8])}" +            params["r[]"] = level +        elif "," in query: +            query_splitted = query.split("," if ", " not in query else ", ") + +            if len(query_splitted) > 2: +                raise commands.BadArgument( +                    "There can only be one comma within the query, separating the difficulty and the query itself." +                ) + +            query, level = query_splitted +            params["q"] = query +            params["r[]"] = f"-{level}" +        elif query.isnumeric(): +            params["r[]"] = f"-{query}" +        else: +            params["q"] = query + +        params["beta"] = str(language in SUPPORTED_LANGUAGES["beta"]).lower() + +        first_kata_id = await self.kata_id(get_kata_link, params) +        if isinstance(first_kata_id, Embed): +            # We ran into an error when retrieving the website link +            await ctx.send(embed=first_kata_id) +            return + +        kata_information = await self.kata_information(first_kata_id) +        if isinstance(kata_information, Embed): +            # Something went wrong when trying to fetch the kata information +            await ctx.d(embed=kata_information) +            return + +        kata_embed = self.main_embed(kata_information) +        language_embed = self.language_embed(kata_information) +        tags_embed = self.tags_embed(kata_information) +        miscellaneous_embed = self.miscellaneous_embed(kata_information) + +        dropdown = InformationDropdown( +            main_embed=kata_embed, +            language_embed=language_embed, +            tags_embed=tags_embed, +            other_info_embed=miscellaneous_embed +        ) +        kata_view = self.create_view(dropdown, f"https://codewars.com/kata/{first_kata_id}") +        original_message = await ctx.send( +            embed=kata_embed, +            view=kata_view +        ) +        dropdown.original_message = original_message + +        wait_for_kata = await kata_view.wait() +        if wait_for_kata: +            await original_message.edit(embed=kata_embed, view=None) + + +def setup(bot: Bot) -> None: +    """Load the Challenges cog.""" +    bot.add_cog(Challenges(bot)) | 
