diff options
| author | 2023-05-17 12:23:29 +0530 | |
|---|---|---|
| committer | 2023-05-17 12:23:29 +0530 | |
| commit | 7876744fedab9fbb22a160c8950ee22262570270 (patch) | |
| tree | f7189b5f3e9c0947d71f86461f151e50092f6e2c /bot/exts/fun | |
| parent | nit (diff) | |
| parent | Bump sentry-sdk from 1.22.2 to 1.23.0 (#1277) (diff) | |
Merge branch 'main' into undeprecate-bookmark
Diffstat (limited to 'bot/exts/fun')
| -rw-r--r-- | bot/exts/fun/anagram.py | 2 | ||||
| -rw-r--r-- | bot/exts/fun/battleship.py | 20 | ||||
| -rw-r--r-- | bot/exts/fun/catify.py | 30 | ||||
| -rw-r--r-- | bot/exts/fun/connect_four.py | 29 | ||||
| -rw-r--r-- | bot/exts/fun/duck_game.py | 14 | ||||
| -rw-r--r-- | bot/exts/fun/game.py | 38 | ||||
| -rw-r--r-- | bot/exts/fun/hangman.py | 9 | ||||
| -rw-r--r-- | bot/exts/fun/latex.py | 19 | ||||
| -rw-r--r-- | bot/exts/fun/madlibs.py | 2 | ||||
| -rw-r--r-- | bot/exts/fun/minesweeper.py | 11 | ||||
| -rw-r--r-- | bot/exts/fun/movie.py | 19 | ||||
| -rw-r--r-- | bot/exts/fun/quack.py | 6 | ||||
| -rw-r--r-- | bot/exts/fun/snakes/__init__.py | 3 | ||||
| -rw-r--r-- | bot/exts/fun/snakes/_snakes_cog.py | 113 | ||||
| -rw-r--r-- | bot/exts/fun/snakes/_utils.py | 46 | ||||
| -rw-r--r-- | bot/exts/fun/space.py | 34 | ||||
| -rw-r--r-- | bot/exts/fun/tic_tac_toe.py | 25 | ||||
| -rw-r--r-- | bot/exts/fun/trivia_quiz.py | 14 | ||||
| -rw-r--r-- | bot/exts/fun/uwu.py | 6 | ||||
| -rw-r--r-- | bot/exts/fun/wonder_twins.py | 4 | ||||
| -rw-r--r-- | bot/exts/fun/xkcd.py | 5 | 
21 files changed, 225 insertions, 224 deletions
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.  |