aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts
diff options
context:
space:
mode:
Diffstat (limited to 'bot/exts')
-rw-r--r--bot/exts/easter/save_the_planet.py29
-rw-r--r--bot/exts/evergreen/emoji_count.py91
-rw-r--r--bot/exts/evergreen/fun.py22
-rw-r--r--bot/exts/evergreen/minesweeper.py12
-rw-r--r--bot/exts/evergreen/snakes/_snakes_cog.py4
-rw-r--r--bot/exts/evergreen/source.py109
-rw-r--r--bot/exts/halloween/hacktober-issue-finder.py2
-rw-r--r--bot/exts/valentines/valentine_zodiac.py145
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: