diff options
| author | 2020-10-21 10:00:02 +0200 | |
|---|---|---|
| committer | 2020-10-21 10:00:02 +0200 | |
| commit | 1f2460bb8e570fe80dbe6788446a97ca1933251a (patch) | |
| tree | f725d60eb2c5a4fd6204fa7793bb7c47adae8fc4 /bot/exts | |
| parent | fix some bugs and allow topics caching (diff) | |
| parent | Merge pull request #507 from python-discord/roll-prefix-msg (diff) | |
Merge branch 'master' into master
Diffstat (limited to 'bot/exts')
| -rw-r--r-- | bot/exts/easter/easter_riddle.py | 6 | ||||
| -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 | 24 | ||||
| -rw-r--r-- | bot/exts/evergreen/githubinfo.py | 98 | ||||
| -rw-r--r-- | bot/exts/evergreen/issues.py | 5 | ||||
| -rw-r--r-- | bot/exts/evergreen/minesweeper.py | 12 | ||||
| -rw-r--r-- | bot/exts/evergreen/movie.py | 5 | ||||
| -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/evergreen/trivia_quiz.py | 6 | ||||
| -rw-r--r-- | bot/exts/halloween/hacktober-issue-finder.py | 2 | ||||
| -rw-r--r-- | bot/exts/halloween/scarymovie.py | 3 | ||||
| -rw-r--r-- | bot/exts/valentines/movie_generator.py | 2 | ||||
| -rw-r--r-- | bot/exts/valentines/valentine_zodiac.py | 145 | 
15 files changed, 483 insertions, 58 deletions
diff --git a/bot/exts/easter/easter_riddle.py b/bot/exts/easter/easter_riddle.py index 8977534f..3c612eb1 100644 --- a/bot/exts/easter/easter_riddle.py +++ b/bot/exts/easter/easter_riddle.py @@ -22,7 +22,7 @@ class EasterRiddle(commands.Cog):      def __init__(self, bot: commands.Bot):          self.bot = bot -        self.winners = [] +        self.winners = set()          self.correct = ""          self.current_channel = None @@ -79,7 +79,7 @@ class EasterRiddle(commands.Cog):          await ctx.send(content, embed=answer_embed) -        self.winners = [] +        self.winners.clear()          self.current_channel = None      @commands.Cog.listener() @@ -92,7 +92,7 @@ class EasterRiddle(commands.Cog):              return          if message.content.lower() == self.correct.lower(): -            self.winners.append(message.author.mention) +            self.winners.add(message.author.mention)  def setup(bot: commands.Bot) -> None: 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..101725da 100644 --- a/bot/exts/evergreen/fun.py +++ b/bot/exts/evergreen/fun.py @@ -7,10 +7,10 @@ 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 +from bot.constants import Client, Colours, Emojis  log = logging.getLogger(__name__) @@ -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(f"`{Client.prefix}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/githubinfo.py b/bot/exts/evergreen/githubinfo.py new file mode 100644 index 00000000..2e38e3ab --- /dev/null +++ b/bot/exts/evergreen/githubinfo.py @@ -0,0 +1,98 @@ +import logging +import random +from datetime import datetime +from typing import Optional + +import discord +from discord.ext import commands +from discord.ext.commands.cooldowns import BucketType + +from bot.constants import NEGATIVE_REPLIES + +log = logging.getLogger(__name__) + + +class GithubInfo(commands.Cog): +    """Fetches info from GitHub.""" + +    def __init__(self, bot: commands.Bot): +        self.bot = bot + +    async def fetch_data(self, url: str) -> dict: +        """Retrieve data as a dictionary.""" +        async with self.bot.http_session.get(url) as r: +            return await r.json() + +    @commands.command(name='github', aliases=['gh']) +    @commands.cooldown(1, 60, BucketType.user) +    async def get_github_info(self, ctx: commands.Context, username: Optional[str]) -> None: +        """ +        Fetches a user's GitHub information. + +        Username is optional and sends the help command if not specified. +        """ +        if username is None: +            await ctx.invoke(self.bot.get_command('help'), 'github') +            ctx.command.reset_cooldown(ctx) +            return + +        async with ctx.typing(): +            user_data = await self.fetch_data(f"https://api.github.com/users/{username}") + +            # User_data will not have a message key if the user exists +            if user_data.get('message') is not None: +                await ctx.send(embed=discord.Embed(title=random.choice(NEGATIVE_REPLIES), +                                                   description=f"The profile for `{username}` was not found.", +                                                   colour=discord.Colour.red())) +                return + +            org_data = await self.fetch_data(user_data['organizations_url']) +            orgs = [f"[{org['login']}](https://github.com/{org['login']})" for org in org_data] +            orgs_to_add = ' | '.join(orgs) + +            gists = user_data['public_gists'] + +            # Forming blog link +            if user_data['blog'].startswith("http"):  # Blog link is complete +                blog = user_data['blog'] +            elif user_data['blog']:  # Blog exists but the link is not complete +                blog = f"https://{user_data['blog']}" +            else: +                blog = "No website link available" + +            embed = discord.Embed( +                title=f"`{user_data['login']}`'s GitHub profile info", +                description=f"```{user_data['bio']}```\n" if user_data['bio'] is not None else "", +                colour=0x7289da, +                url=user_data['html_url'], +                timestamp=datetime.strptime(user_data['created_at'], "%Y-%m-%dT%H:%M:%SZ") +            ) +            embed.set_thumbnail(url=user_data['avatar_url']) +            embed.set_footer(text="Account created at") + +            if user_data['type'] == "User": + +                embed.add_field(name="Followers", +                                value=f"[{user_data['followers']}]({user_data['html_url']}?tab=followers)") +                embed.add_field(name="\u200b", value="\u200b") +                embed.add_field(name="Following", +                                value=f"[{user_data['following']}]({user_data['html_url']}?tab=following)") + +            embed.add_field(name="Public repos", +                            value=f"[{user_data['public_repos']}]({user_data['html_url']}?tab=repositories)") +            embed.add_field(name="\u200b", value="\u200b") + +            if user_data['type'] == "User": +                embed.add_field(name="Gists", value=f"[{gists}](https://gist.github.com/{username})") + +                embed.add_field(name=f"Organization{'s' if len(orgs)!=1 else ''}", +                                value=orgs_to_add if orgs else "No organizations") +                embed.add_field(name="\u200b", value="\u200b") +            embed.add_field(name="Website", value=blog) + +        await ctx.send(embed=embed) + + +def setup(bot: commands.Bot) -> None: +    """Adding the cog to the bot.""" +    bot.add_cog(GithubInfo(bot)) diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py index 5a5c82e7..97ee6a12 100644 --- a/bot/exts/evergreen/issues.py +++ b/bot/exts/evergreen/issues.py @@ -38,7 +38,7 @@ class Issues(commands.Cog):      ) -> None:          """Command to retrieve issue(s) from a GitHub repository."""          links = [] -        numbers = set(numbers) +        numbers = set(numbers)  # Convert from list to set to remove duplicates, if any          if not numbers:              await ctx.invoke(self.bot.get_command('help'), 'issue') @@ -53,8 +53,7 @@ class Issues(commands.Cog):              await ctx.send(embed=embed)              return -        for number in set(numbers): -            # Convert from list to set to remove duplicates, if any. +        for number in numbers:              url = f"https://api.github.com/repos/{user}/{repository}/issues/{number}"              merge_url = f"https://api.github.com/repos/{user}/{repository}/pulls/{number}/merge" 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/movie.py b/bot/exts/evergreen/movie.py index 93aeef30..340a5724 100644 --- a/bot/exts/evergreen/movie.py +++ b/bot/exts/evergreen/movie.py @@ -190,7 +190,10 @@ class Movie(Cog):      async def get_embed(self, name: str) -> Embed:          """Return embed of random movies. Uses name in title.""" -        return Embed(title=f'Random {name} Movies').set_footer(text='Powered by TMDB (themoviedb.org)') +        embed = Embed(title=f"Random {name} Movies") +        embed.set_footer(text="This product uses the TMDb API but is not endorsed or certified by TMDb.") +        embed.set_thumbnail(url="https://i.imgur.com/LtFtC8H.png") +        return embed  def setup(bot: Bot) -> None: 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/evergreen/trivia_quiz.py b/bot/exts/evergreen/trivia_quiz.py index 8dceceac..fe692c2a 100644 --- a/bot/exts/evergreen/trivia_quiz.py +++ b/bot/exts/evergreen/trivia_quiz.py @@ -121,8 +121,10 @@ class TriviaQuiz(commands.Cog):              # A function to check whether user input is the correct answer(close to the right answer)              def check(m: discord.Message) -> bool: -                ratio = fuzz.ratio(answer.lower(), m.content.lower()) -                return ratio > 85 and m.channel == ctx.channel +                return ( +                    m.channel == ctx.channel +                    and fuzz.ratio(answer.lower(), m.content.lower()) > 85 +                )              try:                  msg = await self.bot.wait_for('message', check=check, timeout=10) 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/halloween/scarymovie.py b/bot/exts/halloween/scarymovie.py index c80e0298..0807eca6 100644 --- a/bot/exts/halloween/scarymovie.py +++ b/bot/exts/halloween/scarymovie.py @@ -121,7 +121,8 @@ class ScaryMovie(commands.Cog):              if value:                  embed.add_field(name=name, value=value) -        embed.set_footer(text='powered by themoviedb.org') +        embed.set_footer(text="This product uses the TMDb API but is not endorsed or certified by TMDb.") +        embed.set_thumbnail(url="https://i.imgur.com/LtFtC8H.png")          return embed diff --git a/bot/exts/valentines/movie_generator.py b/bot/exts/valentines/movie_generator.py index 0843175a..4df9e0d5 100644 --- a/bot/exts/valentines/movie_generator.py +++ b/bot/exts/valentines/movie_generator.py @@ -48,6 +48,8 @@ class RomanceMovieFinder(commands.Cog):                  embed.set_image(url=f"http://image.tmdb.org/t/p/w200/{selected_movie['poster_path']}")                  embed.add_field(name="Release date :clock1:", value=selected_movie["release_date"])                  embed.add_field(name="Rating :star2:", value=selected_movie["vote_average"]) +                embed.set_footer(text="This product uses the TMDb API but is not endorsed or certified by TMDb.") +                embed.set_thumbnail(url="https://i.imgur.com/LtFtC8H.png")                  await ctx.send(embed=embed)              except KeyError:                  warning_message = "A KeyError was raised while fetching information on the movie. The API service" \ 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:  |