diff options
Diffstat (limited to 'bot/exts')
70 files changed, 529 insertions, 546 deletions
| diff --git a/bot/exts/avatar_modification/_effects.py b/bot/exts/avatar_modification/_effects.py index f1c2e6d1..fcc62ebb 100644 --- a/bot/exts/avatar_modification/_effects.py +++ b/bot/exts/avatar_modification/_effects.py @@ -1,8 +1,8 @@  import math  import random +from collections.abc import Callable  from io import BytesIO  from pathlib import Path -from typing import Callable, Optional  import discord  from PIL import Image, ImageDraw, ImageOps @@ -55,7 +55,7 @@ class PfpEffects:      @staticmethod      def crop_avatar_circle(avatar: Image.Image) -> Image.Image: -        """This crops the avatar given into a circle.""" +        """Crop the avatar given into a circle."""          mask = Image.new("L", avatar.size, 0)          draw = ImageDraw.Draw(mask)          draw.ellipse((0, 0) + avatar.size, fill=255) @@ -64,7 +64,7 @@ class PfpEffects:      @staticmethod      def crop_ring(ring: Image.Image, px: int) -> Image.Image: -        """This crops the given ring into a circle.""" +        """Crop the given ring into a circle."""          mask = Image.new("L", ring.size, 0)          draw = ImageDraw.Draw(mask)          draw.ellipse((0, 0) + ring.size, fill=255) @@ -108,7 +108,7 @@ class PfpEffects:          return image      @staticmethod -    def easterify_effect(image: Image.Image, overlay_image: Optional[Image.Image] = None) -> Image.Image: +    def easterify_effect(image: Image.Image, overlay_image: Image.Image | None = None) -> Image.Image:          """          Applies the easter effect to the given image. diff --git a/bot/exts/avatar_modification/avatar_modify.py b/bot/exts/avatar_modification/avatar_modify.py index 6d1f26f6..4eae269f 100644 --- a/bot/exts/avatar_modification/avatar_modify.py +++ b/bot/exts/avatar_modification/avatar_modify.py @@ -4,9 +4,10 @@ import logging  import math  import string  import unicodedata +from collections.abc import Callable  from concurrent.futures import ThreadPoolExecutor  from pathlib import Path -from typing import Callable, Optional, TypeVar, Union +from typing import TypeVar  import discord  from discord.ext import commands @@ -64,7 +65,7 @@ class AvatarModify(commands.Cog):      def __init__(self, bot: Bot):          self.bot = bot -    async def _fetch_user(self, user_id: int) -> Optional[discord.User]: +    async def _fetch_user(self, user_id: int) -> discord.User | None:          """          Fetches a user and handles errors. @@ -120,7 +121,7 @@ class AvatarModify(commands.Cog):          await ctx.send(embed=embed, file=file)      @avatar_modify.command(name="reverse", root_aliases=("reverse",)) -    async def reverse(self, ctx: commands.Context, *, text: Optional[str]) -> None: +    async def reverse(self, ctx: commands.Context, *, text: str | None) -> None:          """          Reverses the sent text. @@ -157,9 +158,9 @@ class AvatarModify(commands.Cog):              await ctx.send(embed=embed, file=file)      @avatar_modify.command(aliases=("easterify",), root_aliases=("easterify", "avatareasterify")) -    async def avatareasterify(self, ctx: commands.Context, *colours: Union[discord.Colour, str]) -> None: +    async def avatareasterify(self, ctx: commands.Context, *colours: discord.Colour | str) -> None:          """ -        This "Easterifies" the user's avatar. +        Easterify the user's avatar.          Given colours will produce a personalised egg in the corner, similar to the egg_decorate command.          If colours are not given, a nice little chocolate bunny will sit in the corner. @@ -168,13 +169,14 @@ class AvatarModify(commands.Cog):          """          async def send(*args, **kwargs) -> str:              """ -            This replaces the original ctx.send. +            Replace the original ctx.send.              When invoking the egg decorating command, the egg itself doesn't print to to the channel. -            Returns the message content so that if any errors occur, the error message can be output. +            Return the message content so that if any errors occur, the error message can be output.              """              if args:                  return args[0] +            return None          async with ctx.typing():              user = await self._fetch_user(ctx.author.id) @@ -248,11 +250,11 @@ class AvatarModify(commands.Cog):      )      async def prideavatar(self, ctx: commands.Context, option: str = "lgbt", pixels: int = 64) -> None:          """ -        This surrounds an avatar with a border of a specified LGBT flag. +        Surround an avatar with a border of a specified LGBT flag. -        This defaults to the LGBT rainbow flag if none is given. +        Default to the LGBT rainbow flag if none is given.          The amount of pixels can be given which determines the thickness of the flag border. -        This has a maximum of 512px and defaults to a 64px border. +        A maximum of 512px is enforced, defaults to a 64px border.          The full image is 1024x1024.          """          option = option.lower() @@ -272,7 +274,7 @@ class AvatarModify(commands.Cog):      @prideavatar.command()      async def flags(self, ctx: commands.Context) -> None: -        """This lists the flags that can be used with the prideavatar command.""" +        """Lists the flags that can be used with the prideavatar command."""          choices = sorted(set(GENDER_OPTIONS.values()))          options = "⢠" + "\n⢠".join(choices)          embed = discord.Embed( @@ -288,7 +290,7 @@ class AvatarModify(commands.Cog):          brief="Spookify a user's avatar."      )      async def spookyavatar(self, ctx: commands.Context) -> None: -        """This "spookifies" the user's avatar, with a random *spooky* effect.""" +        """Spookify the user's avatar, with a random *spooky* effect."""          user = await self._fetch_user(ctx.author.id)          if not user:              await ctx.send(f"{Emojis.cross_mark} Could not get user info.") diff --git a/bot/exts/core/error_handler.py b/bot/exts/core/error_handler.py index 04ab55d7..81b923fd 100644 --- a/bot/exts/core/error_handler.py +++ b/bot/exts/core/error_handler.py @@ -2,14 +2,13 @@ import logging  import math  import random  from collections.abc import Iterable -from typing import Union  from discord import Embed, Message  from discord.ext import commands  from sentry_sdk import push_scope  from bot.bot import Bot -from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES, RedirectOutput +from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES  from bot.utils.commands import get_command_suggestions  from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure  from bot.utils.exceptions import APIError, MovedCommandError, UserNotPlayingError @@ -17,6 +16,7 @@ from bot.utils.exceptions import APIError, MovedCommandError, UserNotPlayingErro  log = logging.getLogger(__name__) +DELETE_DELAY = 10  QUESTION_MARK_ICON = "https://cdn.discordapp.com/emojis/512367613339369475.png" @@ -35,7 +35,7 @@ class CommandErrorHandler(commands.Cog):              logging.debug("Cooldown counter reverted as the command was not used correctly.")      @staticmethod -    def error_embed(message: str, title: Union[Iterable, str] = ERROR_REPLIES) -> Embed: +    def error_embed(message: str, title: Iterable | str = ERROR_REPLIES) -> Embed:          """Build a basic embed with red colour and either a random error title or a title provided."""          embed = Embed(colour=Colours.soft_red)          if isinstance(title, str): @@ -59,7 +59,7 @@ class CommandErrorHandler(commands.Cog):          error = getattr(error, "original", error)          logging.debug( -            f"Error Encountered: {type(error).__name__} - {str(error)}, " +            f"Error Encountered: {type(error).__name__} - {error!s}, "              f"Command: {ctx.command}, "              f"Author: {ctx.author}, "              f"Channel: {ctx.channel}" @@ -71,7 +71,7 @@ class CommandErrorHandler(commands.Cog):                  await self.send_command_suggestion(ctx, ctx.invoked_with)              return -        if isinstance(error, (InChannelCheckFailure, InMonthCheckFailure)): +        if isinstance(error, InChannelCheckFailure | InMonthCheckFailure):              await ctx.send(embed=self.error_embed(str(error), NEGATIVE_REPLIES), delete_after=7.5)              return @@ -156,7 +156,7 @@ class CommandErrorHandler(commands.Cog):              if ctx.guild is not None:                  scope.set_extra("jump_to", ctx.message.jump_url) -            log.exception(f"Unhandled command error: {str(error)}", exc_info=error) +            log.exception(f"Unhandled command error: {error!s}", exc_info=error)      async def send_command_suggestion(self, ctx: commands.Context, command_name: str) -> None:          """Sends user similar commands if any can be found.""" @@ -185,7 +185,7 @@ class CommandErrorHandler(commands.Cog):              e.description = "\n".join(                  misspelled_content.replace(command_name, cmd, 1) for cmd in command_suggestions              ) -            await ctx.send(embed=e, delete_after=RedirectOutput.delete_delay) +            await ctx.send(embed=e, delete_after=DELETE_DELAY)  async def setup(bot: Bot) -> None: diff --git a/bot/exts/core/extensions.py b/bot/exts/core/extensions.py index 1d22cf37..87ef2ed1 100644 --- a/bot/exts/core/extensions.py +++ b/bot/exts/core/extensions.py @@ -2,7 +2,6 @@ import functools  import logging  from collections.abc import Mapping  from enum import Enum -from typing import Optional  from discord import Colour, Embed  from discord.ext import commands @@ -48,7 +47,7 @@ class Extension(commands.Converter):          if argument in ctx.bot.all_extensions:              return argument -        elif (qualified_arg := f"{exts.__name__}.{argument}") in ctx.bot.all_extensions: +        if (qualified_arg := f"{exts.__name__}.{argument}") in ctx.bot.all_extensions:              return qualified_arg          matches = [] @@ -63,10 +62,9 @@ class Extension(commands.Converter):                  f":x: `{argument}` is an ambiguous extension name. "                  f"Please use one of the following fully-qualified names.```\n{names}\n```"              ) -        elif matches: +        if matches:              return matches[0] -        else: -            raise commands.BadArgument(f":x: Could not find the extension `{argument}`.") +        raise commands.BadArgument(f":x: Could not find the extension `{argument}`.")  class Extensions(commands.Cog): @@ -86,7 +84,7 @@ class Extensions(commands.Cog):          Load extensions given their fully qualified or unqualified names.          If '\*' or '\*\*' is given as the name, all unloaded extensions will be loaded. -        """  # noqa: W605 +        """          if not extensions:              await self.bot.invoke_help_command(ctx)              return @@ -103,7 +101,7 @@ class Extensions(commands.Cog):          Unload currently loaded extensions given their fully qualified or unqualified names.          If '\*' or '\*\*' is given as the name, all loaded extensions will be unloaded. -        """  # noqa: W605 +        """          if not extensions:              await self.bot.invoke_help_command(ctx)              return @@ -129,7 +127,7 @@ class Extensions(commands.Cog):          If '\*' is given as the name, all currently loaded extensions will be reloaded.          If '\*\*' is given as the name, all extensions, including unloaded ones, will be reloaded. -        """  # noqa: W605 +        """          if not extensions:              await self.bot.invoke_help_command(ctx)              return @@ -220,7 +218,7 @@ class Extensions(commands.Cog):          return msg -    async def manage(self, action: Action, ext: str) -> tuple[str, Optional[str]]: +    async def manage(self, action: Action, ext: str) -> tuple[str, str | None]:          """Apply an action to an extension and return the status message and any error message."""          verb = action.name.lower()          error_msg = None diff --git a/bot/exts/core/help.py b/bot/exts/core/help.py index 30deaff4..9f96a333 100644 --- a/bot/exts/core/help.py +++ b/bot/exts/core/help.py @@ -3,7 +3,7 @@ import asyncio  import itertools  import logging  from contextlib import suppress -from typing import NamedTuple, Optional, Union +from typing import NamedTuple  from discord import Colour, Embed, HTTPException, Message, Reaction, User  from discord.ext import commands @@ -38,7 +38,7 @@ class Cog(NamedTuple):  log = logging.getLogger(__name__) -class HelpQueryNotFound(ValueError): +class HelpQueryNotFoundError(ValueError):      """      Raised when a HelpSession Query doesn't match a command or cog. @@ -49,7 +49,7 @@ class HelpQueryNotFound(ValueError):      """      def __init__( -            self, arg: str, possible_matches: Optional[list[str]] = None, *, parent_command: Optional[Command] = None +            self, arg: str, possible_matches: list[str] | None = None, *, parent_command: Command | None = None      ) -> None:          super().__init__(arg)          self.possible_matches = possible_matches @@ -117,7 +117,7 @@ class HelpSession:          self._timeout_task = None          self.reset_timeout() -    def _get_query(self, query: str) -> Union[Command, Cog]: +    def _get_query(self, query: str) -> Command | Cog | None:          """Attempts to match the provided query with a valid command or cog."""          command = self._bot.get_command(query)          if command: @@ -151,6 +151,7 @@ class HelpSession:              )          self._handle_not_found(query) +        return None      def _handle_not_found(self, query: str) -> None:          """ @@ -164,11 +165,11 @@ class HelpSession:              parent_command = self._bot.get_command(parent)              if parent_command: -                raise HelpQueryNotFound('Invalid Subcommand.', parent_command=parent_command) +                raise HelpQueryNotFoundError("Invalid Subcommand.", parent_command=parent_command)          similar_commands = get_command_suggestions(list(self._bot.all_commands.keys()), query) -        raise HelpQueryNotFound(f'Query "{query}" not found.', similar_commands) +        raise HelpQueryNotFoundError(f'Query "{query}" not found.', similar_commands)      async def timeout(self, seconds: int = 30) -> None:          """Waits for a set number of seconds, then stops the help session.""" @@ -178,9 +179,8 @@ class HelpSession:      def reset_timeout(self) -> None:          """Cancels the original timeout task and sets it again from the start."""          # cancel original if it exists -        if self._timeout_task: -            if not self._timeout_task.cancelled(): -                self._timeout_task.cancel() +        if self._timeout_task and not self._timeout_task.cancelled(): +            self._timeout_task.cancel()          # recreate the timeout task          self._timeout_task = self._bot.loop.create_task(self.timeout()) @@ -252,8 +252,7 @@ class HelpSession:                  pass              return f"**{cmd.cog_name}**" -        else: -            return "**\u200bNo Category:**" +        return "**\u200bNo Category:**"      def _get_command_params(self, cmd: Command) -> str:          """ @@ -286,8 +285,7 @@ class HelpSession:              # if required              else:                  results.append(f"<{name}>") - -        return f"{cmd.qualified_name} {' '.join(results)}" +        return " ".join([cmd.qualified_name, *results])      async def build_pages(self) -> None:          """Builds the list of content pages to be paginated through in the help message, as a list of str.""" @@ -305,7 +303,7 @@ class HelpSession:              paginator.add_line(f"*{self.description}*")          # list all children commands of the queried object -        if isinstance(self.query, (commands.GroupMixin, Cog)): +        if isinstance(self.query, commands.GroupMixin | Cog):              await self._list_child_commands(paginator)          self._pages = paginator.pages @@ -418,7 +416,7 @@ class HelpSession:          """Returns an Embed with the requested page formatted within."""          embed = Embed() -        if isinstance(self.query, (commands.Command, Cog)) and page_number > 0: +        if isinstance(self.query, commands.Command | Cog) and page_number > 0:              title = f'Command Help | "{self.query.name}"'          else:              title = self.title @@ -518,7 +516,7 @@ class Help(DiscordCog):          """Shows Command Help."""          try:              await HelpSession.start(ctx, *commands) -        except HelpQueryNotFound as error: +        except HelpQueryNotFoundError as error:              # Send help message of parent command if subcommand is invalid.              if cmd := error.parent_command: diff --git a/bot/exts/core/internal_eval/_helpers.py b/bot/exts/core/internal_eval/_helpers.py index 5b2f8f5d..34ef7fef 100644 --- a/bot/exts/core/internal_eval/_helpers.py +++ b/bot/exts/core/internal_eval/_helpers.py @@ -8,7 +8,7 @@ import logging  import sys  import traceback  import types -from typing import Any, Optional, Union +from typing import Any  log = logging.getLogger(__name__) @@ -120,7 +120,7 @@ class EvalContext:          log.trace(f"Updating {self._locals} with {locals_}")          self._locals.update(locals_) -    def prepare_eval(self, code: str) -> Optional[str]: +    def prepare_eval(self, code: str) -> str | None:          """Prepare an evaluation by processing the code and setting up the context."""          self.code = code @@ -149,7 +149,7 @@ class EvalContext:          compiled_code = compile(self.eval_tree, filename=INTERNAL_EVAL_FRAMENAME, mode="exec")          log.trace("Executing the compiled code with the desired namespace environment") -        exec(compiled_code, self.locals)  # noqa: B102,S102 +        exec(compiled_code, self.locals)  # noqa: S102          log.trace("Awaiting the created evaluation wrapper coroutine.")          await self.function() @@ -212,7 +212,7 @@ class CaptureLastExpression(ast.NodeTransformer):          self.tree = tree          self.last_node = list(ast.iter_child_nodes(tree))[-1] -    def visit_Expr(self, node: ast.Expr) -> Union[ast.Expr, ast.Assign]:  # noqa: N802 +    def visit_Expr(self, node: ast.Expr) -> ast.Expr | ast.Assign:  # noqa: N802          """          Replace the Expr node that is last child node of Module with an assignment. @@ -230,7 +230,7 @@ class CaptureLastExpression(ast.NodeTransformer):          right_hand_side = list(ast.iter_child_nodes(node))[0]          assignment = ast.Assign( -            targets=[ast.Name(id='_value_last_expression', ctx=ast.Store())], +            targets=[ast.Name(id="_value_last_expression", ctx=ast.Store())],              value=right_hand_side,              lineno=node.lineno,              col_offset=0, diff --git a/bot/exts/core/internal_eval/_internal_eval.py b/bot/exts/core/internal_eval/_internal_eval.py index 2daf8ef9..f5188c1f 100644 --- a/bot/exts/core/internal_eval/_internal_eval.py +++ b/bot/exts/core/internal_eval/_internal_eval.py @@ -1,7 +1,6 @@  import logging  import re  import textwrap -from typing import Optional  import discord  from discord.ext import commands @@ -84,7 +83,7 @@ class InternalEval(commands.Cog):          return shortened_output -    async def _upload_output(self, output: str) -> Optional[str]: +    async def _upload_output(self, output: str) -> str | None:          """Upload `internal eval` output to our pastebin and return the url."""          data = self.shorten_output(output, max_length=MAX_LENGTH)          try: diff --git a/bot/exts/core/source.py b/bot/exts/core/source.py index f771eaca..7c67bcb5 100644 --- a/bot/exts/core/source.py +++ b/bot/exts/core/source.py @@ -1,15 +1,17 @@  import inspect  from pathlib import Path -from typing import Optional  from discord import Embed  from discord.ext import commands  from bot.bot import Bot -from bot.constants import Channels, Source, WHITELISTED_CHANNELS +from bot.constants import Channels, WHITELISTED_CHANNELS  from bot.utils.converters import SourceConverter, SourceType  from bot.utils.decorators import whitelist_override +GITHUB_BOT_URL = "https://github.com/python-discord/sir-lancebot" +BOT_AVATAR_URL = "https://avatars1.githubusercontent.com/u/9919" +  class BotSource(commands.Cog):      """Displays information about the bot's source code.""" @@ -20,15 +22,15 @@ class BotSource(commands.Cog):          """Display information and a GitHub link to the source code of a command, tag, or cog."""          if not source_item:              embed = Embed(title="Sir Lancebot's GitHub Repository") -            embed.add_field(name="Repository", value=f"[Go to GitHub]({Source.github})") -            embed.set_thumbnail(url=Source.github_avatar_url) +            embed.add_field(name="Repository", value=f"[Go to GitHub]({GITHUB_BOT_URL})") +            embed.set_thumbnail(url=BOT_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]]: +    def get_source_link(self, source_item: SourceType) -> tuple[str, str, int | None]:          """          Build GitHub link of source item, return this link, file location and first line number. @@ -58,11 +60,11 @@ class BotSource(commands.Cog):          file_location = Path(filename).relative_to(Path.cwd()).as_posix() -        url = f"{Source.github}/blob/main/{file_location}{lines_extension}" +        url = f"{GITHUB_BOT_URL}/blob/main/{file_location}{lines_extension}"          return url, file_location, first_line_no or None -    async def build_embed(self, source_object: SourceType) -> Optional[Embed]: +    async def build_embed(self, source_object: SourceType) -> Embed | None:          """Build embed based on source object."""          url, location, first_line = self.get_source_link(source_object) @@ -74,7 +76,7 @@ class BotSource(commands.Cog):              description = source_object.description.splitlines()[0]          embed = Embed(title=title, description=description) -        embed.set_thumbnail(url=Source.github_avatar_url) +        embed.set_thumbnail(url=BOT_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}") diff --git a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py b/bot/exts/events/hacktoberfest/hacktober_issue_finder.py index aeffc8d7..69aa3924 100644 --- a/bot/exts/events/hacktoberfest/hacktober-issue-finder.py +++ b/bot/exts/events/hacktoberfest/hacktober_issue_finder.py @@ -1,7 +1,6 @@ -import datetime  import logging  import random -from typing import Optional +from datetime import UTC, datetime  import discord  from discord.ext import commands @@ -18,7 +17,7 @@ REQUEST_HEADERS = {      "User-Agent": "Python Discord Hacktoberbot",      "Accept": "application / vnd.github.v3 + json"  } -if GITHUB_TOKEN := Tokens.github: +if GITHUB_TOKEN := Tokens.github.get_secret_value():      REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" @@ -28,9 +27,9 @@ class HacktoberIssues(commands.Cog):      def __init__(self, bot: Bot):          self.bot = bot          self.cache_normal = None -        self.cache_timer_normal = datetime.datetime(1, 1, 1) +        self.cache_timer_normal = datetime(1, 1, 1, tzinfo=UTC)          self.cache_beginner = None -        self.cache_timer_beginner = datetime.datetime(1, 1, 1) +        self.cache_timer_beginner = datetime(1, 1, 1, tzinfo=UTC)      @in_month(Month.OCTOBER)      @commands.command() @@ -49,7 +48,7 @@ class HacktoberIssues(commands.Cog):              embed = self.format_embed(issue)          await ctx.send(embed=embed) -    async def get_issues(self, ctx: commands.Context, option: str) -> Optional[dict]: +    async def get_issues(self, ctx: commands.Context, option: str) -> dict | None:          """Get a list of the python issues with the label 'hacktoberfest' from the Github api."""          if option == "beginner":              if (ctx.message.created_at.replace(tzinfo=None) - self.cache_timer_beginner).seconds <= 60: diff --git a/bot/exts/events/hacktoberfest/hacktoberstats.py b/bot/exts/events/hacktoberfest/hacktoberstats.py index c29e24b0..c7fd3601 100644 --- a/bot/exts/events/hacktoberfest/hacktoberstats.py +++ b/bot/exts/events/hacktoberfest/hacktoberstats.py @@ -2,8 +2,7 @@ import logging  import random  import re  from collections import Counter -from datetime import datetime, timedelta -from typing import Optional, Union +from datetime import UTC, datetime, timedelta  from urllib.parse import quote_plus  import discord @@ -16,7 +15,7 @@ from bot.utils.decorators import in_month  log = logging.getLogger(__name__) -CURRENT_YEAR = datetime.now().year  # Used to construct GH API query +CURRENT_YEAR = datetime.now(tz=UTC).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 @@ -24,8 +23,8 @@ REQUEST_HEADERS = {"User-Agent": "Python Discord Hacktoberbot"}  # using repo topics API during preview period requires an accept header  GITHUB_TOPICS_ACCEPT_HEADER = {"Accept": "application/vnd.github.mercy-preview+json"}  if GITHUB_TOKEN := Tokens.github: -    REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN}" -    GITHUB_TOPICS_ACCEPT_HEADER["Authorization"] = f"token {GITHUB_TOKEN}" +    REQUEST_HEADERS["Authorization"] = f"token {GITHUB_TOKEN.get_secret_value()}" +    GITHUB_TOPICS_ACCEPT_HEADER["Authorization"] = f"token {GITHUB_TOKEN.get_secret_value()}"  GITHUB_NONEXISTENT_USER_MESSAGE = (      "The listed users cannot be searched either because the users do not exist " @@ -185,7 +184,7 @@ class HacktoberStats(commands.Cog):          logging.info(f"Hacktoberfest PR built for GitHub user '{github_username}'")          return stats_embed -    async def get_october_prs(self, github_username: str) -> Optional[list[dict]]: +    async def get_october_prs(self, github_username: str) -> list[dict] | None:          """          Query GitHub's API for PRs created during the month of October by github_username. @@ -234,9 +233,8 @@ class HacktoberStats(commands.Cog):              # Ignore logging non-existent users or users we do not have permission to see              if api_message == GITHUB_NONEXISTENT_USER_MESSAGE:                  log.debug(f"No GitHub user found named '{github_username}'") -                return -            else: -                log.error(f"GitHub API request for '{github_username}' failed with message: {api_message}") +                return None +            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: @@ -246,7 +244,7 @@ class HacktoberStats(commands.Cog):          logging.info(f"Found {len(jsonresp['items'])} Hacktoberfest PRs for GitHub user: '{github_username}'")          outlist = []  # list of pr information dicts that will get returned -        oct3 = datetime(int(CURRENT_YEAR), 10, 3, 23, 59, 59, tzinfo=None) +        oct3 = datetime(int(CURRENT_YEAR), 10, 3, 23, 59, 59, tzinfo=UTC)          hackto_topics = {}  # cache whether each repo has the appropriate topic (bool values)          for item in jsonresp["items"]:              shortname = self._get_shortname(item["repository_url"]) @@ -255,15 +253,14 @@ class HacktoberStats(commands.Cog):                  "repo_shortname": shortname,                  "created_at": datetime.strptime(                      item["created_at"], "%Y-%m-%dT%H:%M:%SZ" -                ), +                ).replace(tzinfo=UTC),                  "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 self._has_label(item, ["invalid", "spam"]): -                if not await self._is_accepted(itemdict): -                    continue +            if self._has_label(item, ["invalid", "spam"]) and not await self._is_accepted(itemdict): +                continue              # PRs before oct 3 no need to check for topics              # continue the loop if 'hacktoberfest-accepted' is labelled then @@ -302,7 +299,7 @@ class HacktoberStats(commands.Cog):              return await resp.json()      @staticmethod -    def _has_label(pr: dict, labels: Union[list[str], str]) -> bool: +    def _has_label(pr: dict, labels: list[str] | str) -> bool:          """          Check if a PR has label 'labels'. @@ -313,7 +310,7 @@ class HacktoberStats(commands.Cog):              return False          if isinstance(labels, str) and any(label["name"].casefold() == labels for label in pr["labels"]):              return True -        for item in labels: +        for item in labels:  # noqa: SIM110              if any(label["name"].casefold() == item for label in pr["labels"]):                  return True          return False @@ -350,10 +347,7 @@ class HacktoberStats(commands.Cog):              return False          # loop through reviews and check for approval -        for item in jsonresp2: -            if item.get("status") == "APPROVED": -                return True -        return False +        return any(item.get("status") == "APPROVED" for item in jsonresp2)      @staticmethod      def _get_shortname(in_url: str) -> str: @@ -378,8 +372,8 @@ class HacktoberStats(commands.Cog):          PRs that are accepted must either be merged, approved, or labelled          'hacktoberfest-accepted.          """ -        now = datetime.now() -        oct3 = datetime(CURRENT_YEAR, 10, 3, 23, 59, 59, tzinfo=None) +        now = datetime.now(tz=UTC) +        oct3 = datetime(CURRENT_YEAR, 10, 3, 23, 59, 59, tzinfo=UTC)          in_review = []          accepted = []          for pr in prs: @@ -420,8 +414,7 @@ class HacktoberStats(commands.Cog):          """Return "contribution" or "contributions" based on the value of n."""          if n == 1:              return "contribution" -        else: -            return "contributions" +        return "contributions"      @staticmethod      def _author_mention_from_context(ctx: commands.Context) -> tuple[str, str]: @@ -434,4 +427,6 @@ class HacktoberStats(commands.Cog):  async def setup(bot: Bot) -> None:      """Load the Hacktober Stats Cog.""" +    if not Tokens.github: +        log.warning("No GitHub token was provided. The HacktoberStats Cog won't be fully functional.")      await bot.add_cog(HacktoberStats(bot)) diff --git a/bot/exts/events/hacktoberfest/timeleft.py b/bot/exts/events/hacktoberfest/timeleft.py index f470e932..8f46d798 100644 --- a/bot/exts/events/hacktoberfest/timeleft.py +++ b/bot/exts/events/hacktoberfest/timeleft.py @@ -1,5 +1,5 @@  import logging -from datetime import datetime +from datetime import UTC, datetime  from discord.ext import commands @@ -9,25 +9,25 @@ log = logging.getLogger(__name__)  class TimeLeft(commands.Cog): -    """A Cog that tells you how long left until Hacktober is over!""" +    """A Cog that tells users how long left until Hacktober is over!"""      def in_hacktober(self) -> bool:          """Return True if the current time is within Hacktoberfest."""          _, end, start = self.load_date() -        now = datetime.utcnow() +        now = datetime.now(tz=UTC)          return start <= now <= end      @staticmethod      def load_date() -> tuple[datetime, datetime, datetime]: -        """Return of a tuple of the current time and the end and start times of the next October.""" -        now = datetime.utcnow() +        """Return of a tuple of the current time and the end and start times of the next Hacktober.""" +        now = datetime.now(tz=UTC)          year = now.year          if now.month > 10:              year += 1 -        end = datetime(year, 11, 1, 12)  # November 1st 12:00 (UTC-12:00) -        start = datetime(year, 9, 30, 10)  # September 30th 10:00 (UTC+14:00) +        end = datetime(year, 11, 1, 12, tzinfo=UTC)  # November 1st 12:00 (UTC-12:00) +        start = datetime(year, 9, 30, 10, tzinfo=UTC)  # September 30th 10:00 (UTC+14:00)          return now, end, start      @commands.command() diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 8b012a17..15126f60 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -1,7 +1,8 @@  import time +from collections.abc import Iterable  from random import randrange  from string import ascii_uppercase -from typing import Iterable, NamedTuple, Optional, TypedDict +from typing import NamedTuple, TypedDict  DEFAULT_QUESTION_POINTS = 10  DEFAULT_QUESTION_TIME = 20 @@ -14,8 +15,8 @@ class QuestionData(TypedDict):      description: str      answers: list[str]      correct: str -    points: Optional[int] -    time: Optional[int] +    points: int | None +    time: int | None  class UserGuess(NamedTuple): @@ -26,15 +27,15 @@ class UserGuess(NamedTuple):      elapsed: float -class QuestionClosed(RuntimeError): +class QuestionClosedError(RuntimeError):      """Exception raised when the question is not open for guesses anymore.""" -class AlreadyUpdated(RuntimeError): +class AlreadyUpdatedError(RuntimeError):      """Exception raised when the user has already updated their guess once.""" -class AllQuestionsVisited(RuntimeError): +class AllQuestionsVisitedError(RuntimeError):      """Exception raised when all of the questions have been visited.""" @@ -90,10 +91,10 @@ class Question:      def _update_guess(self, user: int, answer: str) -> UserGuess:          """Update an already existing guess."""          if self._started is None: -            raise QuestionClosed("Question is not open for answers.") +            raise QuestionClosedError("Question is not open for answers.")          if self._guesses[user][1] is False: -            raise AlreadyUpdated(f"User({user}) has already updated their guess once.") +            raise AlreadyUpdatedError(f"User({user}) has already updated their guess once.")          self._guesses[user] = (answer, False, time.perf_counter() - self._started)          return self._guesses[user] @@ -104,7 +105,7 @@ class Question:              return self._update_guess(user, answer)          if self._started is None: -            raise QuestionClosed("Question is not open for answers.") +            raise QuestionClosedError("Question is not open for answers.")          self._guesses[user] = (answer, True, time.perf_counter() - self._started)          return self._guesses[user] @@ -126,7 +127,7 @@ class TriviaNightGame:          self._questions = [Question(q) for q in data]          # A copy of the questions to keep for `.trivianight list`          self._all_questions = list(self._questions) -        self.current_question: Optional[Question] = None +        self.current_question: Question | None = None          self._points = {}          self._speed = {} @@ -148,7 +149,7 @@ class TriviaNightGame:              except IndexError:                  raise ValueError(f"Question number {number} does not exist.")          elif len(self._questions) == 0: -            raise AllQuestionsVisited("All of the questions have been visited.") +            raise AllQuestionsVisitedError("All of the questions have been visited.")          else:              question = self._questions.pop(randrange(len(self._questions))) diff --git a/bot/exts/events/trivianight/_questions.py b/bot/exts/events/trivianight/_questions.py index 5f1046dc..a0dd545e 100644 --- a/bot/exts/events/trivianight/_questions.py +++ b/bot/exts/events/trivianight/_questions.py @@ -7,7 +7,7 @@ from discord.ui import Button, View  from bot.constants import Colours, NEGATIVE_REPLIES -from ._game import AlreadyUpdated, Question, QuestionClosed +from ._game import AlreadyUpdatedError, Question, QuestionClosedError  from ._scoreboard import Scoreboard @@ -29,7 +29,7 @@ class AnswerButton(Button):          """          try:              guess = self.question.guess(interaction.user.id, self.label) -        except AlreadyUpdated: +        except AlreadyUpdatedError:              await interaction.response.send_message(                  embed=Embed(                      title=choice(NEGATIVE_REPLIES), @@ -39,7 +39,7 @@ class AnswerButton(Button):                  ephemeral=True              )              return -        except QuestionClosed: +        except QuestionClosedError:              await interaction.response.send_message(                  embed=Embed(                      title=choice(NEGATIVE_REPLIES), @@ -91,7 +91,7 @@ class QuestionView(View):              - text: A string that represents the question description to 'unicodeify'          """          return "".join( -            f"{letter}\u200b" if letter not in ('\n', '\t', '`', 'p', 'y') else letter +            f"{letter}\u200b" if letter not in ("\n", "\t", "`", "p", "y") else letter              for idx, letter in enumerate(text)          ) @@ -127,13 +127,13 @@ class QuestionView(View):          if len(guesses) != 0:              answers_chosen = {                  answer_choice: len( -                    tuple(filter(lambda x: x[0] == answer_choice, guesses.values()))  # noqa: B023 +                    tuple(filter(lambda x: x[0] == answer_choice, guesses.values()))                  )                  for answer_choice in labels              }              answers_chosen = dict( -                sorted(list(answers_chosen.items()), key=lambda item: item[1], reverse=True) +                sorted(answers_chosen.items(), key=lambda item: item[1], reverse=True)              )              for answer, people_answered in answers_chosen.items(): diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index 10435551..e0db45d8 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -1,7 +1,6 @@  import asyncio  from json import JSONDecodeError, loads  from random import choice -from typing import Optional  from discord import Embed  from discord.ext import commands @@ -10,7 +9,7 @@ from bot.bot import Bot  from bot.constants import Colours, NEGATIVE_REPLIES, POSITIVE_REPLIES, Roles  from bot.utils.pagination import LinePaginator -from ._game import AllQuestionsVisited, TriviaNightGame +from ._game import AllQuestionsVisitedError, TriviaNightGame  from ._questions import QuestionView  from ._scoreboard import Scoreboard @@ -23,8 +22,8 @@ class TriviaNightCog(commands.Cog):      def __init__(self, bot: Bot):          self.bot = bot -        self.game: Optional[TriviaNightGame] = None -        self.scoreboard: Optional[Scoreboard] = None +        self.game: TriviaNightGame | None = None +        self.scoreboard: Scoreboard | None = None          self.question_closed: asyncio.Event = None      @commands.group(aliases=["tn"], invoke_without_command=True) @@ -46,7 +45,7 @@ class TriviaNightCog(commands.Cog):      @trivianight.command()      @commands.has_any_role(*TRIVIA_NIGHT_ROLES) -    async def load(self, ctx: commands.Context, *, to_load: Optional[str]) -> None: +    async def load(self, ctx: commands.Context, *, to_load: str | None) -> None:          """          Loads a JSON file from the provided attachment or argument. @@ -76,8 +75,7 @@ class TriviaNightCog(commands.Cog):          elif not to_load:              raise commands.BadArgument("You didn't attach an attachment nor link a message!")          elif ( -            to_load.startswith("https://discord.com/channels") -            or to_load.startswith("https://discordapp.com/channels") +            to_load.startswith(("https://discord.com/channels", "https://discordapp.com/channels"))          ):              channel_id, message_id = to_load.split("/")[-2:]              channel = await ctx.guild.fetch_channel(int(channel_id)) @@ -92,7 +90,7 @@ class TriviaNightCog(commands.Cog):          try:              serialized_json = loads(json_text)          except JSONDecodeError as error: -            raise commands.BadArgument(f"Looks like something went wrong:\n{str(error)}") +            raise commands.BadArgument(f"Looks like something went wrong:\n{error!s}")          self.game = TriviaNightGame(serialized_json)          self.question_closed = asyncio.Event() @@ -107,7 +105,7 @@ class TriviaNightCog(commands.Cog):          await ctx.send(embed=success_embed) -    @trivianight.command(aliases=('next',)) +    @trivianight.command(aliases=("next",))      @commands.has_any_role(*TRIVIA_NIGHT_ROLES)      async def question(self, ctx: commands.Context, question_number: str = None) -> None:          """ @@ -135,7 +133,7 @@ class TriviaNightCog(commands.Cog):          try:              next_question = self.game.next_question(question_number) -        except AllQuestionsVisited: +        except AllQuestionsVisitedError:              error_embed = Embed(                  title=choice(NEGATIVE_REPLIES),                  description="All of the questions have been used.", diff --git a/bot/exts/fun/anagram.py b/bot/exts/fun/anagram.py index d8ea6a55..8210d1d5 100644 --- a/bot/exts/fun/anagram.py +++ b/bot/exts/fun/anagram.py @@ -15,7 +15,7 @@ log = logging.getLogger(__name__)  TIME_LIMIT = 60  # anagram.json file contains all the anagrams -with open(Path("bot/resources/fun/anagram.json"), "r") as f: +with open(Path("bot/resources/fun/anagram.json")) as f:      ANAGRAMS_ALL = json.load(f) diff --git a/bot/exts/fun/battleship.py b/bot/exts/fun/battleship.py index a8039cf2..4a552605 100644 --- a/bot/exts/fun/battleship.py +++ b/bot/exts/fun/battleship.py @@ -4,7 +4,6 @@ import random  import re  from dataclasses import dataclass  from functools import partial -from typing import Optional  import discord  from discord.ext import commands @@ -19,7 +18,7 @@ log = logging.getLogger(__name__)  class Square:      """Each square on the battleship grid - if they contain a boat and if they've been aimed at.""" -    boat: Optional[str] +    boat: str | None      aimed: bool @@ -31,8 +30,8 @@ EmojiSet = dict[tuple[bool, bool], str]  class Player:      """Each player in the game - their messages for the boards and their current grid.""" -    user: Optional[discord.Member] -    board: Optional[discord.Message] +    user: discord.Member | None +    board: discord.Message | None      opponent_board: discord.Message      grid: Grid @@ -110,10 +109,10 @@ class Game:          self.gameover: bool = False -        self.turn: Optional[Player] = None -        self.next: Optional[Player] = None +        self.turn: Player | None = None +        self.next: Player | None = None -        self.match: Optional[re.Match] = None +        self.match: re.Match | None = None          self.surrender: bool = False          self.setup_grids() @@ -135,7 +134,7 @@ class Game:              for row in player.grid          ] -        rows = ["".join([number] + row) for number, row in zip(NUMBERS, grid)] +        rows = ["".join([number] + row) for number, row in zip(NUMBERS, grid, strict=True)]          return "\n".join([LETTERS] + rows)      @staticmethod @@ -215,7 +214,7 @@ class Game:              (self.p1, "board"), (self.p2, "board")          ) -        for board, location in zip(boards, locations): +        for board, location in zip(boards, locations, strict=True):              player, attr = location              if getattr(player, attr):                  await getattr(player, attr).edit(content=board) @@ -232,8 +231,9 @@ class Game:              if not self.match:                  self.bot.loop.create_task(message.add_reaction(CROSS_EMOJI))              return bool(self.match) +        return None -    async def take_turn(self) -> Optional[Square]: +    async def take_turn(self) -> Square | None:          """Lets the player who's turn it is choose a square."""          square = None          turn_message = await self.turn.user.send( diff --git a/bot/exts/fun/catify.py b/bot/exts/fun/catify.py index 6e8c75ba..c1677cd8 100644 --- a/bot/exts/fun/catify.py +++ b/bot/exts/fun/catify.py @@ -1,21 +1,22 @@  import random  from contextlib import suppress -from typing import Optional  from discord import AllowedMentions, Embed, Forbidden  from discord.ext import commands  from bot.bot import Bot -from bot.constants import Cats, Colours, NEGATIVE_REPLIES +from bot.constants import Colours, NEGATIVE_REPLIES  from bot.utils import helpers +CATS = ["įįį¢", "į”įį¢", "š", "įįį¢", "įįį¢", "įįį¢", "į£įį¢", "į¦įį¢", "įįį¢"] +  class Catify(commands.Cog):      """Cog for the catify command."""      @commands.command(aliases=("įįį¢ify", "įįį¢"))      @commands.cooldown(1, 5, commands.BucketType.user) -    async def catify(self, ctx: commands.Context, *, text: Optional[str]) -> None: +    async def catify(self, ctx: commands.Context, *, text: str | None) -> None:          """          Convert the provided text into a cat themed sentence by interspercing cats throughout text. @@ -36,13 +37,12 @@ class Catify(commands.Cog):                  await ctx.send(embed=embed)                  return -            else: -                display_name += f" | {random.choice(Cats.cats)}" +            display_name += f" | {random.choice(CATS)}" -                await ctx.send(f"Your catified nickname is: `{display_name}`", allowed_mentions=AllowedMentions.none()) +            await ctx.send(f"Your catified nickname is: `{display_name}`", allowed_mentions=AllowedMentions.none()) -                with suppress(Forbidden): -                    await ctx.author.edit(nick=display_name) +            with suppress(Forbidden): +                await ctx.author.edit(nick=display_name)          else:              if len(text) >= 1500:                  embed = Embed( @@ -58,21 +58,21 @@ class Catify(commands.Cog):                  name = name.lower()                  if "cat" in name:                      if random.randint(0, 5) == 5: -                        string_list[index] = name.replace("cat", f"**{random.choice(Cats.cats)}**") +                        string_list[index] = name.replace("cat", f"**{random.choice(CATS)}**")                      else: -                        string_list[index] = name.replace("cat", random.choice(Cats.cats)) -                for element in Cats.cats: -                    if element in name: -                        string_list[index] = name.replace(element, "cat") +                        string_list[index] = name.replace("cat", random.choice(CATS)) +                for cat in CATS: +                    if cat in name: +                        string_list[index] = name.replace(cat, "cat")              string_len = len(string_list) // 3 or len(string_list)              for _ in range(random.randint(1, string_len)):                  # insert cat at random index                  if random.randint(0, 5) == 5: -                    string_list.insert(random.randint(0, len(string_list)), f"**{random.choice(Cats.cats)}**") +                    string_list.insert(random.randint(0, len(string_list)), f"**{random.choice(CATS)}**")                  else: -                    string_list.insert(random.randint(0, len(string_list)), random.choice(Cats.cats)) +                    string_list.insert(random.randint(0, len(string_list)), random.choice(CATS))              text = helpers.suppress_links(" ".join(string_list))              await ctx.send( diff --git a/bot/exts/fun/connect_four.py b/bot/exts/fun/connect_four.py index 0d870a6e..6544dc48 100644 --- a/bot/exts/fun/connect_four.py +++ b/bot/exts/fun/connect_four.py @@ -1,7 +1,6 @@  import asyncio  import random  from functools import partial -from typing import Optional, Union  import discord  import emojis @@ -14,8 +13,8 @@ from bot.constants import Emojis  NUMBERS = list(Emojis.number_emojis.values())  CROSS_EMOJI = Emojis.incident_unactioned -Coordinate = Optional[tuple[int, int]] -EMOJI_CHECK = Union[discord.Emoji, str] +Coordinate = tuple[int, int] | None +EMOJI_CHECK = discord.Emoji | str  class Game: @@ -26,7 +25,7 @@ class Game:          bot: Bot,          channel: discord.TextChannel,          player1: discord.Member, -        player2: Optional[discord.Member], +        player2: discord.Member | None,          tokens: list[str],          size: int = 7      ): @@ -73,7 +72,7 @@ class Game:              await self.message.edit(content=None, embed=embed)      async def game_over( -        self, action: str, player1: Union[ClientUser, Member], player2: Union[ClientUser, Member] +        self, action: str, player1: ClientUser | Member, player2: ClientUser | Member      ) -> None:          """Announces to public chat."""          if action == "win": @@ -134,12 +133,12 @@ class Game:                  reaction, user = await self.bot.wait_for("reaction_add", check=self.predicate, timeout=30.0)              except asyncio.TimeoutError:                  await self.channel.send(f"{self.player_active.mention}, you took too long. Game over!") -                return +                return None              else:                  await message.delete()                  if str(reaction.emoji) == CROSS_EMOJI:                      await self.game_over("quit", self.player_active, self.player_inactive) -                    return +                    return None                  await self.message.remove_reaction(reaction, user) @@ -197,7 +196,7 @@ class AI:                      break          return possible_coords -    def check_ai_win(self, coord_list: list[Coordinate]) -> Optional[Coordinate]: +    def check_ai_win(self, coord_list: list[Coordinate]) -> Coordinate:          """          Check AI win. @@ -205,12 +204,13 @@ class AI:          with 10% chance of not winning and returning None          """          if random.randint(1, 10) == 1: -            return +            return None          for coords in coord_list:              if self.game.check_win(coords, 2):                  return coords +        return None -    def check_player_win(self, coord_list: list[Coordinate]) -> Optional[Coordinate]: +    def check_player_win(self, coord_list: list[Coordinate]) -> Coordinate | None:          """          Check Player win. @@ -218,17 +218,18 @@ class AI:          from winning with 25% of not blocking them  and returning None.          """          if random.randint(1, 4) == 1: -            return +            return None          for coords in coord_list:              if self.game.check_win(coords, 1):                  return coords +        return None      @staticmethod      def random_coords(coord_list: list[Coordinate]) -> Coordinate:          """Picks a random coordinate from the possible ones."""          return random.choice(coord_list) -    def play(self) -> Union[Coordinate, bool]: +    def play(self) -> Coordinate | bool:          """          Plays for the AI. @@ -331,7 +332,7 @@ class ConnectFour(commands.Cog):      @staticmethod      def check_emojis(          e1: EMOJI_CHECK, e2: EMOJI_CHECK -    ) -> tuple[bool, Optional[str]]: +    ) -> tuple[bool, str | None]:          """Validate the emojis, the user put."""          if isinstance(e1, str) and emojis.count(e1) != 1:              return False, e1 @@ -342,7 +343,7 @@ class ConnectFour(commands.Cog):      async def _play_game(          self,          ctx: commands.Context, -        user: Optional[discord.Member], +        user: discord.Member | None,          board_size: int,          emoji1: str,          emoji2: str diff --git a/bot/exts/fun/duck_game.py b/bot/exts/fun/duck_game.py index a2612e51..fbdc9ea2 100644 --- a/bot/exts/fun/duck_game.py +++ b/bot/exts/fun/duck_game.py @@ -42,7 +42,7 @@ CARD_HEIGHT = 97  EMOJI_WRONG = "\u274C" -ANSWER_REGEX = re.compile(r'^\D*(\d+)\D+(\d+)\D+(\d+)\D*$') +ANSWER_REGEX = re.compile(r"^\D*(\d+)\D+(\d+)\D+(\d+)\D*$")  HELP_TEXT = """  **Each card has 4 features** @@ -97,7 +97,7 @@ def get_card_image(card: tuple[int]) -> Image:  def as_trinary(card: tuple[int]) -> int:      """Find the card's unique index by interpreting its features as trinary.""" -    return int(''.join(str(x) for x in card), base=3) +    return int("".join(str(x) for x in card), base=3)  class DuckGame: @@ -156,7 +156,7 @@ class DuckGame:                      # which is prevented by the triangle iteration.                      completion = tuple(                          feat_a if feat_a == feat_b else 3-feat_a-feat_b -                        for feat_a, feat_b in zip(card_a, card_b) +                        for feat_a, feat_b in zip(card_a, card_b, strict=True)                      )                      try:                          idx_c = self.board.index(completion) @@ -178,8 +178,8 @@ class DuckGamesDirector(commands.Cog):          self.current_games = {}      @commands.group( -        name='duckduckduckgoose', -        aliases=['dddg', 'ddg', 'duckduckgoose', 'duckgoose'], +        name="duckduckduckgoose", +        aliases=["dddg", "ddg", "duckduckgoose", "duckgoose"],          invoke_without_command=True      )      @commands.cooldown(rate=1, per=2, type=commands.BucketType.channel) @@ -218,7 +218,7 @@ class DuckGamesDirector(commands.Cog):              return          game = self.current_games[channel.id] -        if msg.content.strip().lower() == 'goose': +        if msg.content.strip().lower() == "goose":              # If all of the solutions have been claimed, i.e. the "goose" call is correct.              if len(game.solutions) == len(game.claimed_answers):                  try: @@ -248,7 +248,7 @@ class DuckGamesDirector(commands.Cog):          if answer in game.solutions:              game.claimed_answers[answer] = msg.author              game.scores[msg.author] += CORRECT_SOLN -            await self.append_to_found_embed(game, f"{str(answer):12s}  -  {msg.author.display_name}") +            await self.append_to_found_embed(game, f"{answer!s:12s}  -  {msg.author.display_name}")          else:              await msg.add_reaction(EMOJI_WRONG)              game.scores[msg.author] += INCORRECT_SOLN diff --git a/bot/exts/fun/game.py b/bot/exts/fun/game.py index a8b0b3a0..b2b18f04 100644 --- a/bot/exts/fun/game.py +++ b/bot/exts/fun/game.py @@ -2,9 +2,9 @@ import difflib  import logging  import random  import re -from datetime import datetime as dt, timedelta +from datetime import UTC, datetime, timedelta  from enum import IntEnum -from typing import Any, Optional +from typing import Any  from aiohttp import ClientSession  from discord import Embed @@ -20,8 +20,8 @@ from bot.utils.pagination import ImagePaginator, LinePaginator  # Base URL of IGDB API  BASE_URL = "https://api.igdb.com/v4" -CLIENT_ID = Tokens.igdb_client_id -CLIENT_SECRET = Tokens.igdb_client_secret +CLIENT_ID = Tokens.igdb_client_id.get_secret_value() +CLIENT_SECRET = Tokens.igdb_client_secret.get_secret_value()  # The number of seconds before expiry that we attempt to re-fetch a new access token  ACCESS_TOKEN_RENEWAL_WINDOW = 60*60*24*2 @@ -255,7 +255,7 @@ class Games(Cog):                  self.genres[genre_name] = genre      @group(name="games", aliases=("game",), invoke_without_command=True) -    async def games(self, ctx: Context, amount: Optional[int] = 5, *, genre: Optional[str]) -> None: +    async def games(self, ctx: Context, amount: int | None = 5, *, genre: str | None) -> None:          """          Get random game(s) by genre from IGDB. Use .games genres command to get all available genres. @@ -293,7 +293,8 @@ class Games(Cog):                      f"{f'Maybe you meant `{display_possibilities}`?' if display_possibilities else ''}"                  )                  return -            elif len(possibilities) == 1: + +            if len(possibilities) == 1:                  games = await self.get_games_list(                      amount, self.genres[possibilities[0][1]], offset=random.randint(0, 150)                  ) @@ -368,8 +369,8 @@ class Games(Cog):      async def get_games_list(          self,          amount: int, -        genre: Optional[str] = None, -        sort: Optional[str] = None, +        genre: str | None = None, +        sort: str | None = None,          additional_body: str = "",          offset: int = 0      ) -> list[dict[str, Any]]: @@ -398,10 +399,13 @@ class Games(Cog):      async def create_page(self, data: dict[str, Any]) -> tuple[str, str]:          """Create content of Game Page."""          # Create cover image URL from template -        url = COVER_URL.format(**{"image_id": data["cover"]["image_id"] if "cover" in data else ""}) +        url = COVER_URL.format(image_id=data.get("cover", {}).get("image_id", ""))          # Get release date separately with checking -        release_date = dt.utcfromtimestamp(data["first_release_date"]).date() if "first_release_date" in data else "?" +        if "first_release_date" in data: +            release_date = datetime.fromtimestamp(data["first_release_date"], tz=UTC).date() +        else: +            release_date = "?"          # Create Age Ratings value          rating = ", ".join( @@ -434,7 +438,7 @@ class Games(Cog):          lines = []          # Define request body of IGDB API request and do request -        body = SEARCH_BODY.format(**{"term": search_term}) +        body = SEARCH_BODY.format(term=search_term)          async with self.http_session.post(url=f"{BASE_URL}/games", data=body, headers=self.headers) as resp:              data = await resp.json() @@ -460,10 +464,10 @@ class Games(Cog):          returning results.          """          # Create request body from template -        body = COMPANIES_LIST_BODY.format(**{ -            "limit": limit, -            "offset": offset -        }) +        body = COMPANIES_LIST_BODY.format( +            limit=limit, +            offset=offset, +        )          async with self.http_session.post(url=f"{BASE_URL}/companies", data=body, headers=self.headers) as resp:              return await resp.json() @@ -471,10 +475,10 @@ class Games(Cog):      async def create_company_page(self, data: dict[str, Any]) -> tuple[str, str]:          """Create good formatted Game Company page."""          # Generate URL of company logo -        url = LOGO_URL.format(**{"image_id": data["logo"]["image_id"] if "logo" in data else ""}) +        url = LOGO_URL.format(image_id=data.get("logo", {}).get("image_id", ""))          # Try to get found date of company -        founded = dt.utcfromtimestamp(data["start_date"]).date() if "start_date" in data else "?" +        founded = datetime.fromtimestamp(data["start_date"], tz=UTC).date() if "start_date" in data else "?"          # Generate list of games, that company have developed or published          developed = ", ".join(game["name"] for game in data["developed"]) if "developed" in data else "?" diff --git a/bot/exts/fun/hangman.py b/bot/exts/fun/hangman.py index f385a955..7a02a552 100644 --- a/bot/exts/fun/hangman.py +++ b/bot/exts/fun/hangman.py @@ -89,7 +89,7 @@ class Hangman(commands.Cog):          word = choice(filtered_words)          # `pretty_word` is used for comparing the indices where the guess of the user is similar to the word          # The `user_guess` variable is prettified by adding spaces between every dash, and so is the `pretty_word` -        pretty_word = ''.join([f"{letter} " for letter in word])[:-1] +        pretty_word = "".join([f"{letter} " for letter in word])[:-1]          user_guess = ("_ " * len(word))[:-1]          tries = 6          guessed_letters = set() @@ -104,7 +104,7 @@ class Hangman(commands.Cog):          ))          # Game loop -        while user_guess.replace(' ', '') != word: +        while user_guess.replace(" ", "") != word:              # Edit the message to the current state of the game              await original_message.edit(embed=self.create_embed(tries, user_guess)) @@ -136,7 +136,7 @@ class Hangman(commands.Cog):                  continue              # Checks for repeated guesses -            elif normalized_content in guessed_letters: +            if normalized_content in guessed_letters:                  already_guessed_embed = Embed(                      title=choice(NEGATIVE_REPLIES),                      description=f"You have already guessed `{normalized_content}`, try again!", @@ -146,12 +146,11 @@ class Hangman(commands.Cog):                  continue              # Checks for correct guesses from the user -            elif normalized_content in word: +            if normalized_content in word:                  positions = {idx for idx, letter in enumerate(pretty_word) if letter == normalized_content}                  user_guess = "".join(                      [normalized_content if index in positions else dash for index, dash in enumerate(user_guess)]                  ) -              else:                  tries -= 1 diff --git a/bot/exts/fun/latex.py b/bot/exts/fun/latex.py index 8af05413..12f2d0b6 100644 --- a/bot/exts/fun/latex.py +++ b/bot/exts/fun/latex.py @@ -1,19 +1,22 @@  import hashlib +import logging  import os  import re  import string  from io import BytesIO  from pathlib import Path -from typing import BinaryIO, Optional +from typing import BinaryIO  import discord  from PIL import Image +from aiohttp import web  from discord.ext import commands  from bot.bot import Bot  from bot.constants import Channels, WHITELISTED_CHANNELS  from bot.utils.decorators import whitelist_override +log = logging.getLogger(__name__)  FORMATTED_CODE_REGEX = re.compile(      r"(?P<delim>(?P<block>```)|``?)"        # code delimiter: 1-3 backticks; (?P=block) only matches if it's a block      r"(?(block)(?:(?P<lang>[a-z]+)\n)?)"    # if we're in a block, match optional language (only letters plus newline) @@ -45,8 +48,7 @@ def _prepare_input(text: str) -> str:      """Extract latex from a codeblock, if it is in one."""      if match := FORMATTED_CODE_REGEX.match(text):          return match.group("code") -    else: -        return text +    return text  def _process_image(data: bytes, out_file: BinaryIO) -> None: @@ -65,7 +67,7 @@ def _process_image(data: bytes, out_file: BinaryIO) -> None:  class InvalidLatexError(Exception):      """Represents an error caused by invalid latex.""" -    def __init__(self, logs: Optional[str]): +    def __init__(self, logs: str | None):          super().__init__(logs)          self.logs = logs @@ -89,7 +91,7 @@ class Latex(commands.Cog):          ) as response:              _process_image(await response.read(), out_file) -    async def _upload_to_pastebin(self, text: str) -> Optional[str]: +    async def _upload_to_pastebin(self, text: str) -> str | None:          """Uploads `text` to the paste service, returning the url if successful."""          try:              async with self.bot.http_session.post( @@ -100,9 +102,8 @@ class Latex(commands.Cog):                  response_json = await response.json()              if "key" in response_json:                  return f"{PASTEBIN_URL}/{response_json['key']}.txt?noredirect" -        except Exception: -            # 400 (Bad Request) means there are too many characters -            pass +        except web.HTTPClientError as e: +            log.info("Error when uploading latex output to pastebin. %s", e)      @commands.command()      @commands.max_concurrency(1, commands.BucketType.guild, wait=True) @@ -112,7 +113,7 @@ class Latex(commands.Cog):          query = _prepare_input(query)          # the hash of the query is used as the filename in the cache. -        query_hash = hashlib.md5(query.encode()).hexdigest() +        query_hash = hashlib.md5(query.encode()).hexdigest()  # noqa: S324          image_path = CACHE_DIRECTORY / f"{query_hash}.png"          async with ctx.typing():              if not image_path.exists(): diff --git a/bot/exts/fun/madlibs.py b/bot/exts/fun/madlibs.py index 075dde75..c14e8a3a 100644 --- a/bot/exts/fun/madlibs.py +++ b/bot/exts/fun/madlibs.py @@ -117,7 +117,7 @@ class Madlibs(commands.Cog):          self.checks.remove(author_check)          story = [] -        for value, blank in zip(random_template["value"], blanks): +        for value, blank in zip(random_template["value"], blanks, strict=True):              story.append(f"{value}__{blank}__")          # In each story template, there is always one more "value" diff --git a/bot/exts/fun/minesweeper.py b/bot/exts/fun/minesweeper.py index f16b1db2..69be88d3 100644 --- a/bot/exts/fun/minesweeper.py +++ b/bot/exts/fun/minesweeper.py @@ -2,7 +2,6 @@ import logging  from collections.abc import Iterator  from dataclasses import dataclass  from random import randint, random -from typing import Union  import discord  from discord.ext import commands @@ -33,7 +32,7 @@ MESSAGE_MAPPING = {  log = logging.getLogger(__name__) -GameBoard = list[list[Union[str, int]]] +GameBoard = list[list[str | int]]  @dataclass @@ -205,9 +204,9 @@ class Minesweeper(commands.Cog):              for y in range(10)          ):              return False -        else: -            await self.won(ctx) -            return True + +        await self.won(ctx) +        return True      async def reveal_one(          self, @@ -227,7 +226,7 @@ class Minesweeper(commands.Cog):              await self.lost(ctx)              revealed[y][x] = "x"  # mark bomb that made you lose with a x              return True -        elif board[y][x] == 0: +        if board[y][x] == 0:              self.reveal_zeros(revealed, board, x, y)          return await self.check_if_won(ctx, revealed, board) diff --git a/bot/exts/fun/movie.py b/bot/exts/fun/movie.py index 422a83ac..3d36b119 100644 --- a/bot/exts/fun/movie.py +++ b/bot/exts/fun/movie.py @@ -22,7 +22,7 @@ THUMBNAIL_URL = "https://i.imgur.com/LtFtC8H.png"  # Define movie params, that will be used for every movie request  MOVIE_PARAMS = { -    "api_key": Tokens.tmdb, +    "api_key": Tokens.tmdb.get_secret_value(),      "language": "en-US"  } @@ -63,17 +63,17 @@ class Movie(Cog):      @group(name="movies", aliases=("movie",), invoke_without_command=True)      async def movies(self, ctx: Context, genre: str = "", amount: int = 5) -> None:          """ -        Get random movies by specifying genre. Also support amount parameter,\ -        that define how much movies will be shown. +        Get random movies by specifying genre. -        Default 5. Use .movies genres to get all available genres. +        The amount parameter, that defines how many movies will be shown, defaults to 5. +        Use `.movies genres` to get all available genres.          """          # Check is there more than 20 movies specified, due TMDB return 20 movies          # per page, so this is max. Also you can't get less movies than 1, just logic          if amount > 20:              await ctx.send("You can't get more than 20 movies at once. (TMDB limits)")              return -        elif amount < 1: +        if amount < 1:              await ctx.send("You can't get less than 1 movie.")              return @@ -106,7 +106,7 @@ class Movie(Cog):          """Return JSON of TMDB discover request."""          # Define params of request          params = { -            "api_key": Tokens.tmdb, +            "api_key": Tokens.tmdb.get_secret_value(),              "language": "en-US",              "sort_by": "popularity.desc",              "include_adult": "false", @@ -179,8 +179,8 @@ class Movie(Cog):          text += "__**Some Numbers**__\n" -        budget = f"{movie['budget']:,d}" if movie['budget'] else "?" -        revenue = f"{movie['revenue']:,d}" if movie['revenue'] else "?" +        budget = f"{movie['budget']:,d}" if movie["budget"] else "?" +        revenue = f"{movie['revenue']:,d}" if movie["revenue"] else "?"          if movie["runtime"] is not None:              duration = divmod(movie["runtime"], 60) @@ -208,4 +208,7 @@ class Movie(Cog):  async def setup(bot: Bot) -> None:      """Load the Movie Cog.""" +    if not Tokens.tmdb: +        logger.warning("No TMDB token. Not loading Movie Cog.") +        return      await bot.add_cog(Movie(bot)) diff --git a/bot/exts/fun/quack.py b/bot/exts/fun/quack.py index bb0cd731..9bb024fc 100644 --- a/bot/exts/fun/quack.py +++ b/bot/exts/fun/quack.py @@ -1,6 +1,6 @@  import logging  import random -from typing import Literal, Optional +from typing import Literal  import discord  from discord.ext import commands @@ -8,7 +8,7 @@ from discord.ext import commands  from bot.bot import Bot  from bot.constants import Colours, NEGATIVE_REPLIES -API_URL = 'https://quackstack.pythondiscord.com' +API_URL = "https://quackstack.pythondiscord.com"  log = logging.getLogger(__name__) @@ -25,7 +25,7 @@ class Quackstack(commands.Cog):          ctx: commands.Context,          ducktype: Literal["duck", "manduck"] = "duck",          *, -        seed: Optional[str] = None +        seed: str | None = None      ) -> None:          """          Use the Quackstack API to generate a random duck. diff --git a/bot/exts/fun/snakes/__init__.py b/bot/exts/fun/snakes/__init__.py index 8aa39fb5..be71ac44 100644 --- a/bot/exts/fun/snakes/__init__.py +++ b/bot/exts/fun/snakes/__init__.py @@ -1,6 +1,7 @@  import logging  from bot.bot import Bot +from bot.constants import Tokens  from bot.exts.fun.snakes._snakes_cog import Snakes  log = logging.getLogger(__name__) @@ -8,4 +9,6 @@ log = logging.getLogger(__name__)  async def setup(bot: Bot) -> None:      """Load the Snakes Cog.""" +    if not Tokens.youtube: +        log.warning("No Youtube token. All YouTube related commands in Snakes cog won't work.")      await bot.add_cog(Snakes(bot)) diff --git a/bot/exts/fun/snakes/_snakes_cog.py b/bot/exts/fun/snakes/_snakes_cog.py index d0542c23..eca462c6 100644 --- a/bot/exts/fun/snakes/_snakes_cog.py +++ b/bot/exts/fun/snakes/_snakes_cog.py @@ -9,7 +9,7 @@ import textwrap  import urllib  from functools import partial  from io import BytesIO -from typing import Any, Optional +from typing import Any  import async_timeout  from PIL import Image, ImageDraw, ImageFont @@ -274,7 +274,7 @@ class Snakes(Cog):          return message -    async def _fetch(self, url: str, params: Optional[dict] = None) -> dict: +    async def _fetch(self, url: str, params: dict | None = None) -> dict:          """Asynchronous web request helper method."""          if params is None:              params = {} @@ -518,52 +518,51 @@ class Snakes(Cog):                  log.debug("Antidote timed out waiting for a reaction")                  break  # We're done, no reactions for the last 5 minutes -            if antidote_tries < 10: -                if antidote_guess_count < 4: -                    if reaction.emoji in ANTIDOTE_EMOJI: -                        antidote_guess_list.append(reaction.emoji) -                        antidote_guess_count += 1 - -                    if antidote_guess_count == 4:  # Guesses complete -                        antidote_guess_count = 0 -                        page_guess_list[antidote_tries] = " ".join(antidote_guess_list) - -                        # Now check guess -                        for i in range(0, len(antidote_answer)): -                            if antidote_guess_list[i] == antidote_answer[i]: -                                guess_result.append(TICK_EMOJI) -                            elif antidote_guess_list[i] in antidote_answer: -                                guess_result.append(BLANK_EMOJI) -                            else: -                                guess_result.append(CROSS_EMOJI) -                        guess_result.sort() -                        page_result_list[antidote_tries] = " ".join(guess_result) - -                        # Rebuild the board -                        board = [] -                        for i in range(0, 10): -                            board.append(f"`{i+1:02d}` " -                                         f"{page_guess_list[i]} - " -                                         f"{page_result_list[i]}") -                            board.append(EMPTY_UNICODE) - -                        # Remove Reactions -                        for emoji in antidote_guess_list: -                            await board_id.remove_reaction(emoji, user) - -                        if antidote_guess_list == antidote_answer: -                            win = True - -                        antidote_tries += 1 -                        guess_result = [] -                        antidote_guess_list = [] - -                        antidote_embed.clear_fields() -                        antidote_embed.add_field(name=f"{10 - antidote_tries} " -                                                      f"guesses remaining", -                                                 value="\n".join(board)) -                        # Redisplay the board -                        await board_id.edit(embed=antidote_embed) +            if antidote_tries < 10 and antidote_guess_count < 4: +                if reaction.emoji in ANTIDOTE_EMOJI: +                    antidote_guess_list.append(reaction.emoji) +                    antidote_guess_count += 1 + +                if antidote_guess_count == 4:  # Guesses complete +                    antidote_guess_count = 0 +                    page_guess_list[antidote_tries] = " ".join(antidote_guess_list) + +                    # Now check guess +                    for i in range(0, len(antidote_answer)): +                        if antidote_guess_list[i] == antidote_answer[i]: +                            guess_result.append(TICK_EMOJI) +                        elif antidote_guess_list[i] in antidote_answer: +                            guess_result.append(BLANK_EMOJI) +                        else: +                            guess_result.append(CROSS_EMOJI) +                    guess_result.sort() +                    page_result_list[antidote_tries] = " ".join(guess_result) + +                    # Rebuild the board +                    board = [] +                    for i in range(0, 10): +                        board.append(f"`{i+1:02d}` " +                                     f"{page_guess_list[i]} - " +                                     f"{page_result_list[i]}") +                        board.append(EMPTY_UNICODE) + +                    # Remove Reactions +                    for emoji in antidote_guess_list: +                        await board_id.remove_reaction(emoji, user) + +                    if antidote_guess_list == antidote_answer: +                        win = True + +                    antidote_tries += 1 +                    guess_result = [] +                    antidote_guess_list = [] + +                    antidote_embed.clear_fields() +                    antidote_embed.add_field(name=f"{10 - antidote_tries} " +                                                  f"guesses remaining", +                                             value="\n".join(board)) +                    # Redisplay the board +                    await board_id.edit(embed=antidote_embed)          # Winning / Ending Screen          if win is True: @@ -746,10 +745,10 @@ class Snakes(Cog):          await message.delete()          # Build and send the embed. -        my_snake_embed = Embed(description=":tada: Congrats! You hatched: **{0}**".format(snake_name)) +        my_snake_embed = Embed(description=f":tada: Congrats! You hatched: **{snake_name}**")          my_snake_embed.set_thumbnail(url=snake_image)          my_snake_embed.set_footer( -            text=" Owner: {0}#{1}".format(ctx.author.name, ctx.author.discriminator) +            text=f" Owner: {ctx.author.name}#{ctx.author.discriminator}"          )          await ctx.send(embed=my_snake_embed) @@ -773,7 +772,7 @@ class Snakes(Cog):                      "query": "snake",                      "page": page,                      "language": "en-US", -                    "api_key": Tokens.tmdb, +                    "api_key": Tokens.tmdb.get_secret_value(),                  }              )              data = await response.json() @@ -785,7 +784,7 @@ class Snakes(Cog):                  f"https://api.themoviedb.org/3/movie/{movie}",                  params={                      "language": "en-US", -                    "api_key": Tokens.tmdb, +                    "api_key": Tokens.tmdb.get_secret_value(),                  }              )              data = await response.json() @@ -832,7 +831,7 @@ class Snakes(Cog):          # Prepare a question.          question = random.choice(self.snake_quizzes)          answer = question["answerkey"] -        options = {key: question["options"][key] for key in ANSWERS_EMOJI.keys()} +        options = {key: question["options"][key] for key in ANSWERS_EMOJI}          # Build and send the embed.          embed = Embed( @@ -879,10 +878,7 @@ class Snakes(Cog):              snake_name = snake_name.split()[-1]          # If no name is provided, use whoever called the command. -        if name: -            user_name = name -        else: -            user_name = ctx.author.display_name +        user_name = name if name else ctx.author.display_name          # Get the index of the vowel to slice the username at          user_slice_index = len(user_name) @@ -1095,7 +1091,7 @@ class Snakes(Cog):                  "part": "snippet",                  "q": urllib.parse.quote_plus(query),                  "type": "video", -                "key": Tokens.youtube +                "key": Tokens.youtube.get_secret_value()              }          )          response = await response.json() @@ -1148,3 +1144,4 @@ class Snakes(Cog):              embed.description = "Could not generate the snake card! Please try again."              embed.title = random.choice(ERROR_REPLIES)              await ctx.send(embed=embed) +    # endregion diff --git a/bot/exts/fun/snakes/_utils.py b/bot/exts/fun/snakes/_utils.py index 182fa9d9..ffffcd34 100644 --- a/bot/exts/fun/snakes/_utils.py +++ b/bot/exts/fun/snakes/_utils.py @@ -6,7 +6,6 @@ import math  import random  from itertools import product  from pathlib import Path -from typing import Union  from PIL import Image  from PIL.ImageDraw import ImageDraw @@ -132,7 +131,7 @@ def lerp(t: float, a: float, b: float) -> float:      return a + t * (b - a) -class PerlinNoiseFactory(object): +class PerlinNoiseFactory:      """      Callable that produces Perlin noise for an arbitrary point in an arbitrary number of dimensions. @@ -396,7 +395,7 @@ class SnakeAndLaddersGame:          Listen for reactions until players have joined, and the game has been started.          """ -        def startup_event_check(reaction_: Reaction, user_: Union[User, Member]) -> bool: +        def startup_event_check(reaction_: Reaction, user_: User | Member) -> bool:              """Make sure that this reaction is what we want to operate on."""              return (                  all(( @@ -445,14 +444,12 @@ class SnakeAndLaddersGame:                          # Allow game author or non-playing moderation staff to cancel a waiting game                          await self.cancel_game()                          return -                    else: -                        await self.player_leave(user) -                elif reaction.emoji == START_EMOJI: -                    if self.ctx.author == user: -                        self.started = True -                        await self.start_game(user) -                        await startup.delete() -                        break +                    await self.player_leave(user) +                elif reaction.emoji == START_EMOJI and self.ctx.author == user: +                    self.started = True +                    await self.start_game(user) +                    await startup.delete() +                    break                  await startup.remove_reaction(reaction.emoji, user) @@ -461,7 +458,7 @@ class SnakeAndLaddersGame:                  await self.cancel_game()                  return  # We're done, no reactions for the last 5 minutes -    async def _add_player(self, user: Union[User, Member]) -> None: +    async def _add_player(self, user: User | Member) -> None:          """Add player to game."""          self.players.append(user)          self.player_tiles[user.id] = 1 @@ -470,7 +467,7 @@ class SnakeAndLaddersGame:          im = Image.open(io.BytesIO(avatar_bytes)).resize((BOARD_PLAYER_SIZE, BOARD_PLAYER_SIZE))          self.avatar_images[user.id] = im -    async def player_join(self, user: Union[User, Member]) -> None: +    async def player_join(self, user: User | Member) -> None:          """          Handle players joining the game. @@ -492,11 +489,11 @@ class SnakeAndLaddersGame:          await self.channel.send(              f"**Snakes and Ladders**: {user.mention} has joined the game.\n" -            f"There are now {str(len(self.players))} players in the game.", +            f"There are now {len(self.players)!s} players in the game.",              delete_after=10          ) -    async def player_leave(self, user: Union[User, Member]) -> bool: +    async def player_leave(self, user: User | Member) -> bool:          """          Handle players leaving the game. @@ -531,17 +528,17 @@ class SnakeAndLaddersGame:          await self.channel.send("**Snakes and Ladders**: Game has been canceled.")          self._destruct() -    async def start_game(self, user: Union[User, Member]) -> None: +    async def start_game(self, user: User | Member) -> None:          """          Allow the game author to begin the game.          The game cannot be started if the game is in a waiting state.          """ -        if not user == self.author: +        if user != self.author:              await self.channel.send(user.mention + " Only the author of the game can start it.", delete_after=10)              return -        if not self.state == "waiting": +        if self.state != "waiting":              await self.channel.send(user.mention + " The game cannot be started at this time.", delete_after=10)              return @@ -552,7 +549,7 @@ class SnakeAndLaddersGame:      async def start_round(self) -> None:          """Begin the round.""" -        def game_event_check(reaction_: Reaction, user_: Union[User, Member]) -> bool: +        def game_event_check(reaction_: Reaction, user_: User | Member) -> bool:              """Make sure that this reaction is what we want to operate on."""              return (                  all(( @@ -626,8 +623,7 @@ class SnakeAndLaddersGame:                          # Only allow non-playing moderation staff to cancel a running game                          await self.cancel_game()                          return -                    else: -                        is_surrendered = await self.player_leave(user) +                    is_surrendered = await self.player_leave(user)                  await self.positions.remove_reaction(reaction.emoji, user) @@ -645,7 +641,7 @@ class SnakeAndLaddersGame:          if not is_surrendered:              await self._complete_round() -    async def player_roll(self, user: Union[User, Member]) -> None: +    async def player_roll(self, user: User | Member) -> None:          """Handle the player's roll."""          if user.id not in self.player_tiles:              await self.channel.send(user.mention + " You are not in the match.", delete_after=10) @@ -692,7 +688,7 @@ class SnakeAndLaddersGame:          await self.channel.send("**Snakes and Ladders**: " + winner.mention + " has won the game! :tada:")          self._destruct() -    def _check_winner(self) -> Union[User, Member]: +    def _check_winner(self) -> User | Member:          """Return a winning member if we're in the post-round state and there's a winner."""          if self.state != "post_round":              return None @@ -717,6 +713,6 @@ class SnakeAndLaddersGame:          return x_level, y_level      @staticmethod -    def _is_moderator(user: Union[User, Member]) -> bool: +    def _is_moderator(user: User | Member) -> bool:          """Return True if the user is a Moderator.""" -        return any(role.id in MODERATION_ROLES for role in getattr(user, 'roles', [])) +        return any(role.id in MODERATION_ROLES for role in getattr(user, "roles", [])) diff --git a/bot/exts/fun/space.py b/bot/exts/fun/space.py index 22a89050..3a666bfc 100644 --- a/bot/exts/fun/space.py +++ b/bot/exts/fun/space.py @@ -1,7 +1,7 @@  import logging  import random -from datetime import date, datetime -from typing import Any, Optional +from datetime import UTC, date, datetime +from typing import Any  from urllib.parse import urlencode  from discord import Embed @@ -53,7 +53,7 @@ class Space(Cog):          await self.bot.invoke_help_command(ctx)      @space.command(name="apod") -    async def apod(self, ctx: Context, date: Optional[str]) -> None: +    async def apod(self, ctx: Context, date: str | None) -> None:          """          Get Astronomy Picture of Day from NASA API. Date is optional parameter, what formatting is YYYY-MM-DD. @@ -63,13 +63,13 @@ class Space(Cog):          # Parse date to params, when provided. Show error message when invalid formatting          if date:              try: -                apod_date = datetime.strptime(date, "%Y-%m-%d").date() +                apod_date = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=UTC).date()              except ValueError:                  await ctx.send(f"Invalid date {date}. Please make sure your date is in format YYYY-MM-DD.")                  return -            now = datetime.now().date() -            if APOD_MIN_DATE > apod_date or now < apod_date: +            now = datetime.now(tz=UTC).date() +            if apod_date < APOD_MIN_DATE or now < apod_date:                  await ctx.send(f"Date must be between {APOD_MIN_DATE.isoformat()} and {now.isoformat()} (today).")                  return @@ -86,7 +86,7 @@ class Space(Cog):          )      @space.command(name="nasa") -    async def nasa(self, ctx: Context, *, search_term: Optional[str]) -> None: +    async def nasa(self, ctx: Context, *, search_term: str | None) -> None:          """Get random NASA information/facts + image. Support `search_term` parameter for more specific search."""          params = {              "media_type": "image" @@ -111,11 +111,11 @@ class Space(Cog):          )      @space.command(name="epic") -    async def epic(self, ctx: Context, date: Optional[str]) -> None: +    async def epic(self, ctx: Context, date: str | None) -> None:          """Get a random image of the Earth from the NASA EPIC API. Support date parameter, format is YYYY-MM-DD."""          if date:              try: -                show_date = datetime.strptime(date, "%Y-%m-%d").date().isoformat() +                show_date = datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=UTC).date().isoformat()              except ValueError:                  await ctx.send(f"Invalid date {date}. Please make sure your date is in format YYYY-MM-DD.")                  return @@ -147,7 +147,7 @@ class Space(Cog):      async def mars(          self,          ctx: Context, -        date: Optional[DateConverter], +        date: DateConverter | None,          rover: str = "curiosity"      ) -> None:          """ @@ -158,10 +158,8 @@ class Space(Cog):          rover = rover.lower()          if rover not in self.rovers:              await ctx.send( -                ( -                    f"Invalid rover `{rover}`.\n" -                    f"**Rovers:** `{'`, `'.join(f'{r.capitalize()}' for r in self.rovers)}`" -                ) +                f"Invalid rover `{rover}`.\n" +                f"**Rovers:** `{'`, `'.join(f'{r.capitalize()}' for r in self.rovers)}`"              )              return @@ -203,14 +201,14 @@ class Space(Cog):      async def fetch_from_nasa(          self,          endpoint: str, -        additional_params: Optional[dict[str, Any]] = None, -        base: Optional[str] = NASA_BASE_URL, +        additional_params: dict[str, Any] | None = None, +        base: str | None = NASA_BASE_URL,          use_api_key: bool = True      ) -> dict[str, Any]:          """Fetch information from NASA API, return result."""          params = {}          if use_api_key: -            params["api_key"] = Tokens.nasa +            params["api_key"] = Tokens.nasa.get_secret_value()          # Add additional parameters to request parameters only when they provided by user          if additional_params is not None: @@ -219,7 +217,7 @@ class Space(Cog):          async with self.http_session.get(url=f"{base}/{endpoint}?{urlencode(params)}") as resp:              return await resp.json() -    def create_nasa_embed(self, title: str, description: str, image: str, footer: Optional[str] = "") -> Embed: +    def create_nasa_embed(self, title: str, description: str, image: str, footer: str | None = "") -> Embed:          """Generate NASA commands embeds. Required: title, description and image URL, footer (addition) is optional."""          return Embed(              title=title, diff --git a/bot/exts/fun/tic_tac_toe.py b/bot/exts/fun/tic_tac_toe.py index fa2a7531..d4ae7107 100644 --- a/bot/exts/fun/tic_tac_toe.py +++ b/bot/exts/fun/tic_tac_toe.py @@ -1,6 +1,6 @@  import asyncio  import random -from typing import Callable, Optional, Union +from collections.abc import Callable  import discord  from discord.ext.commands import Cog, Context, check, group @@ -42,7 +42,7 @@ class Player:          self.ctx = ctx          self.symbol = symbol -    async def get_move(self, board: dict[int, str], msg: discord.Message) -> tuple[bool, Optional[int]]: +    async def get_move(self, board: dict[int, str], msg: discord.Message) -> tuple[bool, int | None]:          """          Get move from user. @@ -106,7 +106,7 @@ class AI:  class Game:      """Class that contains information and functions about Tic Tac Toe game.""" -    def __init__(self, players: list[Union[Player, AI]], ctx: Context): +    def __init__(self, players: list[Player | AI], ctx: Context):          self.players = players          self.ctx = ctx          self.channel = ctx.channel @@ -125,13 +125,13 @@ class Game:          self.current = self.players[0]          self.next = self.players[1] -        self.winner: Optional[Union[Player, AI]] = None -        self.loser: Optional[Union[Player, AI]] = None +        self.winner: Player | AI | None = None +        self.loser: Player | AI | None = None          self.over = False          self.canceled = False          self.draw = False -    async def get_confirmation(self) -> tuple[bool, Optional[str]]: +    async def get_confirmation(self) -> tuple[bool, str | None]:          """          Ask does user want to play TicTacToe against requester. First player is always requester. @@ -171,10 +171,10 @@ class Game:          await confirm_message.delete()          if reaction.emoji == Emojis.confirmation:              return True, None -        else: -            self.over = True -            self.canceled = True -            return False, "User declined" + +        self.over = True +        self.canceled = True +        return False, "User declined"      @staticmethod      async def add_reactions(msg: discord.Message) -> None: @@ -186,7 +186,8 @@ class Game:          """Get formatted tic-tac-toe board for message."""          board = list(self.board.values())          return "\n".join( -            (f"{board[line]} {board[line + 1]} {board[line + 2]}" for line in range(0, len(board), 3)) +            f"{board[line]} {board[line + 1]} {board[line + 2]}" +            for line in range(0, len(board), 3)          )      async def play(self) -> None: @@ -256,7 +257,7 @@ class TicTacToe(Cog):      @is_channel_free()      @is_requester_free()      @group(name="tictactoe", aliases=("ttt", "tic"), invoke_without_command=True) -    async def tic_tac_toe(self, ctx: Context, opponent: Optional[discord.User]) -> None: +    async def tic_tac_toe(self, ctx: Context, opponent: discord.User | None) -> None:          """Tic Tac Toe game. Play against friends or AI. Use reactions to add your mark to field."""          if opponent == ctx.author:              await ctx.send("You can't play against yourself.") diff --git a/bot/exts/fun/trivia_quiz.py b/bot/exts/fun/trivia_quiz.py index 31652374..28cd4657 100644 --- a/bot/exts/fun/trivia_quiz.py +++ b/bot/exts/fun/trivia_quiz.py @@ -6,10 +6,10 @@ import random  import re  import string  from collections import defaultdict +from collections.abc import Callable  from dataclasses import dataclass -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta  from pathlib import Path -from typing import Callable, Optional  import discord  from discord.ext import commands, tasks @@ -249,7 +249,7 @@ class TriviaQuiz(commands.Cog):          wiki_questions = []          # trivia_quiz.json follows a pattern, every new category starts with the next century.          start_id = 501 -        yesterday = datetime.strftime(datetime.now() - timedelta(1), '%Y/%m/%d') +        yesterday = datetime.strftime(datetime.now(tz=UTC) - timedelta(1), "%Y/%m/%d")          while error_fetches < MAX_ERROR_FETCH_TRIES:              async with self.bot.http_session.get(url=WIKI_FEED_API_URL.format(date=yesterday)) as r: @@ -267,7 +267,7 @@ class TriviaQuiz(commands.Cog):                      # Normalize the wikipedia article title to remove all punctuations from it                      for word in re.split(r"[\s-]", title := article["normalizedtitle"]):                          cleaned_title = re.sub( -                            rf'\b{word.strip(string.punctuation)}\b', word, title, flags=re.IGNORECASE +                            rf"\b{word.strip(string.punctuation)}\b", word, title, flags=re.IGNORECASE                          )                      # Since the extract contains the article name sometimes this would replace all the matching words @@ -279,7 +279,7 @@ class TriviaQuiz(commands.Cog):                      for word in re.split(r"[\s-]", cleaned_title):                          word = word.strip(string.punctuation)                          secret_word = r"\*" * len(word) -                        question = re.sub(rf'\b{word}\b', f"**{secret_word}**", question, flags=re.IGNORECASE) +                        question = re.sub(rf"\b{word}\b", f"**{secret_word}**", question, flags=re.IGNORECASE)                      formatted_article_question = {                          "id": start_id, @@ -307,7 +307,7 @@ class TriviaQuiz(commands.Cog):          return json.loads(p.read_text(encoding="utf-8"))      @commands.group(name="quiz", aliases=("trivia", "triviaquiz"), invoke_without_command=True) -    async def quiz_game(self, ctx: commands.Context, category: Optional[str], questions: Optional[int]) -> None: +    async def quiz_game(self, ctx: commands.Context, category: str | None, questions: int | None) -> None:          """          Start a quiz! @@ -550,7 +550,7 @@ class TriviaQuiz(commands.Cog):              if self.game_status[ctx.channel.id]:                  # Check if the author is the game starter or a moderator.                  if ctx.author == self.game_owners[ctx.channel.id] or any( -                    role.id in MODERATION_ROLES for role in getattr(ctx.author, 'roles', []) +                    role.id in MODERATION_ROLES for role in getattr(ctx.author, "roles", [])                  ):                      self.game_status[ctx.channel.id] = False                      del self.game_owners[ctx.channel.id] diff --git a/bot/exts/fun/uwu.py b/bot/exts/fun/uwu.py index 7a9d55d0..488c68f3 100644 --- a/bot/exts/fun/uwu.py +++ b/bot/exts/fun/uwu.py @@ -30,7 +30,7 @@ EMOJIS = [      "o.O",      "-.-",      ">w<", -    "ĻĻĻ", +    "ĻĻĻ",  # noqa: RUF001      "òĻó",      "ŹwŹ",      ":3", @@ -74,7 +74,7 @@ class Emoji:          return bot.get_emoji(self.uid) is not None      @classmethod -    def from_match(cls, match: tuple[str, str, str]) -> t.Optional['Emoji']: +    def from_match(cls, match: tuple[str, str, str]) -> t.Optional["Emoji"]:          """Creates an Emoji from a regex match tuple."""          if not match or len(match) != 3 or not match[2].isdecimal():              return None @@ -155,7 +155,7 @@ class Uwu(Cog):          return input_string      @commands.command(name="uwu", aliases=("uwuwize", "uwuify",)) -    async def uwu_command(self, ctx: Context, *, text: t.Optional[str] = None) -> None: +    async def uwu_command(self, ctx: Context, *, text: str | None = None) -> None:          """          Echo an uwuified version the passed text. diff --git a/bot/exts/fun/wonder_twins.py b/bot/exts/fun/wonder_twins.py index 0c5b0a76..9385780d 100644 --- a/bot/exts/fun/wonder_twins.py +++ b/bot/exts/fun/wonder_twins.py @@ -11,8 +11,8 @@ class WonderTwins(Cog):      """Cog for a Wonder Twins inspired command."""      def __init__(self): -        with open(Path.cwd() / "bot" / "resources" / "fun" / "wonder_twins.yaml", "r", encoding="utf-8") as f: -            info = yaml.load(f, Loader=yaml.FullLoader) +        with open(Path.cwd() / "bot" / "resources" / "fun" / "wonder_twins.yaml", encoding="utf-8") as f: +            info = yaml.safe_load(f)              self.water_types = info["water_types"]              self.objects = info["objects"]              self.adjectives = info["adjectives"] diff --git a/bot/exts/fun/xkcd.py b/bot/exts/fun/xkcd.py index 380c3c80..7b34795c 100644 --- a/bot/exts/fun/xkcd.py +++ b/bot/exts/fun/xkcd.py @@ -1,7 +1,6 @@  import logging  import re  from random import randint -from typing import Optional, Union  from discord import Embed  from discord.ext import tasks @@ -21,7 +20,7 @@ class XKCD(Cog):      def __init__(self, bot: Bot):          self.bot = bot -        self.latest_comic_info: dict[str, Union[str, int]] = {} +        self.latest_comic_info: dict[str, str | int] = {}          self.get_latest_comic_info.start()      def cog_unload(self) -> None: @@ -38,7 +37,7 @@ class XKCD(Cog):                  log.debug(f"Failed to get latest XKCD comic information. Status code {resp.status}")      @command(name="xkcd") -    async def fetch_xkcd_comics(self, ctx: Context, comic: Optional[str]) -> None: +    async def fetch_xkcd_comics(self, ctx: Context, comic: str | None) -> None:          """          Getting an xkcd comic's information along with the image. diff --git a/bot/exts/holidays/easter/bunny_name_generator.py b/bot/exts/holidays/easter/bunny_name_generator.py index 50872ebc..3034da4a 100644 --- a/bot/exts/holidays/easter/bunny_name_generator.py +++ b/bot/exts/holidays/easter/bunny_name_generator.py @@ -3,7 +3,6 @@ import logging  import random  import re  from pathlib import Path -from typing import Optional  from discord.ext import commands @@ -18,7 +17,7 @@ class BunnyNameGenerator(commands.Cog):      """Generate a random bunny name, or bunnify your Discord username!"""      @staticmethod -    def find_separators(displayname: str) -> Optional[list[str]]: +    def find_separators(displayname: str) -> list[str] | None:          """Check if Discord name contains spaces so we can bunnify an individual word in the name."""          new_name = re.split(r"[_.\s]", displayname)          if displayname not in new_name: @@ -26,7 +25,7 @@ class BunnyNameGenerator(commands.Cog):          return None      @staticmethod -    def find_vowels(displayname: str) -> Optional[str]: +    def find_vowels(displayname: str) -> str | None:          """          Finds vowels in the user's display name. @@ -46,6 +45,7 @@ class BunnyNameGenerator(commands.Cog):              new_name = re.sub(exp, vowel_sub, displayname)              if new_name != displayname:                  return new_name +        return None      @staticmethod      def append_name(displayname: str) -> str: @@ -77,7 +77,7 @@ class BunnyNameGenerator(commands.Cog):          unmatched_name = self.append_name(username)          if spaces_in_name is not None: -            replacements = ["Cotton", "Fluff", "Floof" "Bounce", "Snuffle", "Nibble", "Cuddle", "Velvetpaw", "Carrot"] +            replacements = ["Cotton", "Fluff", "Floof", "Bounce", "Snuffle", "Nibble", "Cuddle", "Velvetpaw", "Carrot"]              word_to_replace = random.choice(spaces_in_name)              substitute = random.choice(replacements)              bunnified_name = username.replace(word_to_replace, substitute) diff --git a/bot/exts/holidays/easter/earth_photos.py b/bot/exts/holidays/easter/earth_photos.py index e60e2626..66f1b07b 100644 --- a/bot/exts/holidays/easter/earth_photos.py +++ b/bot/exts/holidays/easter/earth_photos.py @@ -12,7 +12,7 @@ API_URL = "https://api.unsplash.com/photos/random"  class EarthPhotos(commands.Cog): -    """This cog contains the command for earth photos.""" +    """The earth photos cog."""      def __init__(self, bot: Bot):          self.bot = bot @@ -23,7 +23,7 @@ class EarthPhotos(commands.Cog):          async with ctx.typing():              async with self.bot.http_session.get(                      API_URL, -                    params={"query": "planet_earth", "client_id": Tokens.unsplash_access_key} +                    params={"query": "planet_earth", "client_id": Tokens.unsplash.get_secret_value()}              ) as r:                  jsondata = await r.json()                  linksdata = jsondata.get("urls") @@ -37,7 +37,7 @@ class EarthPhotos(commands.Cog):                  rf = "?utm_source=Sir%20Lancebot&utm_medium=referral"              async with self.bot.http_session.get(                  downloadlinksdata.get("download_location"), -                    params={"client_id": Tokens.unsplash_access_key} +                    params={"client_id": Tokens.unsplash.get_secret_value()}              ) as _:                  pass @@ -59,7 +59,7 @@ class EarthPhotos(commands.Cog):  async def setup(bot: Bot) -> None:      """Load the Earth Photos cog.""" -    if not Tokens.unsplash_access_key: +    if not Tokens.unsplash:          log.warning("No Unsplash access key found. Cog not loading.")          return      await bot.add_cog(EarthPhotos(bot)) diff --git a/bot/exts/holidays/easter/easter_riddle.py b/bot/exts/holidays/easter/easter_riddle.py index c5d7b164..6c29dd17 100644 --- a/bot/exts/holidays/easter/easter_riddle.py +++ b/bot/exts/holidays/easter/easter_riddle.py @@ -18,7 +18,7 @@ TIMELIMIT = 10  class EasterRiddle(commands.Cog): -    """This cog contains the command for the Easter quiz!""" +    """The Easter quiz cog."""      def __init__(self, bot: Bot):          self.bot = bot diff --git a/bot/exts/holidays/easter/egg_decorating.py b/bot/exts/holidays/easter/egg_decorating.py index a9334820..1327f4d0 100644 --- a/bot/exts/holidays/easter/egg_decorating.py +++ b/bot/exts/holidays/easter/egg_decorating.py @@ -4,7 +4,6 @@ import random  from contextlib import suppress  from io import BytesIO  from pathlib import Path -from typing import Optional, Union  import discord  from PIL import Image @@ -33,7 +32,7 @@ class EggDecorating(commands.Cog):      """Decorate some easter eggs!"""      @staticmethod -    def replace_invalid(colour: str) -> Optional[int]: +    def replace_invalid(colour: str) -> int | None:          """Attempts to match with HTML or XKCD colour names, returning the int value."""          with suppress(KeyError):              return int(HTML_COLOURS[colour], 16) @@ -43,8 +42,8 @@ class EggDecorating(commands.Cog):      @commands.command(aliases=("decorateegg",))      async def eggdecorate( -        self, ctx: commands.Context, *colours: Union[discord.Colour, str] -    ) -> Optional[Image.Image]: +        self, ctx: commands.Context, *colours: discord.Colour | str +    ) -> Image.Image | None:          """          Picks a random egg design and decorates it using the given colours. @@ -53,7 +52,7 @@ class EggDecorating(commands.Cog):          """          if len(colours) < 2:              await ctx.send("You must include at least 2 colours!") -            return +            return None          invalid = []          colours = list(colours) @@ -68,10 +67,10 @@ class EggDecorating(commands.Cog):          if len(invalid) > 1:              await ctx.send(f"Sorry, I don't know these colours: {' '.join(invalid)}") -            return -        elif len(invalid) == 1: +            return None +        if len(invalid) == 1:              await ctx.send(f"Sorry, I don't know the colour {invalid[0]}!") -            return +            return None          async with ctx.typing():              # Expand list to 8 colours diff --git a/bot/exts/holidays/easter/egghead_quiz.py b/bot/exts/holidays/easter/egghead_quiz.py index 2e4d1931..046f9fac 100644 --- a/bot/exts/holidays/easter/egghead_quiz.py +++ b/bot/exts/holidays/easter/egghead_quiz.py @@ -3,7 +3,6 @@ import logging  import random  from json import loads  from pathlib import Path -from typing import Union  import discord  from discord.ext import commands @@ -29,7 +28,7 @@ TIMELIMIT = 30  class EggheadQuiz(commands.Cog): -    """This cog contains the command for the Easter quiz!""" +    """The Egghead quiz cog."""      def __init__(self):          self.quiz_messages = {} @@ -117,9 +116,10 @@ class EggheadQuiz(commands.Cog):          )          await ctx.send(content, embed=a_embed) +        return None      @staticmethod -    async def already_reacted(new_reaction: discord.Reaction, user: Union[discord.Member, discord.User]) -> bool: +    async def already_reacted(new_reaction: discord.Reaction, user: discord.Member | discord.User) -> bool:          """Returns whether a given user has reacted more than once to a given message."""          message = new_reaction.message          for reaction in message.reactions: @@ -131,16 +131,17 @@ class EggheadQuiz(commands.Cog):          return False      @commands.Cog.listener() -    async def on_reaction_add(self, reaction: discord.Reaction, user: Union[discord.Member, discord.User]) -> None: +    async def on_reaction_add(self, reaction: discord.Reaction, user: discord.Member | discord.User) -> None:          """Listener to listen specifically for reactions of quiz messages."""          if user.bot: -            return +            return None          if reaction.message.id not in self.quiz_messages: -            return +            return None          if str(reaction.emoji) not in self.quiz_messages[reaction.message.id]:              return await reaction.message.remove_reaction(reaction, user)          if await self.already_reacted(reaction, user):              return await reaction.message.remove_reaction(reaction, user) +        return None  async def setup(bot: Bot) -> None: diff --git a/bot/exts/holidays/halloween/candy_collection.py b/bot/exts/holidays/halloween/candy_collection.py index 683114f9..ca68888b 100644 --- a/bot/exts/holidays/halloween/candy_collection.py +++ b/bot/exts/holidays/halloween/candy_collection.py @@ -1,6 +1,5 @@  import logging  import random -from typing import Union  import discord  from async_rediscache import RedisCache @@ -18,17 +17,17 @@ ADD_CANDY_EXISTING_REACTION_CHANCE = 10  # 10%  ADD_SKULL_REACTION_CHANCE = 50  # 2%  ADD_SKULL_EXISTING_REACTION_CHANCE = 20  # 5% -EMOJIS = dict( -    CANDY="\N{CANDY}", -    SKULL="\N{SKULL}", -    MEDALS=( +EMOJIS = { +    "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}",      ), -) +}  class CandyCollection(commands.Cog): @@ -69,7 +68,7 @@ class CandyCollection(commands.Cog):      @in_month(Month.OCTOBER)      @commands.Cog.listener() -    async def on_reaction_add(self, reaction: discord.Reaction, user: Union[discord.User, discord.Member]) -> None: +    async def on_reaction_add(self, reaction: discord.Reaction, user: 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 @@ -140,7 +139,7 @@ class CandyCollection(commands.Cog):      @staticmethod      async def send_spook_msg( -        author: discord.Member, channel: discord.TextChannel, candies: Union[str, int] +        author: discord.Member, channel: discord.TextChannel, candies: str | int      ) -> None:          """Send a spooky message."""          e = discord.Embed(colour=author.colour) diff --git a/bot/exts/holidays/halloween/8ball.py b/bot/exts/holidays/halloween/eight_ball.py index 21b55a01..21b55a01 100644 --- a/bot/exts/holidays/halloween/8ball.py +++ b/bot/exts/holidays/halloween/eight_ball.py diff --git a/bot/exts/holidays/halloween/monstersurvey.py b/bot/exts/holidays/halloween/monstersurvey.py index 517f1bcb..d129f3cc 100644 --- a/bot/exts/holidays/halloween/monstersurvey.py +++ b/bot/exts/holidays/halloween/monstersurvey.py @@ -9,8 +9,8 @@ from discord.ext.commands import Bot, Cog, Context  log = logging.getLogger(__name__)  EMOJIS = { -    "SUCCESS": u"\u2705", -    "ERROR": u"\u274C" +    "SUCCESS": "\u2705", +    "ERROR": "\u274C"  } diff --git a/bot/exts/holidays/halloween/scarymovie.py b/bot/exts/holidays/halloween/scarymovie.py index 9f1a95fd..fbab3dd2 100644 --- a/bot/exts/holidays/halloween/scarymovie.py +++ b/bot/exts/holidays/halloween/scarymovie.py @@ -36,7 +36,7 @@ class ScaryMovie(commands.Cog):          """Selects a random movie and returns a JSON of movie details from TMDb."""          url = "https://api.themoviedb.org/3/discover/movie"          params = { -            "api_key": Tokens.tmdb, +            "api_key": Tokens.tmdb.get_secret_value(),              "with_genres": "27",              "vote_count.gte": "5",              "include_adult": "false" @@ -65,7 +65,7 @@ class ScaryMovie(commands.Cog):          # 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"} +            params={"api_key": Tokens.tmdb.get_secret_value(), "append_to_response": "credits"}          ) as selection:              return await selection.json() @@ -135,4 +135,7 @@ class ScaryMovie(commands.Cog):  async def setup(bot: Bot) -> None:      """Load the Scary Movie Cog.""" +    if not Tokens.tmdb: +        log.warning("No TMDB Token. Not loading ScaryMovie Cog.") +        return      await bot.add_cog(ScaryMovie(bot)) diff --git a/bot/exts/holidays/halloween/spookygif.py b/bot/exts/holidays/halloween/spookygif.py index 750e86ca..b3f9d703 100644 --- a/bot/exts/holidays/halloween/spookygif.py +++ b/bot/exts/holidays/halloween/spookygif.py @@ -21,7 +21,7 @@ class SpookyGif(commands.Cog):      async def spookygif(self, ctx: commands.Context) -> None:          """Fetches a random gif from the GIPHY API and responds with it."""          async with ctx.typing(): -            params = {"api_key": Tokens.giphy, "tag": "halloween", "rating": "g"} +            params = {"api_key": Tokens.giphy.get_secret_value(), "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() @@ -35,4 +35,7 @@ class SpookyGif(commands.Cog):  async def setup(bot: Bot) -> None:      """Spooky GIF Cog load.""" +    if not Tokens.giphy: +        log.warning("No Giphy token. Not loading SpookyGif cog.") +        return      await bot.add_cog(SpookyGif(bot)) diff --git a/bot/exts/holidays/halloween/spookynamerate.py b/bot/exts/holidays/halloween/spookynamerate.py index a76e5e12..78e8be8e 100644 --- a/bot/exts/holidays/halloween/spookynamerate.py +++ b/bot/exts/holidays/halloween/spookynamerate.py @@ -2,11 +2,10 @@ import asyncio  import json  import random  from collections import defaultdict -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta  from logging import getLogger  from os import getenv  from pathlib import Path -from typing import Optional  from async_rediscache import RedisCache  from discord import Embed, Reaction, TextChannel, User @@ -146,7 +145,7 @@ class SpookyNameRate(Cog):                  )                  return -            elif data["name"] == name: +            if data["name"] == name:                  await ctx.send("TOO LATE. Someone has already added this name.")                  return @@ -261,12 +260,8 @@ class SpookyNameRate(Cog):              winners = []              for i, winner in enumerate(winner_messages):                  winners.append(winner) -                if len(winner_messages) > i + 1: -                    if winner_messages[i + 1][1]["score"] != winner[1]["score"]: -                        break -                elif len(winner_messages) == (i + 1) + 1:  # The next element is the last element -                    if winner_messages[i + 1][1]["score"] != winner[1]["score"]: -                        break +                if len(winner_messages) > i + 1 and winner_messages[i + 1][1]["score"] != winner[1]["score"]: +                    break              # one iteration is complete              await channel.send("Today's Spooky Name Rate Game ends now, and the winner(s) is(are)...") @@ -313,7 +308,7 @@ class SpookyNameRate(Cog):          if SpookyNameRate.debug:              return -        now = datetime.utcnow() +        now = datetime.now(tz=UTC)          if now.hour < 12:              twelve_pm = now.replace(hour=12, minute=0, second=0, microsecond=0)              time_left = twelve_pm - now @@ -353,7 +348,7 @@ class SpookyNameRate(Cog):          return embed -    async def get_channel(self) -> Optional[TextChannel]: +    async def get_channel(self) -> TextChannel | None:          """Gets the sir-lancebot-channel after waiting until ready."""          channel = self.bot.get_channel(              Channels.sir_lancebot_playground @@ -369,7 +364,7 @@ class SpookyNameRate(Cog):              return True          if not Client.month_override: -            return datetime.utcnow().month == Month.OCTOBER +            return datetime.now(tz=UTC).month == Month.OCTOBER          return Client.month_override == Month.OCTOBER      def cog_check(self, ctx: Context) -> bool: diff --git a/bot/exts/holidays/hanukkah/hanukkah_embed.py b/bot/exts/holidays/hanukkah/hanukkah_embed.py index 1ebc21e8..fd8edca1 100644 --- a/bot/exts/holidays/hanukkah/hanukkah_embed.py +++ b/bot/exts/holidays/hanukkah/hanukkah_embed.py @@ -1,5 +1,5 @@ -import datetime  import logging +from datetime import UTC, date, datetime  from discord import Embed  from discord.ext import commands @@ -21,18 +21,18 @@ class HanukkahEmbed(commands.Cog):      def __init__(self, bot: Bot):          self.bot = bot -        self.hanukkah_dates: list[datetime.date] = [] +        self.hanukkah_dates: list[date] = [] -    def _parse_time_to_datetime(self, date: list[str]) -> datetime.datetime: +    def _parse_time_to_datetime(self, date: list[str]) -> datetime:          """Format the times provided by the api to datetime forms."""          try: -            return datetime.datetime.strptime(date, "%Y-%m-%dT%H:%M:%S%z") +            return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S%z")          except ValueError:              # there is a possibility of an event not having a time, just a day              # to catch this, we try again without time information -            return datetime.datetime.strptime(date, "%Y-%m-%d") +            return datetime.strptime(date, "%Y-%m-%d").replace(tzinfo=UTC) -    async def fetch_hanukkah_dates(self) -> list[datetime.date]: +    async def fetch_hanukkah_dates(self) -> list[date]:          """Gets the dates for hanukkah festival."""          # clear the datetime objects to prevent a memory link          self.hanukkah_dates = [] @@ -52,11 +52,11 @@ class HanukkahEmbed(commands.Cog):          hanukkah_dates = await self.fetch_hanukkah_dates()          start_day = hanukkah_dates[0]          end_day = hanukkah_dates[-1] -        today = datetime.date.today() +        today = datetime.now(tz=UTC).date()          embed = Embed(title="Hanukkah", colour=Colours.blue)          if start_day <= today <= end_day:              if start_day == today: -                now = datetime.datetime.utcnow() +                now = datetime.now(tz=UTC)                  hours = now.hour + 4  # using only hours                  hanukkah_start_hour = 18                  if hours < hanukkah_start_hour: @@ -66,7 +66,8 @@ class HanukkahEmbed(commands.Cog):                      )                      await ctx.send(embed=embed)                      return -                elif hours > hanukkah_start_hour: + +                if hours > hanukkah_start_hour:                      embed.description = (                          "It is the starting day of Hanukkah! "                          f"Its been {hours - hanukkah_start_hour} hours hanukkah started!" diff --git a/bot/exts/holidays/holidayreact.py b/bot/exts/holidays/holidayreact.py index ee20b792..c3686fab 100644 --- a/bot/exts/holidays/holidayreact.py +++ b/bot/exts/holidays/holidayreact.py @@ -72,9 +72,9 @@ HOLIDAYS_TO_REACT = [      Valentines, Easter, EarthDay, Pride, Halloween, Hanukkah, Christmas  ]  # Type (or order) doesn't matter here - set is for de-duplication -MONTHS_TO_REACT = set( +MONTHS_TO_REACT = {      month for holiday in HOLIDAYS_TO_REACT for month in holiday.months -) +}  class HolidayReact(Cog): diff --git a/bot/exts/holidays/pride/pride_anthem.py b/bot/exts/holidays/pride/pride_anthem.py index 6b78cba1..c719e388 100644 --- a/bot/exts/holidays/pride/pride_anthem.py +++ b/bot/exts/holidays/pride/pride_anthem.py @@ -2,7 +2,6 @@ import json  import logging  import random  from pathlib import Path -from typing import Optional  from discord.ext import commands @@ -16,7 +15,7 @@ VIDEOS = json.loads(Path("bot/resources/holidays/pride/anthems.json").read_text(  class PrideAnthem(commands.Cog):      """Embed a random youtube video for a gay anthem!""" -    def get_video(self, genre: Optional[str] = None) -> dict: +    def get_video(self, genre: str | None = None) -> dict:          """          Picks a random anthem from the list. @@ -25,12 +24,12 @@ class PrideAnthem(commands.Cog):          """          if not genre:              return random.choice(VIDEOS) -        else: -            songs = [song for song in VIDEOS if genre.casefold() in song["genre"]] -            try: -                return random.choice(songs) -            except IndexError: -                log.info("No videos for that genre.") + +        songs = [song for song in VIDEOS if genre.casefold() in song["genre"]] +        try: +            return random.choice(songs) +        except IndexError: +            log.info("No videos for that genre.")      @commands.command(name="prideanthem", aliases=("anthem", "pridesong"))      async def prideanthem(self, ctx: commands.Context, genre: str = None) -> None: diff --git a/bot/exts/holidays/pride/pride_facts.py b/bot/exts/holidays/pride/pride_facts.py index 36a9415e..f3ce50a9 100644 --- a/bot/exts/holidays/pride/pride_facts.py +++ b/bot/exts/holidays/pride/pride_facts.py @@ -1,9 +1,8 @@  import json  import logging  import random -from datetime import datetime +from datetime import UTC, datetime  from pathlib import Path -from typing import Union  import dateutil.parser  import discord @@ -29,11 +28,11 @@ class PrideFacts(commands.Cog):      async def send_pride_fact_daily(self) -> None:          """Background task to post the daily pride fact every day."""          channel = self.bot.get_channel(Channels.sir_lancebot_playground) -        await self.send_select_fact(channel, datetime.utcnow()) +        await self.send_select_fact(channel, datetime.now(tz=UTC))      async def send_random_fact(self, ctx: commands.Context) -> None:          """Provides a fact from any previous day, or today.""" -        now = datetime.utcnow() +        now = datetime.now(tz=UTC)          previous_years_facts = (y for x, y in FACTS.items() if int(x) < now.year)          current_year_facts = FACTS.get(str(now.year), [])[:now.day]          previous_facts = current_year_facts + [x for y in previous_years_facts for x in y] @@ -42,9 +41,9 @@ class PrideFacts(commands.Cog):          except IndexError:              await ctx.send("No facts available") -    async def send_select_fact(self, target: discord.abc.Messageable, _date: Union[str, datetime]) -> None: +    async def send_select_fact(self, target: discord.abc.Messageable, _date: str | datetime) -> None:          """Provides the fact for the specified day, if the day is today, or is in the past.""" -        now = datetime.utcnow() +        now = datetime.now(tz=UTC)          if isinstance(_date, str):              try:                  date = dateutil.parser.parse(_date, dayfirst=False, yearfirst=False, fuzzy=True) @@ -76,7 +75,7 @@ class PrideFacts(commands.Cog):          will be provided.          """          if not option: -            await self.send_select_fact(ctx, datetime.utcnow()) +            await self.send_select_fact(ctx, datetime.now(tz=UTC))          elif option.lower().startswith("rand"):              await self.send_random_fact(ctx)          else: diff --git a/bot/exts/holidays/pride/pride_leader.py b/bot/exts/holidays/pride/pride_leader.py index 120e9e16..b4a98892 100644 --- a/bot/exts/holidays/pride/pride_leader.py +++ b/bot/exts/holidays/pride/pride_leader.py @@ -2,7 +2,6 @@ import json  import logging  import random  from pathlib import Path -from typing import Optional  import discord  from discord.ext import commands @@ -90,7 +89,7 @@ class PrideLeader(commands.Cog):          return embed      @commands.command(aliases=("pl", "prideleader")) -    async def pride_leader(self, ctx: commands.Context, *, pride_leader_name: Optional[str]) -> None: +    async def pride_leader(self, ctx: commands.Context, *, pride_leader_name: str | None) -> None:          """          Information about a Pride Leader. diff --git a/bot/exts/holidays/valentines/be_my_valentine.py b/bot/exts/holidays/valentines/be_my_valentine.py index 5ffd14e6..c2dd8bb6 100644 --- a/bot/exts/holidays/valentines/be_my_valentine.py +++ b/bot/exts/holidays/valentines/be_my_valentine.py @@ -7,7 +7,7 @@ import discord  from discord.ext import commands  from bot.bot import Bot -from bot.constants import Channels, Colours, Lovefest, Month, PYTHON_PREFIX +from bot.constants import Channels, Colours, Month, PYTHON_PREFIX, Roles  from bot.utils.decorators import in_month  from bot.utils.exceptions import MovedCommandError @@ -60,7 +60,7 @@ class BeMyValentine(commands.Cog):              # This command should only be used in the server              raise commands.UserInputError("You are supposed to use this command in the server.") -        if Lovefest.role_id not in [role.id for role in user.roles]: +        if Roles.lovefest not in [role.id for role in user.roles]:              raise commands.UserInputError(                  f"You cannot send a valentine to {user} as they do not have the lovefest role!"              ) @@ -95,7 +95,7 @@ class BeMyValentine(commands.Cog):          example : .bemyvalentine secret Iceman#6508 Hey I love you, wanna hang around ? (sends the custom message to          Iceman in DM making you anonymous)          """ -        if Lovefest.role_id not in [role.id for role in user.roles]: +        if Roles.lovefest not in [role.id for role in user.roles]:              await ctx.message.delete()              raise commands.UserInputError(                  f"You cannot send a valentine to {user} as they do not have the lovefest role!" @@ -126,15 +126,14 @@ class BeMyValentine(commands.Cog):          if valentine_type is None:              return self.random_valentine() -        elif valentine_type.lower() in ["p", "poem"]: +        if valentine_type.lower() in ["p", "poem"]:              return self.valentine_poem(), "A poem dedicated to" -        elif valentine_type.lower() in ["c", "compliment"]: +        if valentine_type.lower() in ["c", "compliment"]:              return self.valentine_compliment(), "A compliment for" -        else: -            # in this case, the user decides to type his own valentine. -            return valentine_type, "A message for" +        # in this case, the user decides to type his own valentine. +        return valentine_type, "A message for"      @staticmethod      def random_emoji() -> tuple[str, str]: @@ -148,10 +147,7 @@ class BeMyValentine(commands.Cog):          valentine_poem = random.choice(self.valentines["valentine_poems"])          valentine_compliment = random.choice(self.valentines["valentine_compliments"])          random_valentine = random.choice([valentine_compliment, valentine_poem]) -        if random_valentine == valentine_poem: -            title = "A poem dedicated to" -        else: -            title = "A compliment for " +        title = "A poem dedicated to" if random_valentine == valentine_poem else "A compliment for "          return random_valentine, title      def valentine_poem(self) -> str: diff --git a/bot/exts/holidays/valentines/lovecalculator.py b/bot/exts/holidays/valentines/lovecalculator.py index c212e833..9b363c5e 100644 --- a/bot/exts/holidays/valentines/lovecalculator.py +++ b/bot/exts/holidays/valentines/lovecalculator.py @@ -3,8 +3,8 @@ import hashlib  import json  import logging  import random +from collections.abc import Coroutine  from pathlib import Path -from typing import Coroutine, Optional  import discord  from discord import Member @@ -12,7 +12,7 @@ from discord.ext import commands  from discord.ext.commands import BadArgument, Cog, clean_content  from bot.bot import Bot -from bot.constants import Channels, Lovefest, Month, PYTHON_PREFIX +from bot.constants import Channels, Month, PYTHON_PREFIX, Roles  from bot.utils.decorators import in_month  log = logging.getLogger(__name__) @@ -27,7 +27,7 @@ class LoveCalculator(Cog):      @in_month(Month.FEBRUARY)      @commands.command(aliases=("love_calculator", "love_calc"))      @commands.cooldown(rate=1, per=5, type=commands.BucketType.user) -    async def love(self, ctx: commands.Context, who: Member, whom: Optional[Member] = None) -> None: +    async def love(self, ctx: commands.Context, who: Member, whom: Member | None = None) -> None:          """          Tells you how much the two love each other. @@ -45,8 +45,8 @@ class LoveCalculator(Cog):            Running .love @chrisjl#2655 @joe#6000 will yield the same result as before.          """          if ( -            Lovefest.role_id not in [role.id for role in who.roles] -            or (whom is not None and Lovefest.role_id not in [role.id for role in whom.roles]) +            Roles.lovefest not in [role.id for role in who.roles] +            or (whom is not None and Roles.lovefest not in [role.id for role in whom.roles])          ):              raise BadArgument(                  "This command can only be ran against members with the lovefest role! " diff --git a/bot/exts/holidays/valentines/myvalenstate.py b/bot/exts/holidays/valentines/myvalenstate.py index 8d8772d4..fcef24bc 100644 --- a/bot/exts/holidays/valentines/myvalenstate.py +++ b/bot/exts/holidays/valentines/myvalenstate.py @@ -47,7 +47,7 @@ class MyValenstate(commands.Cog):          else:              author = name.lower().replace(" ", "") -        for state in STATES.keys(): +        for state in STATES:              lower_state = state.lower().replace(" ", "")              eq_chars[state] = self.levenshtein(author, lower_state) @@ -64,8 +64,7 @@ class MyValenstate(commands.Cog):              embed_text = f"You have another match, this being {matches[0]}."          else:              embed_title = "You have a true match!" -            embed_text = "This state is your true Valenstate! There are no states that would suit" \ -                         " you better" +            embed_text = "This state is your true Valenstate! There are no states that would suit you better"          embed = discord.Embed(              title=f"Your Valenstate is {valenstate} \u2764", diff --git a/bot/exts/holidays/valentines/valentine_zodiac.py b/bot/exts/holidays/valentines/valentine_zodiac.py index 0a28a5c5..f0c8978d 100644 --- a/bot/exts/holidays/valentines/valentine_zodiac.py +++ b/bot/exts/holidays/valentines/valentine_zodiac.py @@ -2,9 +2,8 @@ import calendar  import json  import logging  import random -from datetime import datetime +from datetime import UTC, datetime  from pathlib import Path -from typing import Union  import discord  from discord.ext import commands @@ -78,6 +77,7 @@ class ValentineZodiac(commands.Cog):              if zodiac_data["start_at"].date() <= query_date.date() <= zodiac_data["end_at"].date():                  log.trace("Zodiac name sent.")                  return zodiac_name +        return None      @commands.group(name="zodiac", invoke_without_command=True)      async def zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None: @@ -87,7 +87,7 @@ class ValentineZodiac(commands.Cog):          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: +    async def date_and_month(self, ctx: commands.Context, date: int, month: int | str) -> None:          """Provides information about zodiac sign by taking month and date as input."""          if isinstance(month, str):              month = month.capitalize() @@ -103,7 +103,7 @@ class ValentineZodiac(commands.Cog):              final_embed = self.zodiac_build_embed(zodiac)          else:              try: -                zodiac_sign_based_on_date = self.zodiac_date_verifier(datetime(2020, month, date)) +                zodiac_sign_based_on_date = self.zodiac_date_verifier(datetime(2020, month, date, tzinfo=UTC))                  log.trace("zodiac sign based on month and date received.")              except ValueError as e:                  final_embed = discord.Embed() diff --git a/bot/exts/utilities/bookmark.py b/bot/exts/utilities/bookmark.py index 0b7ce429..a2d1d1d4 100644 --- a/bot/exts/utilities/bookmark.py +++ b/bot/exts/utilities/bookmark.py @@ -1,6 +1,5 @@  import logging  import random -from typing import Optional  import discord  from discord.ext import commands @@ -234,15 +233,15 @@ class Bookmark(commands.Cog):          This command allows deleting any message sent by Sir-Lancebot in the user's DM channel with the bot.          The command invocation must be a reply to the message that is to be deleted.          """ -        target_message: Optional[discord.Message] = getattr(ctx.message.reference, "resolved", None) +        target_message: discord.Message | None = getattr(ctx.message.reference, "resolved", None)          if target_message is None:              raise commands.UserInputError("You must reply to the message from Sir-Lancebot you wish to delete.")          if not isinstance(ctx.channel, discord.DMChannel):              raise commands.UserInputError("You can only run this command your own DMs!") -        elif target_message.channel != ctx.channel: +        if target_message.channel != ctx.channel:              raise commands.UserInputError("You can only delete messages in your own DMs!") -        elif target_message.author != self.bot.user: +        if target_message.author != self.bot.user:              raise commands.UserInputError("You can only delete messages sent by Sir Lancebot!")          await target_message.delete() diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py index 46bc0fae..2f9ac73e 100644 --- a/bot/exts/utilities/challenges.py +++ b/bot/exts/utilities/challenges.py @@ -1,7 +1,6 @@  import logging  from asyncio import to_thread  from random import choice -from typing import Union  from bs4 import BeautifulSoup  from discord import Embed, Interaction, SelectOption, ui @@ -58,7 +57,7 @@ class InformationDropdown(ui.Select):              SelectOption(                  label="Other Information",                  description="See how other people performed on this kata and more!", -                emoji="ā¹" +                emoji="š®"              )          ] @@ -101,7 +100,7 @@ class Challenges(commands.Cog):      def __init__(self, bot: Bot):          self.bot = bot -    async def kata_id(self, search_link: str, params: dict) -> Union[str, Embed]: +    async def kata_id(self, search_link: str, params: dict) -> str | Embed:          """          Uses bs4 to get the HTML code for the page of katas, where the page is the link of the formatted `search_link`. @@ -123,14 +122,13 @@ class Challenges(commands.Cog):              if not first_kata_div:                  raise commands.BadArgument("No katas could be found with the filters provided.") -            else: -                first_kata_div = choice(first_kata_div)              # There are numerous divs before arriving at the id of the kata, which can be used for the link. +            first_kata_div = choice(first_kata_div)              first_kata_id = first_kata_div.a["href"].split("/")[-1]              return first_kata_id -    async def kata_information(self, kata_id: str) -> Union[dict, Embed]: +    async def kata_information(self, kata_id: str) -> dict | Embed:          """          Returns the information about the Kata. diff --git a/bot/exts/utilities/cheatsheet.py b/bot/exts/utilities/cheatsheet.py index 3141a050..98ce3f57 100644 --- a/bot/exts/utilities/cheatsheet.py +++ b/bot/exts/utilities/cheatsheet.py @@ -1,6 +1,5 @@  import random  import re -from typing import Union  from urllib.parse import quote_plus  from discord import Embed @@ -52,7 +51,7 @@ class CheatSheet(commands.Cog):          )          return embed -    def result_fmt(self, url: str, body_text: str) -> tuple[bool, Union[str, Embed]]: +    def result_fmt(self, url: str, body_text: str) -> tuple[bool, str | Embed]:          """Format Result."""          if body_text.startswith("#  404 NOT FOUND"):              embed = self.fmt_error_embed() @@ -80,7 +79,7 @@ class CheatSheet(commands.Cog):          aliases=("cht.sh", "cheatsheet", "cheat-sheet", "cht"),      )      @commands.cooldown(1, 10, BucketType.user) -    @whitelist_override(categories=[Categories.help_in_use]) +    @whitelist_override(categories=[Categories.python_help_system])      async def cheat_sheet(self, ctx: Context, *search_terms: str) -> None:          """          Search cheat.sh. diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 20f97e4b..b8f1d62d 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -4,7 +4,6 @@ import pathlib  import random  import string  from io import BytesIO -from typing import Optional  import discord  import rapidfuzz @@ -25,7 +24,7 @@ class Colour(commands.Cog):          self.bot = bot          with open(pathlib.Path("bot/resources/utilities/ryanzec_colours.json")) as f:              self.colour_mapping = json.load(f) -            del self.colour_mapping['_']  # Delete source credit entry +            del self.colour_mapping["_"]  # Delete source credit entry      async def send_colour_response(self, ctx: commands.Context, rgb: tuple[int, int, int]) -> None:          """Create and send embed from user given colour information.""" @@ -84,7 +83,7 @@ class Colour(commands.Cog):          roles=constants.STAFF_ROLES,          categories=[constants.Categories.development, constants.Categories.media]      ) -    async def colour(self, ctx: commands.Context, *, colour_input: Optional[str] = None) -> None: +    async def colour(self, ctx: commands.Context, *, colour_input: str | None = None) -> None:          """          Create an embed that displays colour information. @@ -209,7 +208,7 @@ class Colour(commands.Cog):      def _rgb_to_hsl(rgb: tuple[int, int, int]) -> tuple[int, int, int]:          """Convert RGB values to HSL values."""          rgb_list = [val / 255.0 for val in rgb] -        h, l, s = colorsys.rgb_to_hls(*rgb_list) +        h, l, s = colorsys.rgb_to_hls(*rgb_list)  # noqa: E741          hsl = (round(h * 360), round(s * 100), round(l * 100))          return hsl @@ -233,7 +232,7 @@ class Colour(commands.Cog):          hex_code = f"#{hex_}".upper()          return hex_code -    def _rgb_to_name(self, rgb: tuple[int, int, int]) -> Optional[str]: +    def _rgb_to_name(self, rgb: tuple[int, int, int]) -> str | None:          """Convert RGB values to a fuzzy matched name."""          input_hex_colour = self._rgb_to_hex(rgb)          try: @@ -247,7 +246,7 @@ class Colour(commands.Cog):              colour_name = None          return colour_name -    def match_colour_name(self, ctx: commands.Context, input_colour_name: str) -> Optional[str]: +    def match_colour_name(self, ctx: commands.Context, input_colour_name: str) -> str | None:          """Convert a colour name to HEX code."""          try:              match, certainty, _ = rapidfuzz.process.extractOne( @@ -256,7 +255,7 @@ class Colour(commands.Cog):                  score_cutoff=80              )          except (ValueError, TypeError): -            return +            return None          return f"#{self.colour_mapping[match]}" diff --git a/bot/exts/utilities/conversationstarters.py b/bot/exts/utilities/conversationstarters.py index 410ea884..a019c789 100644 --- a/bot/exts/utilities/conversationstarters.py +++ b/bot/exts/utilities/conversationstarters.py @@ -2,7 +2,6 @@ import asyncio  from contextlib import suppress  from functools import partial  from pathlib import Path -from typing import Union  import discord  import yaml @@ -16,11 +15,11 @@ from bot.utils.randomization import RandomCycle  SUGGESTION_FORM = "https://forms.gle/zw6kkJqv8U43Nfjg9"  with Path("bot/resources/utilities/starter.yaml").open("r", encoding="utf8") as f: -    STARTERS = yaml.load(f, Loader=yaml.FullLoader) +    STARTERS = yaml.safe_load(f)  with Path("bot/resources/utilities/py_topics.yaml").open("r", encoding="utf8") as f:      # First ID is #python-general and the rest are top to bottom categories of Topical Chat/Help. -    PY_TOPICS = yaml.load(f, Loader=yaml.FullLoader) +    PY_TOPICS = yaml.safe_load(f)      # Removing `None` from lists of topics, if not a list, it is changed to an empty one.      PY_TOPICS = {k: [i for i in v if i] if isinstance(v, list) else [] for k, v in PY_TOPICS.items()} @@ -67,7 +66,7 @@ class ConvoStarters(commands.Cog):      @staticmethod      def _predicate( -        command_invoker: Union[discord.User, discord.Member], +        command_invoker: discord.User | discord.Member,          message: discord.Message,          reaction: discord.Reaction,          user: discord.User @@ -84,7 +83,7 @@ class ConvoStarters(commands.Cog):      async def _listen_for_refresh(          self, -        command_invoker: Union[discord.User, discord.Member], +        command_invoker: discord.User | discord.Member,          message: discord.Message      ) -> None:          await message.add_reaction("š") diff --git a/bot/exts/utilities/emoji.py b/bot/exts/utilities/emoji.py index ec40be01..ce352fe2 100644 --- a/bot/exts/utilities/emoji.py +++ b/bot/exts/utilities/emoji.py @@ -2,8 +2,7 @@ import logging  import random  import textwrap  from collections import defaultdict -from datetime import datetime -from typing import Optional +from datetime import UTC, datetime  from discord import Color, Embed, Emoji  from discord.ext import commands @@ -28,7 +27,7 @@ class Emojis(commands.Cog):          embed = Embed(              color=Colours.orange,              title="Emoji Count", -            timestamp=datetime.utcnow() +            timestamp=datetime.now(tz=UTC)          )          msg = [] @@ -71,7 +70,7 @@ class Emojis(commands.Cog):          return embed, msg      @commands.group(name="emoji", invoke_without_command=True) -    async def emoji_group(self, ctx: commands.Context, emoji: Optional[Emoji]) -> None: +    async def emoji_group(self, ctx: commands.Context, emoji: Emoji | None) -> None:          """A group of commands related to emojis."""          if emoji is not None:              await ctx.invoke(self.info_command, emoji) diff --git a/bot/exts/utilities/epoch.py b/bot/exts/utilities/epoch.py index 6f572640..d1ba98bb 100644 --- a/bot/exts/utilities/epoch.py +++ b/bot/exts/utilities/epoch.py @@ -1,4 +1,5 @@ -from typing import Optional, Union + +import contextlib  import arrow  import discord @@ -24,7 +25,7 @@ DROPDOWN_TIMEOUT = 60  class DateString(commands.Converter):      """Convert a relative or absolute date/time string to an arrow.Arrow object.""" -    async def convert(self, ctx: commands.Context, argument: str) -> Union[arrow.Arrow, Optional[tuple]]: +    async def convert(self, ctx: commands.Context, argument: str) -> arrow.Arrow | tuple | None:          """          Convert a relative or absolute date/time string to an arrow.Arrow object. @@ -88,10 +89,8 @@ class Epoch(commands.Cog):          view = TimestampMenuView(ctx, self._format_dates(date_time), epoch)          original = await ctx.send(f"`{epoch}`", view=view)          await view.wait()  # wait until expiration before removing the dropdown -        try: +        with contextlib.suppress(discord.NotFound):              await original.edit(view=None) -        except discord.NotFound:  # disregard the error message if the message is deleled -            pass      @staticmethod      def _format_dates(date: arrow.Arrow) -> list[str]: @@ -100,7 +99,7 @@ class Epoch(commands.Cog):          These are used in the description of each style in the dropdown          """ -        date = date.to('utc') +        date = date.to("utc")          formatted = [str(int(date.timestamp()))]          formatted += [date.format(format[1]) for format in list(STYLES.values())[1:7]]          formatted.append(date.humanize()) @@ -115,7 +114,7 @@ class TimestampMenuView(discord.ui.View):          self.ctx = ctx          self.epoch = epoch          self.dropdown: discord.ui.Select = self.children[0] -        for label, date_time in zip(STYLES.keys(), formatted_times): +        for label, date_time in zip(STYLES.keys(), formatted_times, strict=True):              self.dropdown.add_option(label=label, description=date_time)      @discord.ui.select(placeholder="Select the format of your timestamp") diff --git a/bot/exts/utilities/githubinfo.py b/bot/exts/utilities/githubinfo.py index a7979718..74120f2d 100644 --- a/bot/exts/utilities/githubinfo.py +++ b/bot/exts/utilities/githubinfo.py @@ -1,9 +1,8 @@  import logging  import random  import re -import typing as t  from dataclasses import dataclass -from datetime import datetime +from datetime import UTC, datetime  from urllib.parse import quote  import discord @@ -26,7 +25,7 @@ ISSUE_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/issues/{numbe  PR_ENDPOINT = "https://api.github.com/repos/{user}/{repository}/pulls/{number}"  if Tokens.github: -    REQUEST_HEADERS["Authorization"] = f"token {Tokens.github}" +    REQUEST_HEADERS["Authorization"] = f"token {Tokens.github.get_secret_value()}"  CODE_BLOCK_RE = re.compile(      r"^`([^`\n]+)`"   # Inline codeblock @@ -48,7 +47,7 @@ AUTOMATIC_REGEX = re.compile(  class FoundIssue:      """Dataclass representing an issue found by the regex.""" -    organisation: t.Optional[str] +    organisation: str | None      repository: str      number: str @@ -89,7 +88,7 @@ class GithubInfo(commands.Cog):          number: int,          repository: str,          user: str -    ) -> t.Union[IssueState, FetchError]: +    ) -> IssueState | FetchError:          """          Retrieve an issue from a GitHub repository. @@ -105,9 +104,9 @@ class GithubInfo(commands.Cog):                  log.info(f"Ratelimit reached while fetching {url}")                  return FetchError(403, "Ratelimit reached, please retry in a few minutes.")              return FetchError(403, "Cannot access issue.") -        elif r.status in (404, 410): +        if r.status in (404, 410):              return FetchError(r.status, "Issue not found.") -        elif r.status != 200: +        if r.status != 200:              return FetchError(r.status, "Error while fetching issue.")          # The initial API request is made to the issues API endpoint, which will return information @@ -141,7 +140,7 @@ class GithubInfo(commands.Cog):      @staticmethod      def format_embed( -        results: t.List[t.Union[IssueState, FetchError]] +        results: list[IssueState | FetchError]      ) -> discord.Embed:          """Take a list of IssueState or FetchError and format a Discord embed for them."""          description_list = [] @@ -261,7 +260,7 @@ class GithubInfo(commands.Cog):                  description=f"```\n{user_data['bio']}\n```\n" if user_data["bio"] else "",                  colour=discord.Colour.og_blurple(),                  url=user_data["html_url"], -                timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ") +                timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)              )              embed.set_thumbnail(url=user_data["avatar_url"])              embed.set_footer(text="Account created at") @@ -293,7 +292,7 @@ class GithubInfo(commands.Cog):          await ctx.send(embed=embed) -    @github_group.command(name='repository', aliases=('repo',)) +    @github_group.command(name="repository", aliases=("repo",))      async def github_repo_info(self, ctx: commands.Context, *repo: str) -> None:          """          Fetches a repositories' GitHub information. @@ -347,8 +346,12 @@ class GithubInfo(commands.Cog):              icon_url=repo_owner["avatar_url"]          ) -        repo_created_at = datetime.strptime(repo_data["created_at"], "%Y-%m-%dT%H:%M:%SZ").strftime("%d/%m/%Y") -        last_pushed = datetime.strptime(repo_data["pushed_at"], "%Y-%m-%dT%H:%M:%SZ").strftime("%d/%m/%Y at %H:%M") +        repo_created_at = datetime.strptime( +            repo_data["created_at"], "%Y-%m-%dT%H:%M:%SZ" +        ).replace(tzinfo=UTC).strftime("%d/%m/%Y") +        last_pushed = datetime.strptime( +            repo_data["pushed_at"], "%Y-%m-%dT%H:%M:%SZ" +        ).replace(tzinfo=UTC).strftime("%d/%m/%Y at %H:%M")          embed.set_footer(              text=( diff --git a/bot/exts/utilities/realpython.py b/bot/exts/utilities/realpython.py index 46b02866..c63fca85 100644 --- a/bot/exts/utilities/realpython.py +++ b/bot/exts/utilities/realpython.py @@ -1,6 +1,5 @@  import logging  from html import unescape -from typing import Optional  from urllib.parse import quote_plus  from discord import Embed @@ -31,8 +30,13 @@ class RealPython(commands.Cog):      @commands.command(aliases=["rp"])      @commands.cooldown(1, 10, commands.cooldowns.BucketType.user) -    async def realpython(self, ctx: commands.Context, amount: Optional[int] = 5, *, -                         user_search: Optional[str] = None) -> None: +    async def realpython( +        self, +        ctx: commands.Context, +        amount: int | None = 5, +        *, +        user_search: str | None = None +    ) -> None:          """          Send some articles from RealPython that match the search terms. diff --git a/bot/exts/utilities/reddit.py b/bot/exts/utilities/reddit.py index 028c16bc..f8e358de 100644 --- a/bot/exts/utilities/reddit.py +++ b/bot/exts/utilities/reddit.py @@ -3,8 +3,7 @@ import logging  import random  import textwrap  from collections import namedtuple -from datetime import datetime, timedelta -from typing import Union +from datetime import UTC, datetime, timedelta  from aiohttp import BasicAuth, ClientError  from discord import Colour, Embed, TextChannel @@ -36,15 +35,15 @@ class Reddit(Cog):          self.webhook = None          self.access_token = None -        self.client_auth = BasicAuth(RedditConfig.client_id, RedditConfig.secret) +        self.client_auth = BasicAuth(RedditConfig.client_id.get_secret_value(), RedditConfig.secret.get_secret_value())          self.auto_poster_loop.start()      async def cog_unload(self) -> None:          """Stop the loop task and revoke the access token when the cog is unloaded."""          self.auto_poster_loop.cancel() -        if self.access_token and self.access_token.expires_at > datetime.utcnow(): -            asyncio.create_task(self.revoke_access_token()) +        if self.access_token and self.access_token.expires_at > datetime.now(tz=UTC): +            await self.revoke_access_token()      async def cog_load(self) -> None:          """Sets the reddit webhook when the cog is loaded.""" @@ -55,7 +54,7 @@ class Reddit(Cog):          """Get the #reddit channel object from the bot's cache."""          return self.bot.get_channel(Channels.reddit) -    def build_pagination_pages(self, posts: list[dict], paginate: bool) -> Union[list[tuple], str]: +    def build_pagination_pages(self, posts: list[dict], paginate: bool) -> list[tuple] | str:          """Build embed pages required for Paginator."""          pages = []          first_page = "" @@ -138,17 +137,15 @@ class Reddit(Cog):                  expiration = int(content["expires_in"]) - 60  # Subtract 1 minute for leeway.                  self.access_token = AccessToken(                      token=content["access_token"], -                    expires_at=datetime.utcnow() + timedelta(seconds=expiration) +                    expires_at=datetime.now(tz=UTC) + timedelta(seconds=expiration)                  )                  log.debug(f"New token acquired; expires on UTC {self.access_token.expires_at}")                  return -            else: -                log.debug( -                    f"Failed to get an access token: " -                    f"status {response.status} & content type {response.content_type}; " -                    f"retrying ({i}/{self.MAX_RETRIES})" -                ) +            log.debug( +                f"Failed to get an access token: status {response.status} & content type {response.content_type}; " +                f"retrying ({i}/{self.MAX_RETRIES})" +            )              await asyncio.sleep(3) @@ -183,7 +180,7 @@ class Reddit(Cog):              raise ValueError("Invalid amount of subreddit posts requested.")          # Renew the token if necessary. -        if not self.access_token or self.access_token.expires_at < datetime.utcnow(): +        if not self.access_token or self.access_token.expires_at < datetime.now(tz=UTC):              await self.get_access_token()          url = f"{self.OAUTH_URL}/{route}" @@ -193,7 +190,7 @@ class Reddit(Cog):                  headers={**self.HEADERS, "Authorization": f"bearer {self.access_token.token}"},                  params=params              ) -            if response.status == 200 and response.content_type == 'application/json': +            if response.status == 200 and response.content_type == "application/json":                  # Got appropriate response - process and return.                  content = await response.json()                  posts = content["data"]["children"] @@ -205,11 +202,11 @@ class Reddit(Cog):              await asyncio.sleep(3)          log.debug(f"Invalid response from: {url} - status code {response.status}, mimetype {response.content_type}") -        return list()  # Failed to get appropriate response within allowed number of retries. +        return []  # Failed to get appropriate response within allowed number of retries.      async def get_top_posts(              self, subreddit: Subreddit, time: str = "all", amount: int = 5, paginate: bool = False -    ) -> Union[Embed, list[tuple]]: +    ) -> Embed | list[tuple]:          """          Get the top amount of posts for a given subreddit within a specified timeframe. @@ -248,7 +245,7 @@ class Reddit(Cog):          """Post the top 5 posts daily, and the top 5 posts weekly."""          # once d.py get support for `time` parameter in loop decorator,          # this can be removed and the loop can use the `time=datetime.time.min` parameter -        now = datetime.utcnow() +        now = datetime.now(tz=UTC)          tomorrow = now + timedelta(days=1)          midnight_tomorrow = tomorrow.replace(hour=0, minute=0, second=0) @@ -257,7 +254,7 @@ class Reddit(Cog):          if not self.webhook:              await self.bot.fetch_webhook(RedditConfig.webhook) -        if datetime.utcnow().weekday() == 0: +        if datetime.now(tz=UTC).weekday() == 0:              await self.top_weekly_posts()              # if it's a monday send the top weekly posts @@ -358,6 +355,6 @@ class Reddit(Cog):  async def setup(bot: Bot) -> None:      """Load the Reddit cog."""      if not RedditConfig.secret or not RedditConfig.client_id: -        log.error("Credentials not provided, cog not loaded.") +        log.warning("Credentials not provided, cog not loaded.")          return      await bot.add_cog(Reddit(bot)) diff --git a/bot/exts/utilities/stackoverflow.py b/bot/exts/utilities/stackoverflow.py index b248e83f..1eeff45b 100644 --- a/bot/exts/utilities/stackoverflow.py +++ b/bot/exts/utilities/stackoverflow.py @@ -42,10 +42,10 @@ class Stackoverflow(commands.Cog):              if response.status == 200:                  data = await response.json()              else: -                logger.error(f'Status code is not 200, it is {response.status}') +                logger.error(f"Status code is not 200, it is {response.status}")                  await ctx.send(embed=ERR_EMBED)                  return -        if not data['items']: +        if not data["items"]:              no_search_result = Embed(                  title=f"No search results found for {search_query}",                  color=Colours.soft_red @@ -63,7 +63,7 @@ class Stackoverflow(commands.Cog):          )          for item in top5:              embed.add_field( -                name=unescape(item['title']), +                name=unescape(item["title"]),                  value=(                      f"[{Emojis.reddit_upvote} {item['score']}    "                      f"{Emojis.stackoverflow_views} {item['view_count']}     " diff --git a/bot/exts/utilities/twemoji.py b/bot/exts/utilities/twemoji.py index 25a03d25..a936f733 100644 --- a/bot/exts/utilities/twemoji.py +++ b/bot/exts/utilities/twemoji.py @@ -1,6 +1,6 @@  import logging  import re -from typing import Literal, Optional +from typing import Literal  import discord  from discord.ext import commands @@ -57,7 +57,7 @@ class Twemoji(commands.Cog):          return embed      @staticmethod -    def emoji(codepoint: Optional[str]) -> Optional[str]: +    def emoji(codepoint: str | None) -> str | None:          """          Returns the emoji corresponding to a given `codepoint`, or `None` if no emoji was found. @@ -66,9 +66,10 @@ class Twemoji(commands.Cog):          """          if code := Twemoji.trim_code(codepoint):              return chr(int(code, 16)) +        return None      @staticmethod -    def codepoint(emoji: Optional[str]) -> Optional[str]: +    def codepoint(emoji: str | None) -> str | None:          """          Returns the codepoint, in a trimmed format, of a single emoji. @@ -82,7 +83,7 @@ class Twemoji(commands.Cog):          return hex(ord(emoji)).removeprefix("0x")      @staticmethod -    def trim_code(codepoint: Optional[str]) -> Optional[str]: +    def trim_code(codepoint: str | None) -> str | None:          """          Returns the meaningful information from the given `codepoint`. @@ -98,6 +99,7 @@ class Twemoji(commands.Cog):          """          if code := CODEPOINT_REGEX.search(codepoint or ""):              return code.group() +        return None      @staticmethod      def codepoint_from_input(raw_emoji: tuple[str, ...]) -> str: diff --git a/bot/exts/utilities/wikipedia.py b/bot/exts/utilities/wikipedia.py index d87982c9..d12cb19d 100644 --- a/bot/exts/utilities/wikipedia.py +++ b/bot/exts/utilities/wikipedia.py @@ -1,6 +1,6 @@  import logging  import re -from datetime import datetime +from datetime import UTC, datetime  from html import unescape  from discord import Color, Embed, TextChannel @@ -85,7 +85,7 @@ class WikipediaSearch(commands.Cog):                  colour=Color.og_blurple()              )              embed.set_thumbnail(url=WIKI_THUMBNAIL) -            embed.timestamp = datetime.utcnow() +            embed.timestamp = datetime.now(tz=UTC)              await LinePaginator.paginate(contents, ctx, embed, restrict_to_user=ctx.author)          else:              await ctx.send( diff --git a/bot/exts/utilities/wolfram.py b/bot/exts/utilities/wolfram.py index a2f1228a..d5669c6b 100644 --- a/bot/exts/utilities/wolfram.py +++ b/bot/exts/utilities/wolfram.py @@ -1,6 +1,6 @@  import logging +from collections.abc import Callable  from io import BytesIO -from typing import Callable, Optional  from urllib.parse import urlencode  import arrow @@ -10,12 +10,12 @@ from discord.ext import commands  from discord.ext.commands import BucketType, Cog, Context, check, group  from bot.bot import Bot -from bot.constants import Colours, STAFF_ROLES, Wolfram +from bot.constants import Colours, STAFF_ROLES, Wolfram as WolframConfig  from bot.utils.pagination import ImagePaginator  log = logging.getLogger(__name__) -APPID = Wolfram.key +APPID = WolframConfig.key.get_secret_value()  DEFAULT_OUTPUT_FORMAT = "JSON"  QUERY = "http://api.wolframalpha.com/v2/{request}"  WOLF_IMAGE = "https://www.symbols.com/gi.php?type=1&id=2886&i=1" @@ -23,10 +23,10 @@ WOLF_IMAGE = "https://www.symbols.com/gi.php?type=1&id=2886&i=1"  MAX_PODS = 20  # Allows for 10 wolfram calls pr user pr day -usercd = commands.CooldownMapping.from_cooldown(Wolfram.user_limit_day, 60 * 60 * 24, BucketType.user) +usercd = commands.CooldownMapping.from_cooldown(WolframConfig.user_limit_day, 60 * 60 * 24, BucketType.user)  # Allows for max api requests / days in month per day for the entire guild (Temporary) -guildcd = commands.CooldownMapping.from_cooldown(Wolfram.guild_limit_day, 60 * 60 * 24, BucketType.guild) +guildcd = commands.CooldownMapping.from_cooldown(WolframConfig.guild_limit_day, 60 * 60 * 24, BucketType.guild)  async def send_embed( @@ -64,10 +64,10 @@ def custom_cooldown(*ignore: int) -> Callable:          if ctx.invoked_with == "help":              # if the invoked command is help we don't want to increase the ratelimits since it's not actually              # invoking the command/making a request, so instead just check if the user/guild are on cooldown. -            guild_cooldown = not guildcd.get_bucket(ctx.message).get_tokens() == 0  # if guild is on cooldown +            guild_cooldown = guildcd.get_bucket(ctx.message).get_tokens() != 0  # if guild is on cooldown              # check the message is in a guild, and check user bucket if user is not ignored              if ctx.guild and not any(r.id in ignore for r in ctx.author.roles): -                return guild_cooldown and not usercd.get_bucket(ctx.message).get_tokens() == 0 +                return guild_cooldown and usercd.get_bucket(ctx.message).get_tokens() != 0              return guild_cooldown          user_bucket = usercd.get_bucket(ctx.message) @@ -105,7 +105,7 @@ def custom_cooldown(*ignore: int) -> Callable:      return check(predicate) -async def get_pod_pages(ctx: Context, bot: Bot, query: str) -> Optional[list[tuple[str, str]]]: +async def get_pod_pages(ctx: Context, bot: Bot, query: str) -> list[tuple[str, str]] | None:      """Get the Wolfram API pod pages for the provided query."""      async with ctx.typing():          params = { @@ -253,10 +253,7 @@ class Wolfram(Cog):          if not pages:              return -        if len(pages) >= 2: -            page = pages[1] -        else: -            page = pages[0] +        page = pages[1] if len(pages) >= 2 else pages[0]          await send_embed(ctx, page[0], colour=Colours.soft_orange, img_url=page[1]) @@ -303,4 +300,7 @@ class Wolfram(Cog):  async def setup(bot: Bot) -> None:      """Load the Wolfram cog.""" +    if not WolframConfig.key: +        log.warning("No Wolfram API Key was provided. Not loading Wolfram Cog.") +        return      await bot.add_cog(Wolfram(bot)) diff --git a/bot/exts/utilities/wtf_python.py b/bot/exts/utilities/wtf_python.py index 0c0375cb..29a9611c 100644 --- a/bot/exts/utilities/wtf_python.py +++ b/bot/exts/utilities/wtf_python.py @@ -1,7 +1,6 @@  import logging  import random  import re -from typing import Optional  import rapidfuzz  from discord import Embed, File @@ -67,7 +66,7 @@ class WTFPython(commands.Cog):                  hyper_link = match[0].split("(")[1].replace(")", "")                  self.headers[match[0]] = f"{BASE_URL}/{hyper_link}" -    def fuzzy_match_header(self, query: str) -> Optional[str]: +    def fuzzy_match_header(self, query: str) -> str | None:          """          Returns the fuzzy match of a query if its ratio is above "MINIMUM_CERTAINTY" else returns None. @@ -79,7 +78,7 @@ class WTFPython(commands.Cog):          return match if certainty > MINIMUM_CERTAINTY else None      @commands.command(aliases=("wtf",)) -    async def wtf_python(self, ctx: commands.Context, *, query: Optional[str] = None) -> None: +    async def wtf_python(self, ctx: commands.Context, *, query: str | None = None) -> None:          """          Search WTF Python repository. | 
