diff options
Diffstat (limited to 'bot/exts')
| -rw-r--r-- | bot/exts/easter/save_the_planet.py | 29 | ||||
| -rw-r--r-- | bot/exts/evergreen/emoji_count.py | 91 | ||||
| -rw-r--r-- | bot/exts/evergreen/fun.py | 22 | ||||
| -rw-r--r-- | bot/exts/evergreen/minesweeper.py | 12 | ||||
| -rw-r--r-- | bot/exts/evergreen/snakes/_snakes_cog.py | 4 | ||||
| -rw-r--r-- | bot/exts/evergreen/source.py | 109 | ||||
| -rw-r--r-- | bot/exts/halloween/hacktober-issue-finder.py | 2 | ||||
| -rw-r--r-- | bot/exts/valentines/valentine_zodiac.py | 145 |
8 files changed, 367 insertions, 47 deletions
diff --git a/bot/exts/easter/save_the_planet.py b/bot/exts/easter/save_the_planet.py new file mode 100644 index 00000000..8f644259 --- /dev/null +++ b/bot/exts/easter/save_the_planet.py @@ -0,0 +1,29 @@ +import json +from pathlib import Path + +from discord import Embed +from discord.ext import commands + +from bot.utils.randomization import RandomCycle + + +with Path("bot/resources/easter/save_the_planet.json").open('r', encoding='utf8') as f: + EMBED_DATA = RandomCycle(json.load(f)) + + +class SaveThePlanet(commands.Cog): + """A cog that teaches users how they can help our planet.""" + + def __init__(self, bot: commands.Bot) -> None: + self.bot = bot + + @commands.command(aliases=('savetheearth', 'saveplanet', 'saveearth')) + async def savetheplanet(self, ctx: commands.Context) -> None: + """Responds with a random tip on how to be eco-friendly and help our planet.""" + return_embed = Embed.from_dict(next(EMBED_DATA)) + await ctx.send(embed=return_embed) + + +def setup(bot: commands.Bot) -> None: + """Save the Planet Cog load.""" + bot.add_cog(SaveThePlanet(bot)) diff --git a/bot/exts/evergreen/emoji_count.py b/bot/exts/evergreen/emoji_count.py new file mode 100644 index 00000000..ef900199 --- /dev/null +++ b/bot/exts/evergreen/emoji_count.py @@ -0,0 +1,91 @@ +import datetime +import logging +import random +from typing import Dict, Optional + +import discord +from discord.ext import commands + +from bot.constants import Colours, ERROR_REPLIES + +log = logging.getLogger(__name__) + + +class EmojiCount(commands.Cog): + """Command that give random emoji based on category.""" + + def __init__(self, bot: commands.Bot): + self.bot = bot + + def embed_builder(self, emoji: dict) -> discord.Embed: + """Generates an embed with the emoji names and count.""" + embed = discord.Embed( + color=Colours.orange, + title="Emoji Count", + timestamp=datetime.datetime.utcnow() + ) + + if len(emoji) == 1: + for key, value in emoji.items(): + embed.description = f"There are **{len(value)}** emojis in the **{key}** category" + embed.set_thumbnail(url=random.choice(value).url) + else: + msg = '' + for key, value in emoji.items(): + emoji_choice = random.choice(value) + emoji_info = f'There are **{len(value)}** emojis in the **{key}** category\n' + msg += f'<:{emoji_choice.name}:{emoji_choice.id}> {emoji_info}' + embed.description = msg + return embed + + @staticmethod + def generate_invalid_embed(ctx: commands.Context) -> discord.Embed: + """Genrates error embed.""" + embed = discord.Embed( + color=Colours.soft_red, + title=random.choice(ERROR_REPLIES) + ) + + emoji_dict = {} + for emoji in ctx.guild.emojis: + emoji_dict[emoji.name.split("_")[0]] = [] + + error_comp = ', '.join(key for key in emoji_dict.keys()) + embed.description = f"These are the valid categories\n```{error_comp}```" + return embed + + def emoji_list(self, ctx: commands.Context, categories: dict) -> Dict: + """Generates an embed with the emoji names and count.""" + out = {category: [] for category in categories} + + for emoji in ctx.guild.emojis: + category = emoji.name.split('_')[0] + if category in out: + out[category].append(emoji) + return out + + @commands.command(name="emoji_count", aliases=["ec"]) + async def ec(self, ctx: commands.Context, *, emoji: str = None) -> Optional[str]: + """Returns embed with emoji category and info given by the user.""" + emoji_dict = {} + + for a in ctx.guild.emojis: + if emoji is None: + log.trace("Emoji Category not provided by the user") + emoji_dict.update({a.name.split("_")[0]: []}) + elif a.name.split("_")[0] in emoji: + log.trace("Emoji Category provided by the user") + emoji_dict.update({a.name.split("_")[0]: []}) + + emoji_dict = self.emoji_list(ctx, emoji_dict) + + if len(emoji_dict) == 0: + embed = self.generate_invalid_embed(ctx) + else: + embed = self.embed_builder(emoji_dict) + await ctx.send(embed=embed) + + +def setup(bot: commands.Bot) -> None: + """Emoji Count Cog load.""" + bot.add_cog(EmojiCount(bot)) diff --git a/bot/exts/evergreen/fun.py b/bot/exts/evergreen/fun.py index de6a92c6..231e6d54 100644 --- a/bot/exts/evergreen/fun.py +++ b/bot/exts/evergreen/fun.py @@ -7,7 +7,7 @@ from typing import Callable, Iterable, Tuple, Union from discord import Embed, Message from discord.ext import commands -from discord.ext.commands import Bot, Cog, Context, MessageConverter, clean_content +from discord.ext.commands import BadArgument, Bot, Cog, Context, MessageConverter, clean_content from bot import utils from bot.constants import Colours, Emojis @@ -57,18 +57,20 @@ class Fun(Cog): with Path("bot/resources/evergreen/caesar_info.json").open("r", encoding="UTF-8") as f: self._caesar_cipher_embed = json.load(f) + @staticmethod + def _get_random_die() -> str: + """Generate a random die emoji, ready to be sent on Discord.""" + die_name = f"dice_{random.randint(1, 6)}" + return getattr(Emojis, die_name) + @commands.command() async def roll(self, ctx: Context, num_rolls: int = 1) -> None: """Outputs a number of random dice emotes (up to 6).""" - output = "" - if num_rolls > 6: - num_rolls = 6 - elif num_rolls < 1: - output = ":no_entry: You must roll at least once." - for _ in range(num_rolls): - dice = f"dice_{random.randint(1, 6)}" - output += getattr(Emojis, dice, '') - await ctx.send(output) + if 1 <= num_rolls <= 6: + dice = " ".join(self._get_random_die() for _ in range(num_rolls)) + await ctx.send(dice) + else: + raise BadArgument("`!roll` only supports between 1 and 6 rolls.") @commands.command(name="uwu", aliases=("uwuwize", "uwuify",)) async def uwu_command(self, ctx: Context, *, text: clean_content(fix_channel_mentions=True)) -> None: diff --git a/bot/exts/evergreen/minesweeper.py b/bot/exts/evergreen/minesweeper.py index 3e40f493..286ac7a5 100644 --- a/bot/exts/evergreen/minesweeper.py +++ b/bot/exts/evergreen/minesweeper.py @@ -120,14 +120,14 @@ class Minesweeper(commands.Cog): def format_for_discord(board: GameBoard) -> str: """Format the board as a string for Discord.""" discord_msg = ( - ":stop_button: :regional_indicator_a::regional_indicator_b::regional_indicator_c:" - ":regional_indicator_d::regional_indicator_e::regional_indicator_f::regional_indicator_g:" - ":regional_indicator_h::regional_indicator_i::regional_indicator_j:\n\n" + ":stop_button: :regional_indicator_a: :regional_indicator_b: :regional_indicator_c: " + ":regional_indicator_d: :regional_indicator_e: :regional_indicator_f: :regional_indicator_g: " + ":regional_indicator_h: :regional_indicator_i: :regional_indicator_j:\n\n" ) rows = [] for row_number, row in enumerate(board): new_row = f"{MESSAGE_MAPPING[row_number + 1]} " - new_row += "".join(MESSAGE_MAPPING[cell] for cell in row) + new_row += " ".join(MESSAGE_MAPPING[cell] for cell in row) rows.append(new_row) discord_msg += "\n".join(rows) @@ -158,7 +158,7 @@ class Minesweeper(commands.Cog): if ctx.guild: await ctx.send(f"{ctx.author.mention} is playing Minesweeper") - chat_msg = await ctx.send(f"Here's there board!\n{self.format_for_discord(revealed_board)}") + chat_msg = await ctx.send(f"Here's their board!\n{self.format_for_discord(revealed_board)}") else: chat_msg = None @@ -176,7 +176,7 @@ class Minesweeper(commands.Cog): await game.dm_msg.delete() game.dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(game.revealed)}") if game.activated_on_server: - await game.chat_msg.edit(content=f"Here's there board!\n{self.format_for_discord(game.revealed)}") + await game.chat_msg.edit(content=f"Here's their board!\n{self.format_for_discord(game.revealed)}") @commands.dm_only() @minesweeper_group.command(name="flag") diff --git a/bot/exts/evergreen/snakes/_snakes_cog.py b/bot/exts/evergreen/snakes/_snakes_cog.py index a846274b..70bb0e73 100644 --- a/bot/exts/evergreen/snakes/_snakes_cog.py +++ b/bot/exts/evergreen/snakes/_snakes_cog.py @@ -1083,13 +1083,13 @@ class Snakes(Cog): url, params={ "part": "snippet", - "q": urllib.parse.quote(query), + "q": urllib.parse.quote_plus(query), "type": "video", "key": Tokens.youtube } ) response = await response.json() - data = response['items'] + data = response.get("items", []) # Send the user a video if len(data) > 0: diff --git a/bot/exts/evergreen/source.py b/bot/exts/evergreen/source.py new file mode 100644 index 00000000..0725714f --- /dev/null +++ b/bot/exts/evergreen/source.py @@ -0,0 +1,109 @@ +import inspect +from pathlib import Path +from typing import Optional, Tuple, Union + +from discord import Embed +from discord.ext import commands + +from bot.constants import Source + +SourceType = Union[commands.Command, commands.Cog, str, commands.ExtensionNotLoaded] + + +class SourceConverter(commands.Converter): + """Convert an argument into a help command, tag, command, or cog.""" + + async def convert(self, ctx: commands.Context, argument: str) -> SourceType: + """Convert argument into source object.""" + cog = ctx.bot.get_cog(argument) + if cog: + return cog + + cmd = ctx.bot.get_command(argument) + if cmd: + return cmd + + raise commands.BadArgument( + f"Unable to convert `{argument}` to valid command or Cog." + ) + + +class BotSource(commands.Cog): + """Displays information about the bot's source code.""" + + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.command(name="source", aliases=("src",)) + async def source_command(self, ctx: commands.Context, *, source_item: SourceConverter = None) -> None: + """Display information and a GitHub link to the source code of a command, tag, or cog.""" + if not source_item: + embed = Embed(title="Seasonal Bot's GitHub Repository") + embed.add_field(name="Repository", value=f"[Go to GitHub]({Source.github})") + embed.set_thumbnail(url=Source.github_avatar_url) + await ctx.send(embed=embed) + return + + embed = await self.build_embed(source_item) + await ctx.send(embed=embed) + + def get_source_link(self, source_item: SourceType) -> Tuple[str, str, Optional[int]]: + """ + Build GitHub link of source item, return this link, file location and first line number. + + Raise BadArgument if `source_item` is a dynamically-created object (e.g. via internal eval). + """ + if isinstance(source_item, commands.Command): + src = source_item.callback.__code__ + filename = src.co_filename + else: + src = type(source_item) + try: + filename = inspect.getsourcefile(src) + except TypeError: + raise commands.BadArgument("Cannot get source for a dynamically-created object.") + + if not isinstance(source_item, str): + try: + lines, first_line_no = inspect.getsourcelines(src) + except OSError: + raise commands.BadArgument("Cannot get source for a dynamically-created object.") + + lines_extension = f"#L{first_line_no}-L{first_line_no+len(lines)-1}" + else: + first_line_no = None + lines_extension = "" + + file_location = Path(filename).relative_to(Path.cwd()).as_posix() + + url = f"{Source.github}/blob/master/{file_location}{lines_extension}" + + return url, file_location, first_line_no or None + + async def build_embed(self, source_object: SourceType) -> Optional[Embed]: + """Build embed based on source object.""" + url, location, first_line = self.get_source_link(source_object) + + if isinstance(source_object, commands.Command): + if source_object.cog_name == 'Help': + title = "Help Command" + description = source_object.__doc__.splitlines()[1] + else: + description = source_object.short_doc + title = f"Command: {source_object.qualified_name}" + else: + title = f"Cog: {source_object.qualified_name}" + description = source_object.description.splitlines()[0] + + embed = Embed(title=title, description=description) + embed.set_thumbnail(url=Source.github_avatar_url) + embed.add_field(name="Source Code", value=f"[Go to GitHub]({url})") + line_text = f":{first_line}" if first_line else "" + embed.set_footer(text=f"{location}{line_text}") + + return embed + + +def setup(bot: commands.Bot) -> None: + """Load the BotSource cog.""" + bot.add_cog(BotSource(bot)) diff --git a/bot/exts/halloween/hacktober-issue-finder.py b/bot/exts/halloween/hacktober-issue-finder.py index 78acf391..9deadde9 100644 --- a/bot/exts/halloween/hacktober-issue-finder.py +++ b/bot/exts/halloween/hacktober-issue-finder.py @@ -103,7 +103,7 @@ class HacktoberIssues(commands.Cog): labels = [label["name"] for label in issue["labels"]] embed = discord.Embed(title=title) - embed.description = body + embed.description = body[:500] + '...' if len(body) > 500 else body embed.add_field(name="labels", value="\n".join(labels)) embed.url = issue_url embed.set_footer(text=issue_url) diff --git a/bot/exts/valentines/valentine_zodiac.py b/bot/exts/valentines/valentine_zodiac.py index ef9ddc78..2696999f 100644 --- a/bot/exts/valentines/valentine_zodiac.py +++ b/bot/exts/valentines/valentine_zodiac.py @@ -1,7 +1,10 @@ +import calendar +import json import logging import random -from json import load +from datetime import datetime from pathlib import Path +from typing import Tuple, Union import discord from discord.ext import commands @@ -19,37 +22,123 @@ class ValentineZodiac(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - self.zodiacs = self.load_json() + self.zodiacs, self.zodiac_fact = self.load_comp_json() @staticmethod - def load_json() -> dict: + def load_comp_json() -> Tuple[dict, dict]: """Load zodiac compatibility from static JSON resource.""" - p = Path("bot/resources/valentines/zodiac_compatibility.json") - with p.open(encoding="utf8") as json_data: - zodiacs = load(json_data) - return zodiacs - - @commands.command(name="partnerzodiac") - async def counter_zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None: - """Provides a counter compatible zodiac sign to the given user's zodiac sign.""" - try: - compatible_zodiac = random.choice(self.zodiacs[zodiac_sign.lower()]) - except KeyError: - return await ctx.send(zodiac_sign.capitalize() + " zodiac sign does not exist.") - - emoji1 = random.choice(HEART_EMOJIS) - emoji2 = random.choice(HEART_EMOJIS) - embed = discord.Embed( - title="Zodic Compatibility", - description=f'{zodiac_sign.capitalize()}{emoji1}{compatible_zodiac["Zodiac"]}\n' - f'{emoji2}Compatibility meter : {compatible_zodiac["compatibility_score"]}{emoji2}', - color=Colours.pink - ) - embed.add_field( - name=f'A letter from Dr.Zodiac {LETTER_EMOJI}', - value=compatible_zodiac['description'] - ) + explanation_file = Path("bot/resources/valentines/zodiac_explanation.json") + compatibility_file = Path("bot/resources/valentines/zodiac_compatibility.json") + with explanation_file.open(encoding="utf8") as json_data: + zodiac_fact = json.load(json_data) + for zodiac_data in zodiac_fact.values(): + zodiac_data['start_at'] = datetime.fromisoformat(zodiac_data['start_at']) + zodiac_data['end_at'] = datetime.fromisoformat(zodiac_data['end_at']) + + with compatibility_file.open(encoding="utf8") as json_data: + zodiacs = json.load(json_data) + + return zodiacs, zodiac_fact + + def generate_invalidname_embed(self, zodiac: str) -> discord.Embed: + """Returns error embed.""" + embed = discord.Embed() + embed.color = Colours.soft_red + error_msg = f"**{zodiac}** is not a valid zodiac sign, here is the list of valid zodiac signs.\n" + names = list(self.zodiac_fact) + middle_index = len(names) // 2 + first_half_names = ", ".join(names[:middle_index]) + second_half_names = ", ".join(names[middle_index:]) + embed.description = error_msg + first_half_names + ",\n" + second_half_names + log.info("Invalid zodiac name provided.") + return embed + + def zodiac_build_embed(self, zodiac: str) -> discord.Embed: + """Gives informative zodiac embed.""" + zodiac = zodiac.capitalize() + embed = discord.Embed() + embed.color = Colours.pink + if zodiac in self.zodiac_fact: + log.trace("Making zodiac embed.") + embed.title = f"__{zodiac}__" + embed.description = self.zodiac_fact[zodiac]["About"] + embed.add_field(name='__Motto__', value=self.zodiac_fact[zodiac]["Motto"], inline=False) + embed.add_field(name='__Strengths__', value=self.zodiac_fact[zodiac]["Strengths"], inline=False) + embed.add_field(name='__Weaknesses__', value=self.zodiac_fact[zodiac]["Weaknesses"], inline=False) + embed.add_field(name='__Full form__', value=self.zodiac_fact[zodiac]["full_form"], inline=False) + embed.set_thumbnail(url=self.zodiac_fact[zodiac]["url"]) + else: + embed = self.generate_invalidname_embed(zodiac) + log.trace("Successfully created zodiac information embed.") + return embed + + def zodiac_date_verifier(self, query_date: datetime) -> str: + """Returns zodiac sign by checking date.""" + for zodiac_name, zodiac_data in self.zodiac_fact.items(): + if zodiac_data["start_at"].date() <= query_date.date() <= zodiac_data["end_at"].date(): + log.trace("Zodiac name sent.") + return zodiac_name + + @commands.group(name='zodiac', invoke_without_command=True) + async def zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None: + """Provides information about zodiac sign by taking zodiac sign name as input.""" + final_embed = self.zodiac_build_embed(zodiac_sign) + await ctx.send(embed=final_embed) + log.trace("Embed successfully sent.") + + @zodiac.command(name="date") + async def date_and_month(self, ctx: commands.Context, date: int, month: Union[int, str]) -> None: + """Provides information about zodiac sign by taking month and date as input.""" + if isinstance(month, str): + month = month.capitalize() + try: + month = list(calendar.month_abbr).index(month[:3]) + log.trace('Valid month name entered by user') + except ValueError: + log.info('Invalid month name entered by user') + await ctx.send(f"Sorry, but `{month}` is not a valid month name.") + return + if (month == 1 and 1 <= date <= 19) or (month == 12 and 22 <= date <= 31): + zodiac = "capricorn" + final_embed = self.zodiac_build_embed(zodiac) + else: + try: + zodiac_sign_based_on_date = self.zodiac_date_verifier(datetime(2020, month, date)) + log.trace("zodiac sign based on month and date received.") + except ValueError as e: + final_embed = discord.Embed() + final_embed.color = Colours.soft_red + final_embed.description = f"Zodiac sign could not be found because.\n```{e}```" + log.info(f'Error in "zodiac date" command:\n{e}.') + else: + final_embed = self.zodiac_build_embed(zodiac_sign_based_on_date) + + await ctx.send(embed=final_embed) + log.trace("Embed from date successfully sent.") + + @zodiac.command(name="partnerzodiac", aliases=['partner']) + async def partner_zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None: + """Provides a random counter compatible zodiac sign to the given user's zodiac sign.""" + embed = discord.Embed() + embed.color = Colours.pink + zodiac_check = self.zodiacs.get(zodiac_sign.capitalize()) + if zodiac_check: + compatible_zodiac = random.choice(self.zodiacs[zodiac_sign.capitalize()]) + emoji1 = random.choice(HEART_EMOJIS) + emoji2 = random.choice(HEART_EMOJIS) + embed.title = "Zodiac Compatibility" + embed.description = ( + f'{zodiac_sign.capitalize()}{emoji1}{compatible_zodiac["Zodiac"]}\n' + f'{emoji2}Compatibility meter : {compatible_zodiac["compatibility_score"]}{emoji2}' + ) + embed.add_field( + name=f'A letter from Dr.Zodiac {LETTER_EMOJI}', + value=compatible_zodiac['description'] + ) + else: + embed = self.generate_invalidname_embed(zodiac_sign) await ctx.send(embed=embed) + log.trace("Embed from date successfully sent.") def setup(bot: commands.Bot) -> None: |