diff options
Diffstat (limited to 'bot/exts/halloween')
| -rw-r--r-- | bot/exts/halloween/8ball.py | 18 | ||||
| -rw-r--r-- | bot/exts/halloween/candy_collection.py | 62 | ||||
| -rw-r--r-- | bot/exts/halloween/hacktober-issue-finder.py | 77 | ||||
| -rw-r--r-- | bot/exts/halloween/hacktoberstats.py | 130 | ||||
| -rw-r--r-- | bot/exts/halloween/halloween_facts.py | 22 | ||||
| -rw-r--r-- | bot/exts/halloween/halloweenify.py | 30 | ||||
| -rw-r--r-- | bot/exts/halloween/monsterbio.py | 19 | ||||
| -rw-r--r-- | bot/exts/halloween/monstersurvey.py | 119 | ||||
| -rw-r--r-- | bot/exts/halloween/scarymovie.py | 94 | ||||
| -rw-r--r-- | bot/exts/halloween/spookyavatar.py | 52 | ||||
| -rw-r--r-- | bot/exts/halloween/spookygif.py | 26 | ||||
| -rw-r--r-- | bot/exts/halloween/spookynamerate.py | 50 | ||||
| -rw-r--r-- | bot/exts/halloween/spookyrating.py | 26 | ||||
| -rw-r--r-- | bot/exts/halloween/spookyreact.py | 43 | ||||
| -rw-r--r-- | bot/exts/halloween/timeleft.py | 11 | 
15 files changed, 350 insertions, 429 deletions
diff --git a/bot/exts/halloween/8ball.py b/bot/exts/halloween/8ball.py index 1df48fbf..a2431190 100644 --- a/bot/exts/halloween/8ball.py +++ b/bot/exts/halloween/8ball.py @@ -6,28 +6,26 @@ from pathlib import Path  from discord.ext import commands +from bot.bot import Bot +  log = logging.getLogger(__name__) -with open(Path("bot/resources/halloween/responses.json"), "r", encoding="utf8") as f: -    responses = json.load(f) +RESPONSES = json.loads(Path("bot/resources/halloween/responses.json").read_text("utf8"))  class SpookyEightBall(commands.Cog):      """Spooky Eightball answers.""" -    def __init__(self, bot: commands.Bot): -        self.bot = bot - -    @commands.command(aliases=('spooky8ball',)) +    @commands.command(aliases=("spooky8ball",))      async def spookyeightball(self, ctx: commands.Context, *, question: str) -> None:          """Responds with a random response to a question.""" -        choice = random.choice(responses['responses']) +        choice = random.choice(RESPONSES["responses"])          msg = await ctx.send(choice[0])          if len(choice) > 1:              await asyncio.sleep(random.randint(2, 5))              await msg.edit(content=f"{choice[0]} \n{choice[1]}") -def setup(bot: commands.Bot) -> None: -    """Spooky Eight Ball Cog Load.""" -    bot.add_cog(SpookyEightBall(bot)) +def setup(bot: Bot) -> None: +    """Load the Spooky Eight Ball Cog.""" +    bot.add_cog(SpookyEightBall()) diff --git a/bot/exts/halloween/candy_collection.py b/bot/exts/halloween/candy_collection.py index 0cb37ecd..4afd5913 100644 --- a/bot/exts/halloween/candy_collection.py +++ b/bot/exts/halloween/candy_collection.py @@ -6,6 +6,7 @@ import discord  from async_rediscache import RedisCache  from discord.ext import commands +from bot.bot import Bot  from bot.constants import Channels, Month  from bot.utils.decorators import in_month @@ -21,11 +22,11 @@ EMOJIS = dict(      CANDY="\N{CANDY}",      SKULL="\N{SKULL}",      MEDALS=( -        '\N{FIRST PLACE MEDAL}', -        '\N{SECOND PLACE MEDAL}', -        '\N{THIRD PLACE MEDAL}', -        '\N{SPORTS MEDAL}', -        '\N{SPORTS MEDAL}', +        "\N{FIRST PLACE MEDAL}", +        "\N{SECOND PLACE MEDAL}", +        "\N{THIRD PLACE MEDAL}", +        "\N{SPORTS MEDAL}", +        "\N{SPORTS MEDAL}",      ),  ) @@ -40,13 +41,16 @@ class CandyCollection(commands.Cog):      candy_messages = RedisCache()      skull_messages = RedisCache() -    def __init__(self, bot: commands.Bot): +    def __init__(self, bot: Bot):          self.bot = bot      @in_month(Month.OCTOBER)      @commands.Cog.listener()      async def on_message(self, message: discord.Message) -> None:          """Randomly adds candy or skull reaction to non-bot messages in the Event channel.""" +        # Ignore messages in DMs +        if not message.guild: +            return          # make sure its a human message          if message.author.bot:              return @@ -57,15 +61,15 @@ class CandyCollection(commands.Cog):          # do random check for skull first as it has the lower chance          if random.randint(1, ADD_SKULL_REACTION_CHANCE) == 1:              await self.skull_messages.set(message.id, "skull") -            return await message.add_reaction(EMOJIS['SKULL']) +            await message.add_reaction(EMOJIS["SKULL"])          # check for the candy chance next -        if random.randint(1, ADD_CANDY_REACTION_CHANCE) == 1: +        elif random.randint(1, ADD_CANDY_REACTION_CHANCE) == 1:              await self.candy_messages.set(message.id, "candy") -            return await message.add_reaction(EMOJIS['CANDY']) +            await message.add_reaction(EMOJIS["CANDY"])      @in_month(Month.OCTOBER)      @commands.Cog.listener() -    async def on_reaction_add(self, reaction: discord.Reaction, user: discord.Member) -> None: +    async def on_reaction_add(self, reaction: discord.Reaction, user: Union[discord.User, discord.Member]) -> None:          """Add/remove candies from a person if the reaction satisfies criteria."""          message = reaction.message          # check to ensure the reactor is human @@ -78,7 +82,7 @@ class CandyCollection(commands.Cog):          # if its not a candy or skull, and it is one of 10 most recent messages,          # proceed to add a skull/candy with higher chance -        if str(reaction.emoji) not in (EMOJIS['SKULL'], EMOJIS['CANDY']): +        if str(reaction.emoji) not in (EMOJIS["SKULL"], EMOJIS["CANDY"]):              recent_message_ids = map(                  lambda m: m.id,                  await self.hacktober_channel.history(limit=10).flatten() @@ -87,14 +91,14 @@ class CandyCollection(commands.Cog):                  await self.reacted_msg_chance(message)              return -        if await self.candy_messages.get(message.id) == "candy" and str(reaction.emoji) == EMOJIS['CANDY']: +        if await self.candy_messages.get(message.id) == "candy" and str(reaction.emoji) == EMOJIS["CANDY"]:              await self.candy_messages.delete(message.id)              if await self.candy_records.contains(user.id):                  await self.candy_records.increment(user.id)              else:                  await self.candy_records.set(user.id, 1) -        elif await self.skull_messages.get(message.id) == "skull" and str(reaction.emoji) == EMOJIS['SKULL']: +        elif await self.skull_messages.get(message.id) == "skull" and str(reaction.emoji) == EMOJIS["SKULL"]:              await self.skull_messages.delete(message.id)              if prev_record := await self.candy_records.get(user.id): @@ -102,7 +106,7 @@ class CandyCollection(commands.Cog):                  await self.candy_records.decrement(user.id, lost)                  if lost == prev_record: -                    await CandyCollection.send_spook_msg(user, message.channel, 'all of your') +                    await CandyCollection.send_spook_msg(user, message.channel, "all of your")                  else:                      await CandyCollection.send_spook_msg(user, message.channel, lost)              else: @@ -121,11 +125,11 @@ class CandyCollection(commands.Cog):          """          if random.randint(1, ADD_SKULL_EXISTING_REACTION_CHANCE) == 1:              await self.skull_messages.set(message.id, "skull") -            return await message.add_reaction(EMOJIS['SKULL']) +            await message.add_reaction(EMOJIS["SKULL"]) -        if random.randint(1, ADD_CANDY_EXISTING_REACTION_CHANCE) == 1: +        elif random.randint(1, ADD_CANDY_EXISTING_REACTION_CHANCE) == 1:              await self.candy_messages.set(message.id, "candy") -            return await message.add_reaction(EMOJIS['CANDY']) +            await message.add_reaction(EMOJIS["CANDY"])      @property      def hacktober_channel(self) -> discord.TextChannel: @@ -138,8 +142,10 @@ class CandyCollection(commands.Cog):      ) -> None:          """Send a spooky message."""          e = discord.Embed(colour=author.colour) -        e.set_author(name="Ghosts and Ghouls and Jack o' lanterns at night; " -                          f"I took {candies} candies and quickly took flight.") +        e.set_author( +            name="Ghosts and Ghouls and Jack o' lanterns at night; " +            f"I took {candies} candies and quickly took flight." +        )          await channel.send(embed=e)      @staticmethod @@ -149,8 +155,12 @@ class CandyCollection(commands.Cog):      ) -> None:          """An alternative spooky message sent when user has no candies in the collection."""          embed = discord.Embed(color=author.color) -        embed.set_author(name="Ghosts and Ghouls and Jack o' lanterns at night; " -                              "I tried to take your candies but you had none to begin with!") +        embed.set_author( +            name=( +                "Ghosts and Ghouls and Jack o' lanterns at night; " +                "I tried to take your candies but you had none to begin with!" +            ) +        )          await channel.send(embed=embed)      @in_month(Month.OCTOBER) @@ -167,10 +177,10 @@ class CandyCollection(commands.Cog):              )              top_five = top_sorted[:5] -            return '\n'.join( +            return "\n".join(                  f"{EMOJIS['MEDALS'][index]} <@{record[0]}>: {record[1]}"                  for index, record in enumerate(top_five) -            ) if top_five else 'No Candies' +            ) if top_five else "No Candies"          e = discord.Embed(colour=discord.Colour.blurple())          e.add_field( @@ -179,7 +189,7 @@ class CandyCollection(commands.Cog):              inline=False          )          e.add_field( -            name='\u200b', +            name="\u200b",              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...", @@ -188,6 +198,6 @@ class CandyCollection(commands.Cog):          await ctx.send(embed=e) -def setup(bot: commands.Bot) -> None: -    """Candy Collection game Cog load.""" +def setup(bot: Bot) -> None: +    """Load the Candy Collection Cog."""      bot.add_cog(CandyCollection(bot)) diff --git a/bot/exts/halloween/hacktober-issue-finder.py b/bot/exts/halloween/hacktober-issue-finder.py index 9deadde9..20a06770 100644 --- a/bot/exts/halloween/hacktober-issue-finder.py +++ b/bot/exts/halloween/hacktober-issue-finder.py @@ -3,10 +3,10 @@ import logging  import random  from typing import Dict, Optional -import aiohttp  import discord  from discord.ext import commands +from bot.bot import Bot  from bot.constants import Month, Tokens  from bot.utils.decorators import in_month @@ -25,7 +25,7 @@ if GITHUB_TOKEN := Tokens.github:  class HacktoberIssues(commands.Cog):      """Find a random hacktober python issue on GitHub.""" -    def __init__(self, bot: commands.Bot): +    def __init__(self, bot: Bot):          self.bot = bot          self.cache_normal = None          self.cache_timer_normal = datetime.datetime(1, 1, 1) @@ -41,7 +41,7 @@ class HacktoberIssues(commands.Cog):          If the command is run with beginner (`.hacktoberissues beginner`):          It will also narrow it down to the "first good issue" label.          """ -        with ctx.typing(): +        async with ctx.typing():              issues = await self.get_issues(ctx, option)              if issues is None:                  return @@ -59,40 +59,41 @@ class HacktoberIssues(commands.Cog):              log.debug("using cache")              return self.cache_normal -        async with aiohttp.ClientSession() as session: +        if option == "beginner": +            url = URL + '+label:"good first issue"' +            if self.cache_beginner is not None: +                page = random.randint(1, min(1000, self.cache_beginner["total_count"]) // 100) +                url += f"&page={page}" +        else: +            url = URL +            if self.cache_normal is not None: +                page = random.randint(1, min(1000, self.cache_normal["total_count"]) // 100) +                url += f"&page={page}" + +        log.debug(f"making api request to url: {url}") +        async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as response: +            if response.status != 200: +                log.error(f"expected 200 status (got {response.status}) by the GitHub api.") +                await ctx.send( +                    f"ERROR: expected 200 status (got {response.status}) by the GitHub api.\n" +                    f"{await response.text()}" +                ) +                return None +            data = await response.json() + +            if len(data["items"]) == 0: +                log.error(f"no issues returned by GitHub API, with url: {response.url}") +                await ctx.send(f"ERROR: no issues returned by GitHub API, with url: {response.url}") +                return None +              if option == "beginner": -                url = URL + '+label:"good first issue"' -                if self.cache_beginner is not None: -                    page = random.randint(1, min(1000, self.cache_beginner["total_count"]) // 100) -                    url += f"&page={page}" +                self.cache_beginner = data +                self.cache_timer_beginner = ctx.message.created_at              else: -                url = URL -                if self.cache_normal is not None: -                    page = random.randint(1, min(1000, self.cache_normal["total_count"]) // 100) -                    url += f"&page={page}" - -            log.debug(f"making api request to url: {url}") -            async with session.get(url, headers=REQUEST_HEADERS) as response: -                if response.status != 200: -                    log.error(f"expected 200 status (got {response.status}) from the GitHub api.") -                    await ctx.send(f"ERROR: expected 200 status (got {response.status}) from the GitHub api.") -                    await ctx.send(await response.text()) -                    return None -                data = await response.json() - -                if len(data["items"]) == 0: -                    log.error(f"no issues returned from GitHub api. with url: {response.url}") -                    await ctx.send(f"ERROR: no issues returned from GitHub api. with url: {response.url}") -                    return None - -                if option == "beginner": -                    self.cache_beginner = data -                    self.cache_timer_beginner = ctx.message.created_at -                else: -                    self.cache_normal = data -                    self.cache_timer_normal = ctx.message.created_at - -                return data +                self.cache_normal = data +                self.cache_timer_normal = ctx.message.created_at + +            return data      @staticmethod      def format_embed(issue: Dict) -> discord.Embed: @@ -103,7 +104,7 @@ class HacktoberIssues(commands.Cog):          labels = [label["name"] for label in issue["labels"]]          embed = discord.Embed(title=title) -        embed.description = body[:500] + '...' if len(body) > 500 else 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) @@ -111,6 +112,6 @@ class HacktoberIssues(commands.Cog):          return embed -def setup(bot: commands.Bot) -> None: -    """Hacktober issue finder Cog Load.""" +def setup(bot: Bot) -> None: +    """Load the HacktoberIssue finder."""      bot.add_cog(HacktoberIssues(bot)) diff --git a/bot/exts/halloween/hacktoberstats.py b/bot/exts/halloween/hacktoberstats.py index d9fc0e8a..24106a5e 100644 --- a/bot/exts/halloween/hacktoberstats.py +++ b/bot/exts/halloween/hacktoberstats.py @@ -4,21 +4,21 @@ import re  from collections import Counter  from datetime import datetime, timedelta  from typing import List, Optional, Tuple, Union +from urllib.parse import quote_plus -import aiohttp  import discord  from async_rediscache import RedisCache  from discord.ext import commands -from bot.constants import Channels, Month, NEGATIVE_REPLIES, Tokens, WHITELISTED_CHANNELS -from bot.utils.decorators import in_month, whitelist_override +from bot.bot import Bot +from bot.constants import Colours, Month, NEGATIVE_REPLIES, Tokens +from bot.utils.decorators import in_month  log = logging.getLogger(__name__)  CURRENT_YEAR = datetime.now().year  # Used to construct GH API query  PRS_FOR_SHIRT = 4  # Minimum number of PRs before a shirt is awarded  REVIEW_DAYS = 14  # number of days needed after PR can be mature -HACKTOBER_WHITELIST = WHITELISTED_CHANNELS + (Channels.hacktoberfest_2020,)  REQUEST_HEADERS = {"User-Agent": "Python Discord Hacktoberbot"}  # using repo topics API during preview period requires an accept header @@ -39,12 +39,11 @@ class HacktoberStats(commands.Cog):      # Stores mapping of user IDs and GitHub usernames      linked_accounts = RedisCache() -    def __init__(self, bot: commands.Bot): +    def __init__(self, bot: Bot):          self.bot = bot      @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)      @commands.group(name="hacktoberstats", aliases=("hackstats",), invoke_without_command=True) -    @whitelist_override(channels=HACKTOBER_WHITELIST)      async def hacktoberstats_group(self, ctx: commands.Context, github_username: str = None) -> None:          """          Display an embed for a user's Hacktoberfest contributions. @@ -72,7 +71,6 @@ class HacktoberStats(commands.Cog):      @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)      @hacktoberstats_group.command(name="link") -    @whitelist_override(channels=HACKTOBER_WHITELIST)      async def link_user(self, ctx: commands.Context, github_username: str = None) -> None:          """          Link the invoking user's Github github_username to their Discord ID. @@ -83,20 +81,19 @@ class HacktoberStats(commands.Cog):          if github_username:              if await self.linked_accounts.contains(author_id):                  old_username = await self.linked_accounts.get(author_id) -                logging.info(f"{author_id} has changed their github link from '{old_username}' to '{github_username}'") +                log.info(f"{author_id} has changed their github link from '{old_username}' to '{github_username}'")                  await ctx.send(f"{author_mention}, your GitHub username has been updated to: '{github_username}'")              else: -                logging.info(f"{author_id} has added a github link to '{github_username}'") +                log.info(f"{author_id} has added a github link to '{github_username}'")                  await ctx.send(f"{author_mention}, your GitHub username has been added")              await self.linked_accounts.set(author_id, github_username)          else: -            logging.info(f"{author_id} tried to link a GitHub account but didn't provide a username") +            log.info(f"{author_id} tried to link a GitHub account but didn't provide a username")              await ctx.send(f"{author_mention}, a GitHub username is required to link your account")      @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)      @hacktoberstats_group.command(name="unlink") -    @whitelist_override(channels=HACKTOBER_WHITELIST)      async def unlink_user(self, ctx: commands.Context) -> None:          """Remove the invoking user's account link from the log."""          author_id, author_mention = self._author_mention_from_context(ctx) @@ -138,7 +135,7 @@ class HacktoberStats(commands.Cog):              if prs:                  stats_embed = await self.build_embed(github_username, prs) -                await ctx.send('Here are some stats!', embed=stats_embed) +                await ctx.send("Here are some stats!", embed=stats_embed)              else:                  await ctx.send(f"No valid Hacktoberfest PRs found for '{github_username}'") @@ -157,7 +154,7 @@ class HacktoberStats(commands.Cog):          stats_embed = discord.Embed(              title=f"{github_username}'s Hacktoberfest", -            color=discord.Color(0x9c4af7), +            color=Colours.purple,              description=(                  f"{github_username} has made {n} valid "                  f"{self._contributionator(n)} in " @@ -188,8 +185,7 @@ class HacktoberStats(commands.Cog):          logging.info(f"Hacktoberfest PR built for GitHub user '{github_username}'")          return stats_embed -    @staticmethod -    async def get_october_prs(github_username: str) -> Optional[List[dict]]: +    async def get_october_prs(self, github_username: str) -> Optional[List[dict]]:          """          Query GitHub's API for PRs created during the month of October by github_username. @@ -212,40 +208,40 @@ class HacktoberStats(commands.Cog):          Otherwise, return empty list.          None will be returned when the GitHub user was not found.          """ -        logging.info(f"Fetching Hacktoberfest Stats for GitHub user: '{github_username}'") -        base_url = "https://api.github.com/search/issues?q=" +        log.info(f"Fetching Hacktoberfest Stats for GitHub user: '{github_username}'") +        base_url = "https://api.github.com/search/issues"          action_type = "pr"          is_query = "public"          not_query = "draft"          date_range = f"{CURRENT_YEAR}-09-30T10:00Z..{CURRENT_YEAR}-11-01T12:00Z"          per_page = "300" -        query_url = ( -            f"{base_url}" +        query_params = (              f"+type:{action_type}"              f"+is:{is_query}" -            f"+author:{github_username}" +            f"+author:{quote_plus(github_username)}"              f"+-is:{not_query}"              f"+created:{date_range}"              f"&per_page={per_page}"          ) -        logging.debug(f"GitHub query URL generated: {query_url}") -        jsonresp = await HacktoberStats._fetch_url(query_url, REQUEST_HEADERS) -        if "message" in jsonresp.keys(): +        log.debug(f"GitHub query parameters generated: {query_params}") + +        jsonresp = await self._fetch_url(base_url, REQUEST_HEADERS, {"q": query_params}) +        if "message" in jsonresp:              # One of the parameters is invalid, short circuit for now              api_message = jsonresp["errors"][0]["message"]              # Ignore logging non-existent users or users we do not have permission to see              if api_message == GITHUB_NONEXISTENT_USER_MESSAGE: -                logging.debug(f"No GitHub user found named '{github_username}'") +                log.debug(f"No GitHub user found named '{github_username}'")                  return              else: -                logging.error(f"GitHub API request for '{github_username}' failed with message: {api_message}") +                log.error(f"GitHub API request for '{github_username}' failed with message: {api_message}")              return []  # No October PRs were found due to error          if jsonresp["total_count"] == 0:              # Short circuit if there aren't any PRs -            logging.info(f"No October PRs found for GitHub user: '{github_username}'") +            log.info(f"No October PRs found for GitHub user: '{github_username}'")              return []          logging.info(f"Found {len(jsonresp['items'])} Hacktoberfest PRs for GitHub user: '{github_username}'") @@ -253,20 +249,20 @@ class HacktoberStats(commands.Cog):          oct3 = datetime(int(CURRENT_YEAR), 10, 3, 23, 59, 59, tzinfo=None)          hackto_topics = {}  # cache whether each repo has the appropriate topic (bool values)          for item in jsonresp["items"]: -            shortname = HacktoberStats._get_shortname(item["repository_url"]) +            shortname = self._get_shortname(item["repository_url"])              itemdict = {                  "repo_url": f"https://www.github.com/{shortname}",                  "repo_shortname": shortname,                  "created_at": datetime.strptime( -                    item["created_at"], r"%Y-%m-%dT%H:%M:%SZ" +                    item["created_at"], "%Y-%m-%dT%H:%M:%SZ"                  ),                  "number": item["number"]              }              # If the PR has 'invalid' or 'spam' labels, the PR must be              # either merged or approved for it to be included -            if HacktoberStats._has_label(item, ["invalid", "spam"]): -                if not await HacktoberStats._is_accepted(itemdict): +            if self._has_label(item, ["invalid", "spam"]): +                if not await self._is_accepted(itemdict):                      continue              # PRs before oct 3 no need to check for topics @@ -277,21 +273,20 @@ class HacktoberStats(commands.Cog):                  continue              # Checking PR's labels for "hacktoberfest-accepted" -            if HacktoberStats._has_label(item, "hacktoberfest-accepted"): +            if self._has_label(item, "hacktoberfest-accepted"):                  outlist.append(itemdict)                  continue              # No need to query GitHub if repo topics are fetched before already -            if shortname in hackto_topics.keys(): -                if hackto_topics[shortname]: -                    outlist.append(itemdict) -                    continue +            if hackto_topics.get(shortname): +                outlist.append(itemdict) +                continue              # Fetch topics for the PR's repo              topics_query_url = f"https://api.github.com/repos/{shortname}/topics" -            logging.debug(f"Fetching repo topics for {shortname} with url: {topics_query_url}") -            jsonresp2 = await HacktoberStats._fetch_url(topics_query_url, GITHUB_TOPICS_ACCEPT_HEADER) +            log.debug(f"Fetching repo topics for {shortname} with url: {topics_query_url}") +            jsonresp2 = await self._fetch_url(topics_query_url, GITHUB_TOPICS_ACCEPT_HEADER)              if jsonresp2.get("names") is None: -                logging.error(f"Error fetching topics for {shortname}: {jsonresp2['message']}") +                log.error(f"Error fetching topics for {shortname}: {jsonresp2['message']}")                  continue  # Assume the repo doesn't have the `hacktoberfest` topic if API  request errored              # PRs after oct 3 that doesn't have 'hacktoberfest-accepted' label @@ -301,13 +296,10 @@ class HacktoberStats(commands.Cog):                  outlist.append(itemdict)          return outlist -    @staticmethod -    async def _fetch_url(url: str, headers: dict) -> dict: +    async def _fetch_url(self, url: str, headers: dict, params: dict) -> dict:          """Retrieve API response from URL.""" -        async with aiohttp.ClientSession() as session: -            async with session.get(url, headers=headers) as resp: -                jsonresp = await resp.json() -        return jsonresp +        async with self.bot.http_session.get(url, headers=headers, params=params) as resp: +            return await resp.json()      @staticmethod      def _has_label(pr: dict, labels: Union[List[str], str]) -> bool: @@ -319,40 +311,36 @@ class HacktoberStats(commands.Cog):          """          if not pr.get("labels"):  # if PR has no labels              return False -        if (isinstance(labels, str)) and (any(label["name"].casefold() == labels for label in pr["labels"])): +        if isinstance(labels, str) and any(label["name"].casefold() == labels for label in pr["labels"]):              return True          for item in labels:              if any(label["name"].casefold() == item for label in pr["labels"]):                  return True          return False -    @staticmethod -    async def _is_accepted(pr: dict) -> bool: +    async def _is_accepted(self, pr: dict) -> bool:          """Check if a PR is merged, approved, or labelled hacktoberfest-accepted."""          # checking for merge status -        query_url = f"https://api.github.com/repos/{pr['repo_shortname']}/pulls/" -        query_url += str(pr["number"]) -        jsonresp = await HacktoberStats._fetch_url(query_url, REQUEST_HEADERS) - -        if "message" in jsonresp.keys(): -            logging.error( -                f"Error fetching PR stats for #{pr['number']} in repo {pr['repo_shortname']}:\n" -                f"{jsonresp['message']}" -            ) +        query_url = f"https://api.github.com/repos/{pr['repo_shortname']}/pulls/{pr['number']}" +        jsonresp = await self._fetch_url(query_url, REQUEST_HEADERS) + +        if message := jsonresp.get("message"): +            log.error(f"Error fetching PR stats for #{pr['number']} in repo {pr['repo_shortname']}:\n{message}")              return False -        if ("merged" in jsonresp.keys()) and jsonresp["merged"]: + +        if jsonresp.get("merged"):              return True          # checking for the label, using `jsonresp` which has the label information -        if HacktoberStats._has_label(jsonresp, "hacktoberfest-accepted"): +        if self._has_label(jsonresp, "hacktoberfest-accepted"):              return True          # checking approval          query_url += "/reviews" -        jsonresp2 = await HacktoberStats._fetch_url(query_url, REQUEST_HEADERS) +        jsonresp2 = await self._fetch_url(query_url, REQUEST_HEADERS)          if isinstance(jsonresp2, dict):              # if API request is unsuccessful it will be a dict with the error in 'message' -            logging.error( +            log.error(                  f"Error fetching PR reviews for #{pr['number']} in repo {pr['repo_shortname']}:\n"                  f"{jsonresp2['message']}"              ) @@ -363,9 +351,8 @@ class HacktoberStats(commands.Cog):          # loop through reviews and check for approval          for item in jsonresp2: -            if "status" in item.keys(): -                if item['status'] == "APPROVED": -                    return True +            if item.get("status") == "APPROVED": +                return True          return False      @staticmethod @@ -381,8 +368,7 @@ class HacktoberStats(commands.Cog):          exp = r"https?:\/\/api.github.com\/repos\/([/\-\_\.\w]+)"          return re.findall(exp, in_url)[0] -    @staticmethod -    async def _categorize_prs(prs: List[dict]) -> tuple: +    async def _categorize_prs(self, prs: List[dict]) -> tuple:          """          Categorize PRs into 'in_review' and 'accepted' and returns as a tuple. @@ -397,9 +383,9 @@ class HacktoberStats(commands.Cog):          in_review = []          accepted = []          for pr in prs: -            if (pr['created_at'] + timedelta(REVIEW_DAYS)) > now: +            if (pr["created_at"] + timedelta(REVIEW_DAYS)) > now:                  in_review.append(pr) -            elif (pr['created_at'] <= oct3) or await HacktoberStats._is_accepted(pr): +            elif (pr["created_at"] <= oct3) or await self._is_accepted(pr):                  accepted.append(pr)          return in_review, accepted @@ -438,14 +424,14 @@ class HacktoberStats(commands.Cog):              return "contributions"      @staticmethod -    def _author_mention_from_context(ctx: commands.Context) -> Tuple: +    def _author_mention_from_context(ctx: commands.Context) -> Tuple[str, str]:          """Return stringified Message author ID and mentionable string from commands.Context.""" -        author_id = str(ctx.message.author.id) -        author_mention = ctx.message.author.mention +        author_id = str(ctx.author.id) +        author_mention = ctx.author.mention          return author_id, author_mention -def setup(bot: commands.Bot) -> None: -    """Hacktoberstats Cog load.""" +def setup(bot: Bot) -> None: +    """Load the Hacktober Stats Cog."""      bot.add_cog(HacktoberStats(bot)) diff --git a/bot/exts/halloween/halloween_facts.py b/bot/exts/halloween/halloween_facts.py index 7eb6d56f..5ad8cc57 100644 --- a/bot/exts/halloween/halloween_facts.py +++ b/bot/exts/halloween/halloween_facts.py @@ -8,6 +8,8 @@ from typing import Tuple  import discord  from discord.ext import commands +from bot.bot import Bot +  log = logging.getLogger(__name__)  SPOOKY_EMOJIS = [ @@ -20,23 +22,19 @@ SPOOKY_EMOJIS = [      "\N{SKULL AND CROSSBONES}",      "\N{SPIDER WEB}",  ] -PUMPKIN_ORANGE = discord.Color(0xFF7518) +PUMPKIN_ORANGE = 0xFF7518  INTERVAL = timedelta(hours=6).total_seconds() +FACTS = json.loads(Path("bot/resources/halloween/halloween_facts.json").read_text("utf8")) +FACTS = list(enumerate(FACTS)) +  class HalloweenFacts(commands.Cog):      """A Cog for displaying interesting facts about Halloween.""" -    def __init__(self, bot: commands.Bot): -        self.bot = bot -        with open(Path("bot/resources/halloween/halloween_facts.json"), "r", encoding="utf8") as file: -            self.halloween_facts = json.load(file) -        self.facts = list(enumerate(self.halloween_facts)) -        random.shuffle(self.facts) -      def random_fact(self) -> Tuple[int, str]:          """Return a random fact from the loaded facts.""" -        return random.choice(self.facts) +        return random.choice(FACTS)      @commands.command(name="spookyfact", aliases=("halloweenfact",), brief="Get the most recent Halloween fact")      async def get_random_fact(self, ctx: commands.Context) -> None: @@ -53,6 +51,6 @@ class HalloweenFacts(commands.Cog):          return discord.Embed(title=title, description=fact, color=PUMPKIN_ORANGE) -def setup(bot: commands.Bot) -> None: -    """Halloween facts Cog load.""" -    bot.add_cog(HalloweenFacts(bot)) +def setup(bot: Bot) -> None: +    """Load the Halloween Facts Cog.""" +    bot.add_cog(HalloweenFacts()) diff --git a/bot/exts/halloween/halloweenify.py b/bot/exts/halloween/halloweenify.py index 596c6682..83cfbaa7 100644 --- a/bot/exts/halloween/halloweenify.py +++ b/bot/exts/halloween/halloweenify.py @@ -1,42 +1,40 @@  import logging -from json import load +from json import loads  from pathlib import Path  from random import choice  import discord  from discord.errors import Forbidden  from discord.ext import commands -from discord.ext.commands.cooldowns import BucketType +from discord.ext.commands import BucketType + +from bot.bot import Bot  log = logging.getLogger(__name__) +HALLOWEENIFY_DATA = loads(Path("bot/resources/halloween/halloweenify.json").read_text("utf8")) +  class Halloweenify(commands.Cog):      """A cog to change a invokers nickname to a spooky one!""" -    def __init__(self, bot: commands.Bot): -        self.bot = bot -      @commands.cooldown(1, 300, BucketType.user)      @commands.command()      async def halloweenify(self, ctx: commands.Context) -> None:          """Change your nickname into a much spookier one!"""          async with ctx.typing(): -            with open(Path("bot/resources/halloween/halloweenify.json"), "r", encoding="utf8") as f: -                data = load(f) -              # Choose a random character from our list we loaded above and set apart the nickname and image url. -            character = choice(data["characters"]) -            nickname = ''.join([nickname for nickname in character]) -            image = ''.join([character[nickname] for nickname in character]) +            character = choice(HALLOWEENIFY_DATA["characters"]) +            nickname = "".join(nickname for nickname in character) +            image = "".join(character[nickname] for nickname in character)              # Build up a Embed              embed = discord.Embed()              embed.colour = discord.Colour.dark_orange()              embed.title = "Not spooky enough?"              embed.description = ( -                f"**{ctx.author.display_name}** wasn\'t spooky enough for you? That\'s understandable, " -                f"{ctx.author.display_name} isn\'t scary at all! " +                f"**{ctx.author.display_name}** wasn't spooky enough for you? That's understandable, " +                f"{ctx.author.display_name} isn't scary at all! "                  "Let me think of something better. Hmm... I got it!\n\n "              )              embed.set_image(url=image) @@ -61,6 +59,6 @@ class Halloweenify(commands.Cog):          await ctx.send(embed=embed) -def setup(bot: commands.Bot) -> None: -    """Halloweenify Cog load.""" -    bot.add_cog(Halloweenify(bot)) +def setup(bot: Bot) -> None: +    """Load the Halloweenify Cog.""" +    bot.add_cog(Halloweenify()) diff --git a/bot/exts/halloween/monsterbio.py b/bot/exts/halloween/monsterbio.py index 016a66d1..69e898cb 100644 --- a/bot/exts/halloween/monsterbio.py +++ b/bot/exts/halloween/monsterbio.py @@ -6,20 +6,19 @@ from pathlib import Path  import discord  from discord.ext import commands +from bot.bot import Bot  from bot.constants import Colours  log = logging.getLogger(__name__) -with open(Path("bot/resources/halloween/monster.json"), "r", encoding="utf8") as f: -    TEXT_OPTIONS = json.load(f)  # Data for a mad-lib style generation of text +TEXT_OPTIONS = json.loads( +    Path("bot/resources/halloween/monster.json").read_text("utf8") +)  # Data for a mad-lib style generation of text  class MonsterBio(commands.Cog):      """A cog that generates a spooky monster biography.""" -    def __init__(self, bot: commands.Bot): -        self.bot = bot -      def generate_name(self, seeded_random: random.Random) -> str:          """Generates a name (for either monster species or monster name)."""          n_candidate_strings = seeded_random.randint(2, len(TEXT_OPTIONS["monster_type"])) @@ -28,7 +27,7 @@ class MonsterBio(commands.Cog):      @commands.command(brief="Sends your monster bio!")      async def monsterbio(self, ctx: commands.Context) -> None:          """Sends a description of a monster.""" -        seeded_random = random.Random(ctx.message.author.id)  # Seed a local Random instance rather than the system one +        seeded_random = random.Random(ctx.author.id)  # Seed a local Random instance rather than the system one          name = self.generate_name(seeded_random)          species = self.generate_name(seeded_random) @@ -39,7 +38,7 @@ class MonsterBio(commands.Cog):                  continue              options = seeded_random.sample(TEXT_OPTIONS[key], value) -            words[key] = ' '.join(options) +            words[key] = " ".join(options)          embed = discord.Embed(              title=f"{name}'s Biography", @@ -50,6 +49,6 @@ class MonsterBio(commands.Cog):          await ctx.send(embed=embed) -def setup(bot: commands.Bot) -> None: -    """Monster bio Cog load.""" -    bot.add_cog(MonsterBio(bot)) +def setup(bot: Bot) -> None: +    """Load the Monster Bio Cog.""" +    bot.add_cog(MonsterBio()) diff --git a/bot/exts/halloween/monstersurvey.py b/bot/exts/halloween/monstersurvey.py index 80196825..96cda11e 100644 --- a/bot/exts/halloween/monstersurvey.py +++ b/bot/exts/halloween/monstersurvey.py @@ -1,6 +1,6 @@  import json  import logging -import os +import pathlib  from discord import Embed  from discord.ext import commands @@ -9,8 +9,8 @@ from discord.ext.commands import Bot, Cog, Context  log = logging.getLogger(__name__)  EMOJIS = { -    'SUCCESS': u'\u2705', -    'ERROR': u'\u274C' +    "SUCCESS": u"\u2705", +    "ERROR": u"\u274C"  } @@ -23,18 +23,15 @@ class MonsterSurvey(Cog):      Users may change their vote, but only their current vote will be counted.      """ -    def __init__(self, bot: Bot): +    def __init__(self):          """Initializes values for the bot to use within the voting commands.""" -        self.bot = bot -        self.registry_location = os.path.join(os.getcwd(), 'bot', 'resources', 'halloween', 'monstersurvey.json') -        with open(self.registry_location, 'r', encoding="utf8") as jason: -            self.voter_registry = json.load(jason) +        self.registry_path = pathlib.Path("bot", "resources", "halloween", "monstersurvey.json") +        self.voter_registry = json.loads(self.registry_path.read_text("utf8"))      def json_write(self) -> None:          """Write voting results to a local JSON file."""          log.info("Saved Monster Survey Results") -        with open(self.registry_location, 'w', encoding="utf8") as jason: -            json.dump(self.voter_registry, jason, indent=2) +        self.registry_path.write_text(json.dumps(self.voter_registry, indent=2))      def cast_vote(self, id: int, monster: str) -> None:          """ @@ -43,54 +40,55 @@ class MonsterSurvey(Cog):          If the user has already voted, their existing vote is removed.          """          vr = self.voter_registry -        for m in vr.keys(): -            if id not in vr[m]['votes'] and m == monster: -                vr[m]['votes'].append(id) +        for m in vr: +            if id not in vr[m]["votes"] and m == monster: +                vr[m]["votes"].append(id)              else: -                if id in vr[m]['votes'] and m != monster: -                    vr[m]['votes'].remove(id) +                if id in vr[m]["votes"] and m != monster: +                    vr[m]["votes"].remove(id)      def get_name_by_leaderboard_index(self, n: int) -> str:          """Return the monster at the specified leaderboard index."""          n = n - 1          vr = self.voter_registry -        top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True) +        top = sorted(vr, key=lambda k: len(vr[k]["votes"]), reverse=True)          name = top[n] if n >= 0 else None          return name      @commands.group( -        name='monster', -        aliases=('mon',) +        name="monster", +        aliases=("mon",)      )      async def monster_group(self, ctx: Context) -> None:          """The base voting command. If nothing is called, then it will return an embed."""          if ctx.invoked_subcommand is None:              async with ctx.typing():                  default_embed = Embed( -                    title='Monster Voting', +                    title="Monster Voting",                      color=0xFF6800, -                    description='Vote for your favorite monster!' +                    description="Vote for your favorite monster!"                  )                  default_embed.add_field( -                    name='.monster show monster_name(optional)', -                    value='Show a specific monster. If none is listed, it will give you an error with valid choices.', -                    inline=False) +                    name=".monster show monster_name(optional)", +                    value="Show a specific monster. If none is listed, it will give you an error with valid choices.", +                    inline=False +                )                  default_embed.add_field( -                    name='.monster vote monster_name', -                    value='Vote for a specific monster. You get one vote, but can change it at any time.', +                    name=".monster vote monster_name", +                    value="Vote for a specific monster. You get one vote, but can change it at any time.",                      inline=False                  )                  default_embed.add_field( -                    name='.monster leaderboard', -                    value='Which monster has the most votes? This command will tell you.', +                    name=".monster leaderboard", +                    value="Which monster has the most votes? This command will tell you.",                      inline=False                  ) -                default_embed.set_footer(text=f"Monsters choices are: {', '.join(self.voter_registry.keys())}") +                default_embed.set_footer(text=f"Monsters choices are: {', '.join(self.voter_registry)}")              await ctx.send(embed=default_embed)      @monster_group.command( -        name='vote' +        name="vote"      )      async def monster_vote(self, ctx: Context, name: str = None) -> None:          """ @@ -111,37 +109,37 @@ class MonsterSurvey(Cog):                  name = name.lower()              vote_embed = Embed( -                name='Monster Voting', +                name="Monster Voting",                  color=0xFF6800              )              m = self.voter_registry.get(name)              if m is None: -                vote_embed.description = f'You cannot vote for {name} because it\'s not in the running.' +                vote_embed.description = f"You cannot vote for {name} because it's not in the running."                  vote_embed.add_field( -                    name='Use `.monster show {monster_name}` for more information on a specific monster', -                    value='or use `.monster vote {monster}` to cast your vote for said monster.', +                    name="Use `.monster show {monster_name}` for more information on a specific monster", +                    value="or use `.monster vote {monster}` to cast your vote for said monster.",                      inline=False                  )                  vote_embed.add_field( -                    name='You may vote for or show the following monsters:', -                    value=f"{', '.join(self.voter_registry.keys())}" +                    name="You may vote for or show the following monsters:", +                    value=", ".join(self.voter_registry.keys())                  )              else:                  self.cast_vote(ctx.author.id, name)                  vote_embed.add_field( -                    name='Vote successful!', -                    value=f'You have successfully voted for {m["full_name"]}!', +                    name="Vote successful!", +                    value=f"You have successfully voted for {m['full_name']}!",                      inline=False                  ) -                vote_embed.set_thumbnail(url=m['image']) +                vote_embed.set_thumbnail(url=m["image"])                  vote_embed.set_footer(text="Please note that any previous votes have been removed.")                  self.json_write()          await ctx.send(embed=vote_embed)      @monster_group.command( -        name='show' +        name="show"      )      async def monster_show(self, ctx: Context, name: str = None) -> None:          """Shows the named monster. If one is not named, it sends the default voting embed instead.""" @@ -159,41 +157,43 @@ class MonsterSurvey(Cog):              m = self.voter_registry.get(name)              if not m: -                await ctx.send('That monster does not exist.') +                await ctx.send("That monster does not exist.")                  await ctx.invoke(self.monster_vote)                  return -            embed = Embed(title=m['full_name'], color=0xFF6800) -            embed.add_field(name='Summary', value=m['summary']) -            embed.set_image(url=m['image']) -            embed.set_footer(text=f'To vote for this monster, type .monster vote {name}') +            embed = Embed(title=m["full_name"], color=0xFF6800) +            embed.add_field(name="Summary", value=m["summary"]) +            embed.set_image(url=m["image"]) +            embed.set_footer(text=f"To vote for this monster, type .monster vote {name}")          await ctx.send(embed=embed)      @monster_group.command( -        name='leaderboard', -        aliases=('lb',) +        name="leaderboard", +        aliases=("lb",)      )      async def monster_leaderboard(self, ctx: Context) -> None:          """Shows the current standings."""          async with ctx.typing():              vr = self.voter_registry -            top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True) -            total_votes = sum(len(m['votes']) for m in self.voter_registry.values()) +            top = sorted(vr, key=lambda k: len(vr[k]["votes"]), reverse=True) +            total_votes = sum(len(m["votes"]) for m in self.voter_registry.values())              embed = Embed(title="Monster Survey Leader Board", color=0xFF6800)              for rank, m in enumerate(top): -                votes = len(vr[m]['votes']) +                votes = len(vr[m]["votes"])                  percentage = ((votes / total_votes) * 100) if total_votes > 0 else 0 -                embed.add_field(name=f"{rank+1}. {vr[m]['full_name']}", -                                value=( -                                    f"{votes} votes. {percentage:.1f}% of total votes.\n" -                                    f"Vote for this monster by typing " -                                    f"'.monster vote {m}'\n" -                                    f"Get more information on this monster by typing " -                                    f"'.monster show {m}'" -                                ), -                                inline=False) +                embed.add_field( +                    name=f"{rank+1}. {vr[m]['full_name']}", +                    value=( +                        f"{votes} votes. {percentage:.1f}% of total votes.\n" +                        f"Vote for this monster by typing " +                        f"'.monster vote {m}'\n" +                        f"Get more information on this monster by typing " +                        f"'.monster show {m}'" +                    ), +                    inline=False +                )              embed.set_footer(text="You can also vote by their rank number. '.monster vote {number}' ") @@ -201,4 +201,5 @@ class MonsterSurvey(Cog):  def setup(bot: Bot) -> None: -    """Monster survey Cog load.""" +    """Load the Monster Survey Cog.""" +    bot.add_cog(MonsterSurvey()) diff --git a/bot/exts/halloween/scarymovie.py b/bot/exts/halloween/scarymovie.py index 0807eca6..33659fd8 100644 --- a/bot/exts/halloween/scarymovie.py +++ b/bot/exts/halloween/scarymovie.py @@ -1,25 +1,21 @@  import logging  import random -from os import environ -import aiohttp  from discord import Embed  from discord.ext import commands +from bot.bot import Bot +from bot.constants import Tokens  log = logging.getLogger(__name__) -TMDB_API_KEY = environ.get('TMDB_API_KEY') -TMDB_TOKEN = environ.get('TMDB_TOKEN') - -  class ScaryMovie(commands.Cog):      """Selects a random scary movie and embeds info into Discord chat.""" -    def __init__(self, bot: commands.Bot): +    def __init__(self, bot: Bot):          self.bot = bot -    @commands.command(name='scarymovie', alias=['smovie']) +    @commands.command(name="scarymovie", alias=["smovie"])      async def random_movie(self, ctx: commands.Context) -> None:          """Randomly select a scary movie and display information about it."""          async with ctx.typing(): @@ -28,36 +24,35 @@ class ScaryMovie(commands.Cog):          await ctx.send(embed=movie_details) -    @staticmethod -    async def select_movie() -> dict: +    async def select_movie(self) -> dict:          """Selects a random movie and returns a JSON of movie details from TMDb.""" -        url = 'https://api.themoviedb.org/4/discover/movie' +        url = "https://api.themoviedb.org/3/discover/movie"          params = { -            'with_genres': '27', -            'vote_count.gte': '5' +            "api_key": Tokens.tmdb, +            "with_genres": "27", +            "vote_count.gte": "5", +            "include_adult": "false"          }          headers = { -            'Authorization': 'Bearer ' + TMDB_TOKEN, -            'Content-Type': 'application/json;charset=utf-8' +            "Content-Type": "application/json;charset=utf-8"          }          # Get total page count of horror movies -        async with aiohttp.ClientSession() as session: -            response = await session.get(url=url, params=params, headers=headers) -            total_pages = await response.json() -            total_pages = total_pages.get('total_pages') - -            # Get movie details from one random result on a random page -            params['page'] = random.randint(1, total_pages) -            response = await session.get(url=url, params=params, headers=headers) -            response = await response.json() -            selection_id = random.choice(response.get('results')).get('id') - -            # Get full details and credits -            selection = await session.get( -                url='https://api.themoviedb.org/3/movie/' + str(selection_id), -                params={'api_key': TMDB_API_KEY, 'append_to_response': 'credits'} -            ) +        async with self.bot.http_session.get(url=url, params=params, headers=headers) as response: +            data = await response.json() +            total_pages = data.get("total_pages") + +        # Get movie details from one random result on a random page +        params["page"] = random.randint(1, total_pages) +        async with self.bot.http_session.get(url=url, params=params, headers=headers) as response: +            data = await response.json() +            selection_id = random.choice(data.get("results")).get("id") + +        # Get full details and credits +        async with self.bot.http_session.get( +            url=f"https://api.themoviedb.org/3/movie/{selection_id}", +            params={"api_key": Tokens.tmdb, "append_to_response": "credits"} +        ) as selection:              return await selection.json() @@ -67,40 +62,37 @@ class ScaryMovie(commands.Cog):          # Build the relevant URLs.          movie_id = movie.get("id")          poster_path = movie.get("poster_path") -        tmdb_url = f'https://www.themoviedb.org/movie/{movie_id}' if movie_id else None -        poster = f'https://image.tmdb.org/t/p/original{poster_path}' if poster_path else None +        tmdb_url = f"https://www.themoviedb.org/movie/{movie_id}" if movie_id else None +        poster = f"https://image.tmdb.org/t/p/original{poster_path}" if poster_path else None          # Get cast names          cast = [] -        for actor in movie.get('credits', {}).get('cast', [])[:3]: -            cast.append(actor.get('name')) +        for actor in movie.get("credits", {}).get("cast", [])[:3]: +            cast.append(actor.get("name"))          # Get director name -        director = movie.get('credits', {}).get('crew', []) +        director = movie.get("credits", {}).get("crew", [])          if director: -            director = director[0].get('name') +            director = director[0].get("name")          # Determine the spookiness rating -        rating = '' -        rating_count = movie.get('vote_average', 0) - -        if rating_count: -            rating_count /= 2 +        rating = "" +        rating_count = movie.get("vote_average", 0) / 2          for _ in range(int(rating_count)): -            rating += ':skull:' +            rating += ":skull:"          if (rating_count % 1) >= .5: -            rating += ':bat:' +            rating += ":bat:"          # Try to get year of release and runtime -        year = movie.get('release_date', [])[:4] -        runtime = movie.get('runtime') +        year = movie.get("release_date", [])[:4] +        runtime = movie.get("runtime")          runtime = f"{runtime} minutes" if runtime else None          # Not all these attributes will always be present          movie_attributes = {              "Directed by": director, -            "Starring": ', '.join(cast), +            "Starring": ", ".join(cast),              "Running time": runtime,              "Release year": year,              "Spookiness rating": rating, @@ -108,9 +100,9 @@ class ScaryMovie(commands.Cog):          embed = Embed(              colour=0x01d277, -            title='**' + movie.get('title') + '**', +            title=f"**{movie.get('title')}**",              url=tmdb_url, -            description=movie.get('overview') +            description=movie.get("overview")          )          if poster: @@ -127,6 +119,6 @@ class ScaryMovie(commands.Cog):          return embed -def setup(bot: commands.Bot) -> None: -    """Scary movie Cog load.""" +def setup(bot: Bot) -> None: +    """Load the Scary Movie Cog."""      bot.add_cog(ScaryMovie(bot)) diff --git a/bot/exts/halloween/spookyavatar.py b/bot/exts/halloween/spookyavatar.py deleted file mode 100644 index 2d7df678..00000000 --- a/bot/exts/halloween/spookyavatar.py +++ /dev/null @@ -1,52 +0,0 @@ -import logging -import os -from io import BytesIO - -import aiohttp -import discord -from PIL import Image -from discord.ext import commands - -from bot.utils.halloween import spookifications - -log = logging.getLogger(__name__) - - -class SpookyAvatar(commands.Cog): -    """A cog that spookifies an avatar.""" - -    def __init__(self, bot: commands.Bot): -        self.bot = bot - -    async def get(self, url: str) -> bytes: -        """Returns the contents of the supplied URL.""" -        async with aiohttp.ClientSession() as session: -            async with session.get(url) as resp: -                return await resp.read() - -    @commands.command(name='savatar', aliases=('spookyavatar', 'spookify'), -                      brief='Spookify an user\'s avatar.') -    async def spooky_avatar(self, ctx: commands.Context, user: discord.Member = None) -> None: -        """A command to print the user's spookified avatar.""" -        if user is None: -            user = ctx.message.author - -        async with ctx.typing(): -            embed = discord.Embed(colour=0xFF0000) -            embed.title = "Is this you or am I just really paranoid?" -            embed.set_author(name=str(user.name), icon_url=user.avatar_url) - -            image_bytes = await ctx.author.avatar_url.read() -            im = Image.open(BytesIO(image_bytes)) -            modified_im = spookifications.get_random_effect(im) -            modified_im.save(str(ctx.message.id)+'.png') -            f = discord.File(str(ctx.message.id)+'.png') -            embed.set_image(url='attachment://'+str(ctx.message.id)+'.png') - -        await ctx.send(file=f, embed=embed) -        os.remove(str(ctx.message.id)+'.png') - - -def setup(bot: commands.Bot) -> None: -    """Spooky avatar Cog load.""" -    bot.add_cog(SpookyAvatar(bot)) diff --git a/bot/exts/halloween/spookygif.py b/bot/exts/halloween/spookygif.py index f402437f..9511d407 100644 --- a/bot/exts/halloween/spookygif.py +++ b/bot/exts/halloween/spookygif.py @@ -1,38 +1,38 @@  import logging -import aiohttp  import discord  from discord.ext import commands -from bot.constants import Tokens +from bot.bot import Bot +from bot.constants import Colours, Tokens  log = logging.getLogger(__name__) +API_URL = "http://api.giphy.com/v1/gifs/random" +  class SpookyGif(commands.Cog):      """A cog to fetch a random spooky gif from the web!""" -    def __init__(self, bot: commands.Bot): +    def __init__(self, bot: Bot):          self.bot = bot      @commands.command(name="spookygif", aliases=("sgif", "scarygif"))      async def spookygif(self, ctx: commands.Context) -> None:          """Fetches a random gif from the GIPHY API and responds with it."""          async with ctx.typing(): -            async with aiohttp.ClientSession() as session: -                params = {'api_key': Tokens.giphy, 'tag': 'halloween', 'rating': 'g'} -                # Make a GET request to the Giphy API to get a random halloween gif. -                async with session.get('http://api.giphy.com/v1/gifs/random', params=params) as resp: -                    data = await resp.json() -                url = data['data']['image_url'] +            params = {"api_key": Tokens.giphy, "tag": "halloween", "rating": "g"} +            # Make a GET request to the Giphy API to get a random halloween gif. +            async with self.bot.http_session.get(API_URL, params=params) as resp: +                data = await resp.json() +            url = data["data"]["image_url"] -                embed = discord.Embed(colour=0x9b59b6) -                embed.title = "A spooooky gif!" -                embed.set_image(url=url) +            embed = discord.Embed(title="A spooooky gif!", colour=Colours.purple) +            embed.set_image(url=url)          await ctx.send(embed=embed) -def setup(bot: commands.Bot) -> None: +def setup(bot: Bot) -> None:      """Spooky GIF Cog load."""      bot.add_cog(SpookyGif(bot)) diff --git a/bot/exts/halloween/spookynamerate.py b/bot/exts/halloween/spookynamerate.py index e2950343..3d6d95fa 100644 --- a/bot/exts/halloween/spookynamerate.py +++ b/bot/exts/halloween/spookynamerate.py @@ -6,14 +6,15 @@ from datetime import datetime, timedelta  from logging import getLogger  from os import getenv  from pathlib import Path -from typing import Dict, Union +from typing import Union  from async_rediscache import RedisCache  from discord import Embed, Reaction, TextChannel, User  from discord.colour import Colour  from discord.ext import tasks -from discord.ext.commands import Bot, Cog, Context, group +from discord.ext.commands import Cog, Context, group +from bot.bot import Bot  from bot.constants import Channels, Client, Colours, Month  from bot.utils.decorators import InMonthCheckFailure @@ -34,7 +35,7 @@ ADDED_MESSAGES = [  ]  PING = "<@{id}>" -EMOJI_MESSAGE = "\n".join([f"- {emoji} {val}" for emoji, val in EMOJIS_VAL.items()]) +EMOJI_MESSAGE = "\n".join(f"- {emoji} {val}" for emoji, val in EMOJIS_VAL.items())  HELP_MESSAGE_DICT = {      "title": "Spooky Name Rate",      "description": f"Help for the `{Client.prefix}spookynamerate` command", @@ -64,6 +65,11 @@ HELP_MESSAGE_DICT = {      ],  } +# The names are from https://www.mockaroo.com/ +NAMES = json.loads(Path("bot/resources/halloween/spookynamerate_names.json").read_text("utf8")) +FIRST_NAMES = NAMES["first_names"] +LAST_NAMES = NAMES["last_names"] +  class SpookyNameRate(Cog):      """ @@ -80,21 +86,13 @@ class SpookyNameRate(Cog):      # The data cache stores small information such as the current name that is going on and whether it is the first time      # the bot is running      data = RedisCache() -    debug = getenv('SPOOKYNAMERATE_DEBUG', False)  # Enable if you do not want to limit the commands to October or if +    debug = getenv("SPOOKYNAMERATE_DEBUG", False)  # Enable if you do not want to limit the commands to October or if      # you do not want to wait till 12 UTC. Note: if debug is enabled and you run `.cogs reload spookynamerate`, it      # will automatically start the scoring and announcing the result (without waiting for 12, so do not expect it to.).      # Also, it won't wait for the two hours (when the poll closes).      def __init__(self, bot: Bot) -> None:          self.bot = bot - -        names_data = self.load_json( -            Path("bot", "resources", "halloween", "spookynamerate_names.json") -        ) -        self.first_names = names_data["first_names"] -        self.last_names = names_data["last_names"] -        # the names are from https://www.mockaroo.com/ -          self.name = None          self.bot.loop.create_task(self.load_vars()) @@ -116,7 +114,7 @@ class SpookyNameRate(Cog):          """Get help on the Spooky Name Rate game."""          await ctx.send(embed=Embed.from_dict(HELP_MESSAGE_DICT)) -    @spooky_name_rate.command(name="list", aliases=["all", "entries"]) +    @spooky_name_rate.command(name="list", aliases=("all", "entries"))      async def list_entries(self, ctx: Context) -> None:          """Send all the entries up till now in a single embed."""          await ctx.send(embed=await self.get_responses_list(final=False)) @@ -133,18 +131,16 @@ class SpookyNameRate(Cog):              "add an entry."          ) -    @spooky_name_rate.command(name="add", aliases=["register"]) +    @spooky_name_rate.command(name="add", aliases=("register",))      async def add_name(self, ctx: Context, *, name: str) -> None:          """Use this command to add/register your spookified name."""          if self.poll: -            logger.info(f"{ctx.message.author} tried to add a name, but the poll had already started.") +            logger.info(f"{ctx.author} tried to add a name, but the poll had already started.")              await ctx.send("Sorry, the poll has started! You can try and participate in the next round though!")              return -        message = ctx.message -          for data in (json.loads(user_data) for _, user_data in await self.messages.items()): -            if data["author"] == message.author.id: +            if data["author"] == ctx.author.id:                  await ctx.send(                      "But you have already added an entry! Type "                      f"`{self.bot.command_prefix}spookynamerate " @@ -156,14 +152,14 @@ class SpookyNameRate(Cog):                  await ctx.send("TOO LATE. Someone has already added this name.")                  return -        msg = await (await self.get_channel()).send(f"{message.author.mention} added the name {name!r}!") +        msg = await (await self.get_channel()).send(f"{ctx.author.mention} added the name {name!r}!")          await self.messages.set(              msg.id,              json.dumps(                  {                      "name": name, -                    "author": message.author.id, +                    "author": ctx.author.id,                      "score": 0,                  }              ), @@ -172,7 +168,7 @@ class SpookyNameRate(Cog):          for emoji in EMOJIS_VAL:              await msg.add_reaction(emoji) -        logger.info(f"{message.author} added the name {name!r}") +        logger.info(f"{ctx.author} added the name {name!r}")      @spooky_name_rate.command(name="delete")      async def delete_name(self, ctx: Context) -> None: @@ -185,7 +181,7 @@ class SpookyNameRate(Cog):              if ctx.author.id == data["author"]:                  await self.messages.delete(message_id) -                await ctx.send(f'Name deleted successfully ({data["name"]!r})!') +                await ctx.send(f"Name deleted successfully ({data['name']!r})!")                  return          await ctx.send( @@ -303,7 +299,7 @@ class SpookyNameRate(Cog):                  await self.messages.clear()  # reset the messages          # send the next name -        self.name = f"{random.choice(self.first_names)} {random.choice(self.last_names)}" +        self.name = f"{random.choice(FIRST_NAMES)} {random.choice(LAST_NAMES)}"          await self.data.set("name", self.name)          await channel.send( @@ -370,12 +366,6 @@ class SpookyNameRate(Cog):          return channel      @staticmethod -    def load_json(file: Path) -> Dict[str, str]: -        """Loads a JSON file and returns its contents.""" -        with file.open("r", encoding="utf-8") as f: -            return json.load(f) - -    @staticmethod      def in_allowed_month() -> bool:          """Returns whether running in the limited month."""          if SpookyNameRate.debug: @@ -397,5 +387,5 @@ class SpookyNameRate(Cog):  def setup(bot: Bot) -> None: -    """Loads the SpookyNameRate Cog.""" +    """Load the SpookyNameRate Cog."""      bot.add_cog(SpookyNameRate(bot)) diff --git a/bot/exts/halloween/spookyrating.py b/bot/exts/halloween/spookyrating.py index 6f069f8c..105d2164 100644 --- a/bot/exts/halloween/spookyrating.py +++ b/bot/exts/halloween/spookyrating.py @@ -3,24 +3,24 @@ import json  import logging  import random  from pathlib import Path +from typing import Dict  import discord  from discord.ext import commands +from bot.bot import Bot  from bot.constants import Colours  log = logging.getLogger(__name__) -with Path("bot/resources/halloween/spooky_rating.json").open(encoding="utf8") as file: -    SPOOKY_DATA = json.load(file) -    SPOOKY_DATA = sorted((int(key), value) for key, value in SPOOKY_DATA.items()) +data: Dict[str, Dict[str, str]] = json.loads(Path("bot/resources/halloween/spooky_rating.json").read_text("utf8")) +SPOOKY_DATA = sorted((int(key), value) for key, value in data.items())  class SpookyRating(commands.Cog):      """A cog for calculating one's spooky rating.""" -    def __init__(self, bot: commands.Bot): -        self.bot = bot +    def __init__(self):          self.local_random = random.Random()      @commands.command() @@ -46,21 +46,21 @@ class SpookyRating(commands.Cog):          _, data = SPOOKY_DATA[index]          embed = discord.Embed( -            title=data['title'], -            description=f'{who} scored {spooky_percent}%!', +            title=data["title"], +            description=f"{who} scored {spooky_percent}%!",              color=Colours.orange          )          embed.add_field( -            name='A whisper from Satan', -            value=data['text'] +            name="A whisper from Satan", +            value=data["text"]          )          embed.set_thumbnail( -            url=data['image'] +            url=data["image"]          )          await ctx.send(embed=embed) -def setup(bot: commands.Bot) -> None: -    """Spooky Rating Cog load.""" -    bot.add_cog(SpookyRating(bot)) +def setup(bot: Bot) -> None: +    """Load the Spooky Rating Cog.""" +    bot.add_cog(SpookyRating()) diff --git a/bot/exts/halloween/spookyreact.py b/bot/exts/halloween/spookyreact.py index b335df75..25e783f4 100644 --- a/bot/exts/halloween/spookyreact.py +++ b/bot/exts/halloween/spookyreact.py @@ -2,21 +2,22 @@ import logging  import re  import discord -from discord.ext.commands import Bot, Cog +from discord.ext.commands import Cog +from bot.bot import Bot  from bot.constants import Month  from bot.utils.decorators import in_month  log = logging.getLogger(__name__)  SPOOKY_TRIGGERS = { -    'spooky': (r"\bspo{2,}ky\b", "\U0001F47B"), -    'skeleton': (r"\bskeleton\b", "\U0001F480"), -    'doot': (r"\bdo{2,}t\b", "\U0001F480"), -    'pumpkin': (r"\bpumpkin\b", "\U0001F383"), -    'halloween': (r"\bhalloween\b", "\U0001F383"), -    'jack-o-lantern': (r"\bjack-o-lantern\b", "\U0001F383"), -    'danger': (r"\bdanger\b", "\U00002620") +    "spooky": (r"\bspo{2,}ky\b", "\U0001F47B"), +    "skeleton": (r"\bskeleton\b", "\U0001F480"), +    "doot": (r"\bdo{2,}t\b", "\U0001F480"), +    "pumpkin": (r"\bpumpkin\b", "\U0001F383"), +    "halloween": (r"\bhalloween\b", "\U0001F383"), +    "jack-o-lantern": (r"\bjack-o-lantern\b", "\U0001F383"), +    "danger": (r"\bdanger\b", "\U00002620")  } @@ -28,20 +29,20 @@ class SpookyReact(Cog):      @in_month(Month.OCTOBER)      @Cog.listener() -    async def on_message(self, ctx: discord.Message) -> None: +    async def on_message(self, message: discord.Message) -> None:          """Triggered when the bot sees a message in October.""" -        for trigger in SPOOKY_TRIGGERS.keys(): -            trigger_test = re.search(SPOOKY_TRIGGERS[trigger][0], ctx.content.lower()) +        for name, trigger in SPOOKY_TRIGGERS.items(): +            trigger_test = re.search(trigger[0], message.content.lower())              if trigger_test:                  # Check message for bot replies and/or command invocations                  # Short circuit if they're found, logging is handled in _short_circuit_check -                if await self._short_circuit_check(ctx): +                if await self._short_circuit_check(message):                      return                  else: -                    await ctx.add_reaction(SPOOKY_TRIGGERS[trigger][1]) -                    logging.info(f"Added '{trigger}' reaction to message ID: {ctx.id}") +                    await message.add_reaction(trigger[1]) +                    log.info(f"Added {name!r} reaction to message ID: {message.id}") -    async def _short_circuit_check(self, ctx: discord.Message) -> bool: +    async def _short_circuit_check(self, message: discord.Message) -> bool:          """          Short-circuit helper check. @@ -50,20 +51,20 @@ class SpookyReact(Cog):            * prefix is not None          """          # Check for self reaction -        if ctx.author == self.bot.user: -            logging.debug(f"Ignoring reactions on self message. Message ID: {ctx.id}") +        if message.author == self.bot.user: +            log.debug(f"Ignoring reactions on self message. Message ID: {message.id}")              return True          # Check for command invocation          # Because on_message doesn't give a full Context object, generate one first -        tmp_ctx = await self.bot.get_context(ctx) -        if tmp_ctx.prefix: -            logging.debug(f"Ignoring reactions on command invocation. Message ID: {ctx.id}") +        ctx = await self.bot.get_context(message) +        if ctx.prefix: +            log.debug(f"Ignoring reactions on command invocation. Message ID: {message.id}")              return True          return False  def setup(bot: Bot) -> None: -    """Spooky reaction Cog load.""" +    """Load the Spooky Reaction Cog."""      bot.add_cog(SpookyReact(bot)) diff --git a/bot/exts/halloween/timeleft.py b/bot/exts/halloween/timeleft.py index 47adb09b..e80025dc 100644 --- a/bot/exts/halloween/timeleft.py +++ b/bot/exts/halloween/timeleft.py @@ -4,15 +4,14 @@ from typing import Tuple  from discord.ext import commands +from bot.bot import Bot +  log = logging.getLogger(__name__)  class TimeLeft(commands.Cog):      """A Cog that tells you how long left until Hacktober is over!""" -    def __init__(self, bot: commands.Bot): -        self.bot = bot -      def in_hacktober(self) -> bool:          """Return True if the current time is within Hacktoberfest."""          _, end, start = self.load_date() @@ -64,6 +63,6 @@ class TimeLeft(commands.Cog):              ) -def setup(bot: commands.Bot) -> None: -    """Cog load.""" -    bot.add_cog(TimeLeft(bot)) +def setup(bot: Bot) -> None: +    """Load the Time Left Cog.""" +    bot.add_cog(TimeLeft())  |