diff options
Diffstat (limited to 'bot/exts')
| -rw-r--r-- | bot/exts/christmas/adventofcode.py | 2 | ||||
| -rw-r--r-- | bot/exts/easter/conversationstarters.py | 28 | ||||
| -rw-r--r-- | bot/exts/evergreen/conversationstarters.py | 71 | ||||
| -rw-r--r-- | bot/exts/evergreen/fun.py | 30 | ||||
| -rw-r--r-- | bot/exts/evergreen/reddit.py | 6 | ||||
| -rw-r--r-- | bot/exts/evergreen/snakes/snakes_cog.py | 4 | ||||
| -rw-r--r-- | bot/exts/evergreen/status_cats.py | 33 | ||||
| -rw-r--r-- | bot/exts/evergreen/wolfram.py | 278 | ||||
| -rw-r--r-- | bot/exts/halloween/candy_collection.py | 6 | 
9 files changed, 403 insertions, 55 deletions
| diff --git a/bot/exts/christmas/adventofcode.py b/bot/exts/christmas/adventofcode.py index 00607074..b3fe0623 100644 --- a/bot/exts/christmas/adventofcode.py +++ b/bot/exts/christmas/adventofcode.py @@ -58,7 +58,7 @@ async def countdown_status(bot: commands.Bot) -> None:          hours, minutes = aligned_seconds // 3600, aligned_seconds // 60 % 60          if aligned_seconds == 0: -            playing = f"right now!" +            playing = "right now!"          elif aligned_seconds == COUNTDOWN_STEP:              playing = f"in less than {minutes} minutes"          elif hours == 0: diff --git a/bot/exts/easter/conversationstarters.py b/bot/exts/easter/conversationstarters.py deleted file mode 100644 index a5f40445..00000000 --- a/bot/exts/easter/conversationstarters.py +++ /dev/null @@ -1,28 +0,0 @@ -import json -import logging -import random -from pathlib import Path - -from discord.ext import commands - -log = logging.getLogger(__name__) - -with open(Path("bot/resources/easter/starter.json"), "r", encoding="utf8") as f: -    starters = json.load(f) - - -class ConvoStarters(commands.Cog): -    """Easter conversation topics.""" - -    def __init__(self, bot: commands.Bot): -        self.bot = bot - -    @commands.command() -    async def topic(self, ctx: commands.Context) -> None: -        """Responds with a random topic to start a conversation.""" -        await ctx.send(random.choice(starters['starters'])) - - -def setup(bot: commands.Bot) -> None: -    """Conversation starters Cog load.""" -    bot.add_cog(ConvoStarters(bot)) diff --git a/bot/exts/evergreen/conversationstarters.py b/bot/exts/evergreen/conversationstarters.py new file mode 100644 index 00000000..576b8d76 --- /dev/null +++ b/bot/exts/evergreen/conversationstarters.py @@ -0,0 +1,71 @@ +from pathlib import Path + +import yaml +from discord import Color, Embed +from discord.ext import commands + +from bot.constants import WHITELISTED_CHANNELS +from bot.utils.decorators import override_in_channel +from bot.utils.randomization import RandomCycle + +SUGGESTION_FORM = 'https://forms.gle/zw6kkJqv8U43Nfjg9' + +with Path("bot/resources/evergreen/starter.yaml").open("r", encoding="utf8") as f: +    STARTERS = yaml.load(f, Loader=yaml.FullLoader) + +with Path("bot/resources/evergreen/py_topics.yaml").open("r", encoding="utf8") as f: +    # First ID is #python-general and the rest are top to bottom categories of Topical Chat/Help. +    PY_TOPICS = yaml.load(f, Loader=yaml.FullLoader) + +    # Removing `None` from lists of topics, if not a list, it is changed to an empty one. +    PY_TOPICS = {k: [i for i in v if i] if isinstance(v, list) else [] for k, v in PY_TOPICS.items()} + +    # All the allowed channels that the ".topic" command is allowed to be executed in. +    ALL_ALLOWED_CHANNELS = list(PY_TOPICS.keys()) + list(WHITELISTED_CHANNELS) + +# Putting all topics into one dictionary and shuffling lists to reduce same-topic repetitions. +ALL_TOPICS = {'default': STARTERS, **PY_TOPICS} +TOPICS = { +    channel: RandomCycle(topics or ['No topics found for this channel.']) +    for channel, topics in ALL_TOPICS.items() +} + + +class ConvoStarters(commands.Cog): +    """Evergreen conversation topics.""" + +    def __init__(self, bot: commands.Bot): +        self.bot = bot + +    @commands.command() +    @override_in_channel(ALL_ALLOWED_CHANNELS) +    async def topic(self, ctx: commands.Context) -> None: +        """ +        Responds with a random topic to start a conversation. + +        If in a Python channel, a python-related topic will be given. + +        Otherwise, a random conversation topic will be received by the user. +        """ +        # No matter what, the form will be shown. +        embed = Embed(description=f'Suggest more topics [here]({SUGGESTION_FORM})!', color=Color.blurple()) + +        try: +            # Fetching topics. +            channel_topics = TOPICS[ctx.channel.id] + +        # If the channel isn't Python-related. +        except KeyError: +            embed.title = f'**{next(TOPICS["default"])}**' + +        # If the channel ID doesn't have any topics. +        else: +            embed.title = f'**{next(channel_topics)}**' + +        finally: +            await ctx.send(embed=embed) + + +def setup(bot: commands.Bot) -> None: +    """Conversation starters Cog load.""" +    bot.add_cog(ConvoStarters(bot)) diff --git a/bot/exts/evergreen/fun.py b/bot/exts/evergreen/fun.py index c5f8f9c8..2f575c1c 100644 --- a/bot/exts/evergreen/fun.py +++ b/bot/exts/evergreen/fun.py @@ -66,17 +66,13 @@ class Fun(Cog):          elif num_rolls < 1:              output = ":no_entry: You must roll at least once."          for _ in range(num_rolls): -            terning = f"terning{random.randint(1, 6)}" -            output += getattr(Emojis, terning, '') +            dice = f"dice_{random.randint(1, 6)}" +            output += getattr(Emojis, dice, '')          await ctx.send(output)      @commands.command(name="uwu", aliases=("uwuwize", "uwuify",))      async def uwu_command(self, ctx: Context, *, text: str) -> None: -        """ -        Converts a given `text` into it's uwu equivalent. - -        Also accepts a valid discord Message ID or link. -        """ +        """Converts a given `text` into it's uwu equivalent."""          conversion_func = functools.partial(              utils.replace_many, replacements=UWU_WORDS, ignore_case=True, match_case=True          ) @@ -92,11 +88,7 @@ class Fun(Cog):      @commands.command(name="randomcase", aliases=("rcase", "randomcaps", "rcaps",))      async def randomcase_command(self, ctx: Context, *, text: str) -> None: -        """ -        Randomly converts the casing of a given `text`. - -        Also accepts a valid discord Message ID or link. -        """ +        """Randomly converts the casing of a given `text`."""          def conversion_func(text: str) -> str:              """Randomly converts the casing of a given string."""              return "".join( @@ -194,12 +186,14 @@ class Fun(Cog):              Union[Embed, None]: The embed if found in the valid Message, else None          """          embed = None -        message = await Fun._get_discord_message(ctx, text) -        if isinstance(message, Message): -            text = message.content -            # Take first embed because we can't send multiple embeds -            if message.embeds: -                embed = message.embeds[0] + +        # message = await Fun._get_discord_message(ctx, text) +        # if isinstance(message, Message): +        #     text = message.content +        #     # Take first embed because we can't send multiple embeds +        #     if message.embeds: +        #         embed = message.embeds[0] +          return (text, embed)      @staticmethod diff --git a/bot/exts/evergreen/reddit.py b/bot/exts/evergreen/reddit.py index fe204419..49127bea 100644 --- a/bot/exts/evergreen/reddit.py +++ b/bot/exts/evergreen/reddit.py @@ -68,9 +68,9 @@ class Reddit(commands.Cog):          # -----------------------------------------------------------          # This code below is bound of change when the emojis are added. -        upvote_emoji = self.bot.get_emoji(638729835245731840) -        comment_emoji = self.bot.get_emoji(638729835073765387) -        user_emoji = self.bot.get_emoji(638729835442602003) +        upvote_emoji = self.bot.get_emoji(755845219890757644) +        comment_emoji = self.bot.get_emoji(755845255001014384) +        user_emoji = self.bot.get_emoji(755845303822974997)          text_emoji = self.bot.get_emoji(676030265910493204)          video_emoji = self.bot.get_emoji(676030265839190047)          image_emoji = self.bot.get_emoji(676030265734201344) diff --git a/bot/exts/evergreen/snakes/snakes_cog.py b/bot/exts/evergreen/snakes/snakes_cog.py index b3896fcd..9bbad9fe 100644 --- a/bot/exts/evergreen/snakes/snakes_cog.py +++ b/bot/exts/evergreen/snakes/snakes_cog.py @@ -567,7 +567,7 @@ class Snakes(Cog):              antidote_embed = Embed(color=SNAKE_COLOR, title="Antidote")              antidote_embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar_url)              antidote_embed.set_image(url="https://i.makeagif.com/media/7-12-2015/Cj1pts.gif") -            antidote_embed.add_field(name=f"You have created the snake antidote!", +            antidote_embed.add_field(name="You have created the snake antidote!",                                       value=f"The solution was: {' '.join(antidote_answer)}\n"                                             f"You had {10 - antidote_tries} tries remaining.")              await board_id.edit(embed=antidote_embed) @@ -1078,7 +1078,7 @@ class Snakes(Cog):              query = snake['name']          # Build the URL and make the request -        url = f'https://www.googleapis.com/youtube/v3/search' +        url = 'https://www.googleapis.com/youtube/v3/search'          response = await self.bot.http_session.get(              url,              params={ diff --git a/bot/exts/evergreen/status_cats.py b/bot/exts/evergreen/status_cats.py new file mode 100644 index 00000000..586b8378 --- /dev/null +++ b/bot/exts/evergreen/status_cats.py @@ -0,0 +1,33 @@ +from http import HTTPStatus + +import discord +from discord.ext import commands + + +class StatusCats(commands.Cog): +    """Commands that give HTTP statuses described and visualized by cats.""" + +    def __init__(self, bot: commands.Bot): +        self.bot = bot + +    @commands.command(aliases=['statuscat']) +    async def http_cat(self, ctx: commands.Context, code: int) -> None: +        """Sends an embed with an image of a cat, potraying the status code.""" +        embed = discord.Embed(title=f'**Status: {code}**') + +        try: +            HTTPStatus(code) + +        except ValueError: +            embed.set_footer(text='Inputted status code does not exist.') + +        else: +            embed.set_image(url=f'https://http.cat/{code}.jpg') + +        finally: +            await ctx.send(embed=embed) + + +def setup(bot: commands.Bot) -> None: +    """Load the StatusCats cog.""" +    bot.add_cog(StatusCats(bot)) diff --git a/bot/exts/evergreen/wolfram.py b/bot/exts/evergreen/wolfram.py new file mode 100644 index 00000000..898e8d2a --- /dev/null +++ b/bot/exts/evergreen/wolfram.py @@ -0,0 +1,278 @@ +import logging +from io import BytesIO +from typing import Callable, List, Optional, Tuple +from urllib import parse + +import arrow +import discord +from discord import Embed +from discord.ext import commands +from discord.ext.commands import BucketType, Cog, Context, check, group + +from bot.constants import Colours, STAFF_ROLES, Wolfram +from bot.utils.pagination import ImagePaginator + +log = logging.getLogger(__name__) + +APPID = Wolfram.key +DEFAULT_OUTPUT_FORMAT = "JSON" +QUERY = "http://api.wolframalpha.com/v2/{request}?{data}" +WOLF_IMAGE = "https://www.symbols.com/gi.php?type=1&id=2886&i=1" + +MAX_PODS = 20 + +# Allows for 10 wolfram calls pr user pr day +usercd = commands.CooldownMapping.from_cooldown(Wolfram.user_limit_day, 60 * 60 * 24, BucketType.user) + +# Allows for max api requests / days in month per day for the entire guild (Temporary) +guildcd = commands.CooldownMapping.from_cooldown(Wolfram.guild_limit_day, 60 * 60 * 24, BucketType.guild) + + +async def send_embed( +        ctx: Context, +        message_txt: str, +        colour: int = Colours.soft_red, +        footer: str = None, +        img_url: str = None, +        f: discord.File = None +) -> None: +    """Generate & send a response embed with Wolfram as the author.""" +    embed = Embed(colour=colour) +    embed.description = message_txt +    embed.set_author(name="Wolfram Alpha", +                     icon_url=WOLF_IMAGE, +                     url="https://www.wolframalpha.com/") +    if footer: +        embed.set_footer(text=footer) + +    if img_url: +        embed.set_image(url=img_url) + +    await ctx.send(embed=embed, file=f) + + +def custom_cooldown(*ignore: List[int]) -> Callable: +    """ +    Implement per-user and per-guild cooldowns for requests to the Wolfram API. + +    A list of roles may be provided to ignore the per-user cooldown +    """ +    async def predicate(ctx: Context) -> bool: +        if ctx.invoked_with == 'help': +            # if the invoked command is help we don't want to increase the ratelimits since it's not actually +            # invoking the command/making a request, so instead just check if the user/guild are on cooldown. +            guild_cooldown = not guildcd.get_bucket(ctx.message).get_tokens() == 0  # if guild is on cooldown +            if not any(r.id in ignore for r in ctx.author.roles):  # check user bucket if user is not ignored +                return guild_cooldown and not usercd.get_bucket(ctx.message).get_tokens() == 0 +            return guild_cooldown + +        user_bucket = usercd.get_bucket(ctx.message) + +        if all(role.id not in ignore for role in ctx.author.roles): +            user_rate = user_bucket.update_rate_limit() + +            if user_rate: +                # Can't use api; cause: member limit +                cooldown = arrow.utcnow().shift(seconds=int(user_rate)).humanize(only_distance=True) +                message = ( +                    "You've used up your limit for Wolfram|Alpha requests.\n" +                    f"Cooldown: {cooldown}" +                ) +                await send_embed(ctx, message) +                return False + +        guild_bucket = guildcd.get_bucket(ctx.message) +        guild_rate = guild_bucket.update_rate_limit() + +        # Repr has a token attribute to read requests left +        log.debug(guild_bucket) + +        if guild_rate: +            # Can't use api; cause: guild limit +            message = ( +                "The max limit of requests for the server has been reached for today.\n" +                f"Cooldown: {int(guild_rate)}" +            ) +            await send_embed(ctx, message) +            return False + +        return True + +    return check(predicate) + + +async def get_pod_pages(ctx: Context, bot: commands.Bot, query: str) -> Optional[List[Tuple]]: +    """Get the Wolfram API pod pages for the provided query.""" +    async with ctx.channel.typing(): +        url_str = parse.urlencode({ +            "input": query, +            "appid": APPID, +            "output": DEFAULT_OUTPUT_FORMAT, +            "format": "image,plaintext" +        }) +        request_url = QUERY.format(request="query", data=url_str) + +        async with bot.http_session.get(request_url) as response: +            json = await response.json(content_type='text/plain') + +        result = json["queryresult"] + +        if result["error"]: +            # API key not set up correctly +            if result["error"]["msg"] == "Invalid appid": +                message = "Wolfram API key is invalid or missing." +                log.warning( +                    "API key seems to be missing, or invalid when " +                    f"processing a wolfram request: {url_str}, Response: {json}" +                ) +                await send_embed(ctx, message) +                return + +            message = "Something went wrong internally with your request, please notify staff!" +            log.warning(f"Something went wrong getting a response from wolfram: {url_str}, Response: {json}") +            await send_embed(ctx, message) +            return + +        if not result["success"]: +            message = f"I couldn't find anything for {query}." +            await send_embed(ctx, message) +            return + +        if not result["numpods"]: +            message = "Could not find any results." +            await send_embed(ctx, message) +            return + +        pods = result["pods"] +        pages = [] +        for pod in pods[:MAX_PODS]: +            subs = pod.get("subpods") + +            for sub in subs: +                title = sub.get("title") or sub.get("plaintext") or sub.get("id", "") +                img = sub["img"]["src"] +                pages.append((title, img)) +        return pages + + +class Wolfram(Cog): +    """Commands for interacting with the Wolfram|Alpha API.""" + +    def __init__(self, bot: commands.Bot): +        self.bot = bot + +    @group(name="wolfram", aliases=("wolf", "wa"), invoke_without_command=True) +    @custom_cooldown(*STAFF_ROLES) +    async def wolfram_command(self, ctx: Context, *, query: str) -> None: +        """Requests all answers on a single image, sends an image of all related pods.""" +        url_str = parse.urlencode({ +            "i": query, +            "appid": APPID, +        }) +        query = QUERY.format(request="simple", data=url_str) + +        # Give feedback that the bot is working. +        async with ctx.channel.typing(): +            async with self.bot.http_session.get(query) as response: +                status = response.status +                image_bytes = await response.read() + +            f = discord.File(BytesIO(image_bytes), filename="image.png") +            image_url = "attachment://image.png" + +            if status == 501: +                message = "Failed to get response" +                footer = "" +                color = Colours.soft_red +            elif status == 400: +                message = "No input found" +                footer = "" +                color = Colours.soft_red +            elif status == 403: +                message = "Wolfram API key is invalid or missing." +                footer = "" +                color = Colours.soft_red +            else: +                message = "" +                footer = "View original for a bigger picture." +                color = Colours.soft_orange + +            # Sends a "blank" embed if no request is received, unsure how to fix +            await send_embed(ctx, message, color, footer=footer, img_url=image_url, f=f) + +    @wolfram_command.command(name="page", aliases=("pa", "p")) +    @custom_cooldown(*STAFF_ROLES) +    async def wolfram_page_command(self, ctx: Context, *, query: str) -> None: +        """ +        Requests a drawn image of given query. + +        Keywords worth noting are, "like curve", "curve", "graph", "pokemon", etc. +        """ +        pages = await get_pod_pages(ctx, self.bot, query) + +        if not pages: +            return + +        embed = Embed() +        embed.set_author(name="Wolfram Alpha", +                         icon_url=WOLF_IMAGE, +                         url="https://www.wolframalpha.com/") +        embed.colour = Colours.soft_orange + +        await ImagePaginator.paginate(pages, ctx, embed) + +    @wolfram_command.command(name="cut", aliases=("c",)) +    @custom_cooldown(*STAFF_ROLES) +    async def wolfram_cut_command(self, ctx: Context, *, query: str) -> None: +        """ +        Requests a drawn image of given query. + +        Keywords worth noting are, "like curve", "curve", "graph", "pokemon", etc. +        """ +        pages = await get_pod_pages(ctx, self.bot, query) + +        if not pages: +            return + +        if len(pages) >= 2: +            page = pages[1] +        else: +            page = pages[0] + +        await send_embed(ctx, page[0], colour=Colours.soft_orange, img_url=page[1]) + +    @wolfram_command.command(name="short", aliases=("sh", "s")) +    @custom_cooldown(*STAFF_ROLES) +    async def wolfram_short_command(self, ctx: Context, *, query: str) -> None: +        """Requests an answer to a simple question.""" +        url_str = parse.urlencode({ +            "i": query, +            "appid": APPID, +        }) +        query = QUERY.format(request="result", data=url_str) + +        # Give feedback that the bot is working. +        async with ctx.channel.typing(): +            async with self.bot.http_session.get(query) as response: +                status = response.status +                response_text = await response.text() + +            if status == 501: +                message = "Failed to get response" +                color = Colours.soft_red +            elif status == 400: +                message = "No input found" +                color = Colours.soft_red +            elif response_text == "Error 1: Invalid appid": +                message = "Wolfram API key is invalid or missing." +                color = Colours.soft_red +            else: +                message = response_text +                color = Colours.soft_orange + +            await send_embed(ctx, message, color) + + +def setup(bot: commands.Bot) -> None: +    """Load the Wolfram cog.""" +    bot.add_cog(Wolfram(bot)) diff --git a/bot/exts/halloween/candy_collection.py b/bot/exts/halloween/candy_collection.py index 2c7d2f23..caf0df11 100644 --- a/bot/exts/halloween/candy_collection.py +++ b/bot/exts/halloween/candy_collection.py @@ -212,9 +212,9 @@ class CandyCollection(commands.Cog):          e = discord.Embed(colour=discord.Colour.blurple())          e.add_field(name="Top Candy Records", value=value, inline=False)          e.add_field(name='\u200b', -                    value=f"Candies will randomly appear on messages sent. " -                          f"\nHit the candy when it appears as fast as possible to get the candy! " -                          f"\nBut beware the ghosts...", +                    value="Candies will randomly appear on messages sent. " +                          "\nHit the candy when it appears as fast as possible to get the candy! " +                          "\nBut beware the ghosts...",                      inline=False)          await ctx.send(embed=e) | 
