aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts/evergreen
diff options
context:
space:
mode:
Diffstat (limited to 'bot/exts/evergreen')
-rw-r--r--bot/exts/evergreen/8bitify.py55
-rw-r--r--bot/exts/evergreen/avatar_modification/__init__.py0
-rw-r--r--bot/exts/evergreen/avatar_modification/_effects.py287
-rw-r--r--bot/exts/evergreen/avatar_modification/avatar_modify.py370
-rw-r--r--bot/exts/evergreen/battleship.py35
-rw-r--r--bot/exts/evergreen/bookmark.py127
-rw-r--r--bot/exts/evergreen/catify.py86
-rw-r--r--bot/exts/evergreen/cheatsheet.py27
-rw-r--r--bot/exts/evergreen/connect_four.py95
-rw-r--r--bot/exts/evergreen/conversationstarters.py22
-rw-r--r--bot/exts/evergreen/emoji.py18
-rw-r--r--bot/exts/evergreen/error_handler.py21
-rw-r--r--bot/exts/evergreen/fun.py13
-rw-r--r--bot/exts/evergreen/game.py39
-rw-r--r--bot/exts/evergreen/githubinfo.py54
-rw-r--r--bot/exts/evergreen/help.py228
-rw-r--r--bot/exts/evergreen/issues.py27
-rw-r--r--bot/exts/evergreen/latex.py11
-rw-r--r--bot/exts/evergreen/magic_8ball.py17
-rw-r--r--bot/exts/evergreen/minesweeper.py57
-rw-r--r--bot/exts/evergreen/movie.py40
-rw-r--r--bot/exts/evergreen/ping.py26
-rw-r--r--bot/exts/evergreen/pythonfacts.py29
-rw-r--r--bot/exts/evergreen/recommend_game.py17
-rw-r--r--bot/exts/evergreen/reddit.py425
-rw-r--r--bot/exts/evergreen/snakes/__init__.py7
-rw-r--r--bot/exts/evergreen/snakes/_converter.py19
-rw-r--r--bot/exts/evergreen/snakes/_snakes_cog.py337
-rw-r--r--bot/exts/evergreen/snakes/_utils.py60
-rw-r--r--bot/exts/evergreen/source.py33
-rw-r--r--bot/exts/evergreen/space.py36
-rw-r--r--bot/exts/evergreen/speedrun.py13
-rw-r--r--bot/exts/evergreen/status_codes.py21
-rw-r--r--bot/exts/evergreen/tic_tac_toe.py9
-rw-r--r--bot/exts/evergreen/timed.py10
-rw-r--r--bot/exts/evergreen/trivia_quiz.py18
-rw-r--r--bot/exts/evergreen/uptime.py33
-rw-r--r--bot/exts/evergreen/wikipedia.py14
-rw-r--r--bot/exts/evergreen/wolfram.py45
-rw-r--r--bot/exts/evergreen/wonder_twins.py12
-rw-r--r--bot/exts/evergreen/xkcd.py6
41 files changed, 1891 insertions, 908 deletions
diff --git a/bot/exts/evergreen/8bitify.py b/bot/exts/evergreen/8bitify.py
deleted file mode 100644
index 7eb4d313..00000000
--- a/bot/exts/evergreen/8bitify.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from io import BytesIO
-
-import discord
-from PIL import Image
-from discord.ext import commands
-
-
-class EightBitify(commands.Cog):
- """Make your avatar 8bit!"""
-
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
-
- @staticmethod
- def pixelate(image: Image) -> Image:
- """Takes an image and pixelates it."""
- return image.resize((32, 32), resample=Image.NEAREST).resize((1024, 1024), resample=Image.NEAREST)
-
- @staticmethod
- def quantize(image: Image) -> Image:
- """Reduces colour palette to 256 colours."""
- return image.quantize()
-
- @commands.command(name="8bitify")
- async def eightbit_command(self, ctx: commands.Context) -> None:
- """Pixelates your avatar and changes the palette to an 8bit one."""
- async with ctx.typing():
- author = await self.bot.fetch_user(ctx.author.id)
- image_bytes = await author.avatar_url.read()
- avatar = Image.open(BytesIO(image_bytes))
- avatar = avatar.convert("RGBA").resize((1024, 1024))
-
- eightbit = self.pixelate(avatar)
- eightbit = self.quantize(eightbit)
-
- bufferedio = BytesIO()
- eightbit.save(bufferedio, format="PNG")
- bufferedio.seek(0)
-
- file = discord.File(bufferedio, filename="8bitavatar.png")
-
- embed = discord.Embed(
- title="Your 8-bit avatar",
- description='Here is your avatar. I think it looks all cool and "retro"'
- )
-
- embed.set_image(url="attachment://8bitavatar.png")
- embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=ctx.author.avatar_url)
-
- await ctx.send(file=file, embed=embed)
-
-
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
- bot.add_cog(EightBitify(bot))
diff --git a/bot/exts/evergreen/avatar_modification/__init__.py b/bot/exts/evergreen/avatar_modification/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/bot/exts/evergreen/avatar_modification/__init__.py
diff --git a/bot/exts/evergreen/avatar_modification/_effects.py b/bot/exts/evergreen/avatar_modification/_effects.py
new file mode 100644
index 00000000..b53b26f3
--- /dev/null
+++ b/bot/exts/evergreen/avatar_modification/_effects.py
@@ -0,0 +1,287 @@
+import math
+import random
+import typing as t
+from io import BytesIO
+from pathlib import Path
+
+import discord
+from PIL import Image, ImageDraw, ImageOps
+
+from bot.constants import Colours
+
+
+class PfpEffects:
+ """
+ Implements various image modifying effects, for the PfpModify cog.
+
+ All of these functions are slow, and blocking, so they should be ran in executors.
+ """
+
+ @staticmethod
+ def apply_effect(image_bytes: bytes, effect: t.Callable, filename: str, *args) -> discord.File:
+ """Applies the given effect to the image passed to it."""
+ im = Image.open(BytesIO(image_bytes))
+ im = im.convert("RGBA")
+ im = effect(im, *args)
+
+ bufferedio = BytesIO()
+ im.save(bufferedio, format="PNG")
+ bufferedio.seek(0)
+
+ return discord.File(bufferedio, filename=filename)
+
+ @staticmethod
+ def closest(x: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]:
+ """
+ Finds the closest "easter" colour to a given pixel.
+
+ Returns a merge between the original colour and the closest colour.
+ """
+ r1, g1, b1 = x
+
+ def distance(point: t.Tuple[int, int, int]) -> t.Tuple[int, int, int]:
+ """Finds the difference between a pastel colour and the original pixel colour."""
+ r2, g2, b2 = point
+ return (r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2
+
+ closest_colours = sorted(Colours.easter_like_colours, key=distance)
+ r2, g2, b2 = closest_colours[0]
+ r = (r1 + r2) // 2
+ g = (g1 + g2) // 2
+ b = (b1 + b2) // 2
+
+ return r, g, b
+
+ @staticmethod
+ def crop_avatar_circle(avatar: Image.Image) -> Image.Image:
+ """This crops 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)
+ avatar.putalpha(mask)
+ return avatar
+
+ @staticmethod
+ def crop_ring(ring: Image.Image, px: int) -> Image.Image:
+ """This crops 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)
+ draw.ellipse((px, px, 1024-px, 1024-px), fill=0)
+ ring.putalpha(mask)
+ return ring
+
+ @staticmethod
+ def pridify_effect(image: Image.Image, pixels: int, flag: str) -> Image.Image:
+ """Applies the given pride effect to the given image."""
+ image = image.resize((1024, 1024))
+ image = PfpEffects.crop_avatar_circle(image)
+
+ ring = Image.open(Path(f"bot/resources/pride/flags/{flag}.png")).resize((1024, 1024))
+ ring = ring.convert("RGBA")
+ ring = PfpEffects.crop_ring(ring, pixels)
+
+ image.alpha_composite(ring, (0, 0))
+ return image
+
+ @staticmethod
+ def eight_bitify_effect(image: Image.Image) -> Image.Image:
+ """
+ Applies the 8bit effect to the given image.
+
+ This is done by reducing the image to 32x32 and then back up to 1024x1024.
+ We then quantize the image before returning too.
+ """
+ image = image.resize((32, 32), resample=Image.NEAREST)
+ image = image.resize((1024, 1024), resample=Image.NEAREST)
+ return image.quantize()
+
+ @staticmethod
+ def easterify_effect(image: Image.Image, overlay_image: t.Optional[Image.Image] = None) -> Image.Image:
+ """
+ Applies the easter effect to the given image.
+
+ This is done by getting the closest "easter" colour to each pixel and changing the colour
+ to the half-way RGB value.
+
+ We also then add an overlay image on top in middle right, a chocolate bunny by default.
+ """
+ if overlay_image:
+ ratio = 64 / overlay_image.height
+ overlay_image = overlay_image.resize((
+ round(overlay_image.width * ratio),
+ round(overlay_image.height * ratio)
+ ))
+ overlay_image = overlay_image.convert("RGBA")
+ else:
+ overlay_image = Image.open(Path("bot/resources/easter/chocolate_bunny.png"))
+
+ alpha = image.getchannel("A").getdata()
+ image = image.convert("RGB")
+ image = ImageOps.posterize(image, 6)
+
+ data = image.getdata()
+ data_set = set(data)
+ easterified_data_set = {}
+
+ for x in data_set:
+ easterified_data_set[x] = PfpEffects.closest(x)
+ new_pixel_data = [
+ (*easterified_data_set[x], alpha[i])
+ if x in easterified_data_set else x
+ for i, x in enumerate(data)
+ ]
+
+ im = Image.new("RGBA", image.size)
+ im.putdata(new_pixel_data)
+ im.alpha_composite(
+ overlay_image,
+ (im.width - overlay_image.width, (im.height - overlay_image.height) // 2)
+ )
+ return im
+
+ @staticmethod
+ def split_image(img: Image.Image, squares: int) -> list:
+ """
+ Split an image into a selection of squares, specified by the squares argument.
+
+ Explanation:
+
+ 1. It gets the width and the height of the Image passed to the function.
+
+ 2. It gets the root of a number of squares (number of squares) passed, which is called xy. Reason: if let's say
+ 25 squares (number of squares) were passed, that is the total squares (split pieces) that the image is supposed
+ to be split into. As it is known, a 2D shape has a height and a width, and in this case the program thinks of it
+ as rows and columns. Rows multiplied by columns is equal to the passed squares (number of squares). To get rows
+ and columns, since in this case, each square (split piece) is identical, rows are equal to columns and the
+ program treats the image as a square-shaped, it gets the root out of the squares (number of squares) passed.
+
+ 3. Now width and height are both of the original Image, Discord PFP, so when it comes to forming the squares,
+ the program divides the original image's height and width by the xy. In a case of 25 squares (number of squares)
+ passed, xy would be 5, so if an image was 250x300, x_frac would be 50 and y_frac - 60. Note:
+ x_frac stands for a fracture of width. The reason it's called that is because it is shorter to use x for width
+ in mind and then it's just half of the word fracture, same applies to y_frac, just height instead of width.
+ x_frac and y_frac are width and height of a single square (split piece).
+
+ 4. With left, top, right, bottom, = 0, 0, x_frac, y_frac, the program sets these variables to create the initial
+ square (split piece). Explanation: all of these 4 variables start at the top left corner of the Image, by adding
+ value to right and bottom, it's creating the initial square (split piece).
+
+ 5. In the for loop, it keeps adding those squares (split pieces) in a row and once (index + 1) % xy == 0 is
+ True, it adds to top and bottom to lower them and reset right and left to recreate the initial space between
+ them, forming a square (split piece), it also adds the newly created square (split piece) into the new_imgs list
+ where it stores them. The program keeps repeating this process till all 25 squares get added to the list.
+
+ 6. It returns new_imgs, a list of squares (split pieces).
+ """
+ width, heigth = img.size
+
+ xy = math.sqrt(squares)
+
+ x_frac = width // xy
+ y_frac = heigth // xy
+
+ left, top, right, bottom, = 0, 0, x_frac, y_frac
+
+ new_imgs = []
+
+ for index in range(squares):
+ new_img = img.crop((left, top, right, bottom))
+ new_imgs.append(new_img)
+
+ if (index + 1) % xy == 0:
+ top += y_frac
+ bottom += y_frac
+ left = 0
+ right = x_frac
+ else:
+ left += x_frac
+ right += x_frac
+
+ return new_imgs
+
+ @staticmethod
+ def join_images(images: t.List[Image.Image]) -> Image.Image:
+ """
+ Stitches all the image squares into a new image.
+
+ Explanation:
+
+ 1. Shuffles the passed images to randomize the pieces.
+
+ 2. The program gets a single square (split piece) out of the list and defines single_width as the square's width
+ and single_height as the square's height.
+
+ 3. It gets the root of type integer of the number of images (split pieces) in the list and calls it multiplier.
+ Program then proceeds to calculate total height and width of the new image that it's creating using the same
+ multiplier.
+
+ 4. The program then defines new_image as the image that it's creating, using the previously obtained total_width
+ and total_height.
+
+ 5. Now it defines width_multiplier as well as height with values of 0. These will be used to correctly position
+ squares (split pieces) onto the new_image canvas.
+
+ 6. Similar to how in the split_image function, the program gets the root of number of images in the list.
+ In split_image function, it was the passed squares (number of squares) instead of a number of imgs in the
+ list that it got the square of here.
+
+ 7. In the for loop, as it iterates, the program multiplies single_width by width_multiplier to correctly
+ position a square (split piece) width wise. It then proceeds to paste the newly positioned square (split piece)
+ onto the new_image. The program increases the width_multiplier by 1 every iteration so the image wouldn't get
+ pasted in the same spot and the positioning would move accordingly. It makes sure to increase the
+ width_multiplier before the check, which checks if the end of a row has been reached, -
+ (index + 1) % pieces == 0, so after it, if it was True, width_multiplier would have been reset to 0 (start of
+ the row). If the check returns True, the height gets increased by a single square's (split piece) height to
+ lower the positioning height wise and, as mentioned, the width_multiplier gets reset to 0 and width will
+ then be calculated from the start of the new row. The for loop finishes once all the squares (split pieces) were
+ positioned accordingly.
+
+ 8. Finally, it returns the new_image, the randomized squares (split pieces) stitched back into the format of the
+ original image - user's PFP.
+ """
+ random.shuffle(images)
+ single_img = images[0]
+
+ single_wdith = single_img.size[0]
+ single_height = single_img.size[1]
+
+ multiplier = int(math.sqrt(len(images)))
+
+ total_width = multiplier * single_wdith
+ total_height = multiplier * single_height
+
+ new_image = Image.new("RGBA", (total_width, total_height), (250, 250, 250))
+
+ width_multiplier = 0
+ height = 0
+
+ squares = math.sqrt(len(images))
+
+ for index, image in enumerate(images):
+ width = single_wdith * width_multiplier
+
+ new_image.paste(image, (width, height))
+
+ width_multiplier += 1
+
+ if (index + 1) % squares == 0:
+ width_multiplier = 0
+ height += single_height
+
+ return new_image
+
+ @staticmethod
+ def mosaic_effect(img_bytes: bytes, squares: int, file_name: str) -> discord.File:
+ """Separate function run from an executor which turns an image into a mosaic."""
+ avatar = Image.open(BytesIO(img_bytes))
+ avatar = avatar.convert("RGBA").resize((1024, 1024))
+
+ img_squares = PfpEffects.split_image(avatar, squares)
+ new_img = PfpEffects.join_images(img_squares)
+
+ bufferedio = BytesIO()
+ new_img.save(bufferedio, format="PNG")
+ bufferedio.seek(0)
+
+ return discord.File(bufferedio, filename=file_name)
diff --git a/bot/exts/evergreen/avatar_modification/avatar_modify.py b/bot/exts/evergreen/avatar_modification/avatar_modify.py
new file mode 100644
index 00000000..17f34ed4
--- /dev/null
+++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py
@@ -0,0 +1,370 @@
+import asyncio
+import json
+import logging
+import math
+import string
+import typing as t
+import unicodedata
+from concurrent.futures import ThreadPoolExecutor
+from pathlib import Path
+
+import discord
+from aiohttp import client_exceptions
+from discord.ext import commands
+
+from bot.bot import Bot
+from bot.constants import Colours, Emojis
+from bot.exts.evergreen.avatar_modification._effects import PfpEffects
+from bot.utils.extensions import invoke_help_command
+from bot.utils.halloween import spookifications
+
+log = logging.getLogger(__name__)
+
+_EXECUTOR = ThreadPoolExecutor(10)
+
+FILENAME_STRING = "{effect}_{author}.png"
+
+MAX_SQUARES = 10_000
+
+T = t.TypeVar("T")
+
+GENDER_OPTIONS = json.loads(Path("bot/resources/pride/gender_options.json").read_text("utf8"))
+
+
+async def in_executor(func: t.Callable[..., T], *args) -> T:
+ """
+ Runs the given synchronous function `func` in an executor.
+
+ This is useful for running slow, blocking code within async
+ functions, so that they don't block the bot.
+ """
+ log.trace(f"Running {func.__name__} in an executor.")
+ loop = asyncio.get_event_loop()
+ return await loop.run_in_executor(_EXECUTOR, func, *args)
+
+
+def file_safe_name(effect: str, display_name: str) -> str:
+ """Returns a file safe filename based on the given effect and display name."""
+ valid_filename_chars = f"-_. {string.ascii_letters}{string.digits}"
+
+ file_name = FILENAME_STRING.format(effect=effect, author=display_name)
+
+ # Replace spaces
+ file_name = file_name.replace(" ", "_")
+
+ # Normalize unicode characters
+ cleaned_filename = unicodedata.normalize("NFKD", file_name).encode("ASCII", "ignore").decode()
+
+ # Remove invalid filename characters
+ cleaned_filename = "".join(c for c in cleaned_filename if c in valid_filename_chars)
+ return cleaned_filename
+
+
+class AvatarModify(commands.Cog):
+ """Various commands for users to apply affects to their own avatars."""
+
+ def __init__(self, bot: Bot) -> None:
+ self.bot = bot
+
+ async def _fetch_user(self, user_id: int) -> t.Optional[discord.User]:
+ """
+ Fetches a user and handles errors.
+
+ This helper function is required as the member cache doesn't always have the most up to date
+ profile picture. This can lead to errors if the image is deleted from the Discord CDN.
+ fetch_member can't be used due to the avatar url being part of the user object, and
+ some weird caching that D.py does
+ """
+ try:
+ user = await self.bot.fetch_user(user_id)
+ except discord.errors.NotFound:
+ log.debug(f"User {user_id} could not be found.")
+ return None
+ except discord.HTTPException:
+ log.exception(f"Exception while trying to retrieve user {user_id} from Discord.")
+ return None
+
+ return user
+
+ @commands.group(aliases=("avatar_mod", "pfp_mod", "avatarmod", "pfpmod"))
+ async def avatar_modify(self, ctx: commands.Context) -> None:
+ """Groups all of the pfp modifying commands to allow a single concurrency limit."""
+ if not ctx.invoked_subcommand:
+ await invoke_help_command(ctx)
+
+ @avatar_modify.command(name="8bitify", root_aliases=("8bitify",))
+ async def eightbit_command(self, ctx: commands.Context) -> None:
+ """Pixelates your avatar and changes the palette to an 8bit one."""
+ async with ctx.typing():
+ user = await self._fetch_user(ctx.author.id)
+ if not user:
+ await ctx.send(f"{Emojis.cross_mark} Could not get user info.")
+ return
+
+ image_bytes = await user.avatar_url_as(size=1024).read()
+ file_name = file_safe_name("eightbit_avatar", ctx.author.display_name)
+
+ file = await in_executor(
+ PfpEffects.apply_effect,
+ image_bytes,
+ PfpEffects.eight_bitify_effect,
+ file_name
+ )
+
+ embed = discord.Embed(
+ title="Your 8-bit avatar",
+ description="Here is your avatar. I think it looks all cool and 'retro'."
+ )
+
+ embed.set_image(url=f"attachment://{file_name}")
+ embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=user.avatar_url)
+
+ await ctx.send(embed=embed, file=file)
+
+ @avatar_modify.command(aliases=("easterify",), root_aliases=("easterify", "avatareasterify"))
+ async def avatareasterify(self, ctx: commands.Context, *colours: t.Union[discord.Colour, str]) -> None:
+ """
+ This "Easterifies" 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.
+ Colours are split by spaces, unless you wrap the colour name in double quotes.
+ Discord colour names, HTML colour names, XKCD colour names and hex values are accepted.
+ """
+ async def send(*args, **kwargs) -> str:
+ """
+ This replaces 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.
+ """
+ if args:
+ return args[0]
+
+ async with ctx.typing():
+ user = await self._fetch_user(ctx.author.id)
+ if not user:
+ await ctx.send(f"{Emojis.cross_mark} Could not get user info.")
+ return
+
+ egg = None
+ if colours:
+ send_message = ctx.send
+ ctx.send = send # Assigns ctx.send to a fake send
+ egg = await ctx.invoke(self.bot.get_command("eggdecorate"), *colours)
+ if isinstance(egg, str): # When an error message occurs in eggdecorate.
+ await send_message(egg)
+ return
+ ctx.send = send_message # Reassigns ctx.send
+
+ image_bytes = await user.avatar_url_as(size=256).read()
+ file_name = file_safe_name("easterified_avatar", ctx.author.display_name)
+
+ file = await in_executor(
+ PfpEffects.apply_effect,
+ image_bytes,
+ PfpEffects.easterify_effect,
+ file_name,
+ egg
+ )
+
+ embed = discord.Embed(
+ name="Your Lovely Easterified Avatar!",
+ description="Here is your lovely avatar, all bright and colourful\nwith Easter pastel colours. Enjoy :D"
+ )
+ embed.set_image(url=f"attachment://{file_name}")
+ embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=user.avatar_url)
+
+ await ctx.send(file=file, embed=embed)
+
+ @staticmethod
+ async def send_pride_image(
+ ctx: commands.Context,
+ image_bytes: bytes,
+ pixels: int,
+ flag: str,
+ option: str
+ ) -> None:
+ """Gets and sends the image in an embed. Used by the pride commands."""
+ async with ctx.typing():
+ file_name = file_safe_name("pride_avatar", ctx.author.display_name)
+
+ file = await in_executor(
+ PfpEffects.apply_effect,
+ image_bytes,
+ PfpEffects.pridify_effect,
+ file_name,
+ pixels,
+ flag
+ )
+
+ embed = discord.Embed(
+ name="Your Lovely Pride Avatar!",
+ description=f"Here is your lovely avatar, surrounded by\n a beautiful {option} flag. Enjoy :D"
+ )
+ embed.set_image(url=f"attachment://{file_name}")
+ embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url)
+ await ctx.send(file=file, embed=embed)
+
+ @avatar_modify.group(
+ aliases=("avatarpride", "pridepfp", "prideprofile"),
+ root_aliases=("prideavatar", "avatarpride", "pridepfp", "prideprofile"),
+ invoke_without_command=True
+ )
+ 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.
+
+ This defaults 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.
+ The full image is 1024x1024.
+ """
+ option = option.lower()
+ pixels = max(0, min(512, pixels))
+ flag = GENDER_OPTIONS.get(option)
+ if flag is None:
+ await ctx.send("I don't have that flag!")
+ return
+
+ async with ctx.typing():
+ user = await self._fetch_user(ctx.author.id)
+ if not user:
+ await ctx.send(f"{Emojis.cross_mark} Could not get user info.")
+ return
+ image_bytes = await user.avatar_url_as(size=1024).read()
+ await self.send_pride_image(ctx, image_bytes, pixels, flag, option)
+
+ @prideavatar.command()
+ async def image(self, ctx: commands.Context, url: str, option: str = "lgbt", pixels: int = 64) -> None:
+ """
+ This surrounds the image specified by the URL with a border of a specified LGBT flag.
+
+ This defaults 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.
+ The full image is 1024x1024.
+ """
+ option = option.lower()
+ pixels = max(0, min(512, pixels))
+ flag = GENDER_OPTIONS.get(option)
+ if flag is None:
+ await ctx.send("I don't have that flag!")
+ return
+
+ async with ctx.typing():
+ try:
+ async with self.bot.http_session.get(url) as response:
+ if response.status != 200:
+ await ctx.send("Bad response from provided URL!")
+ return
+ image_bytes = await response.read()
+ except client_exceptions.ClientConnectorError:
+ raise commands.BadArgument("Cannot connect to provided URL!")
+ except client_exceptions.InvalidURL:
+ raise commands.BadArgument("Invalid URL!")
+
+ await self.send_pride_image(ctx, image_bytes, pixels, flag, option)
+
+ @prideavatar.command()
+ async def flags(self, ctx: commands.Context) -> None:
+ """This lists the flags that can be used with the prideavatar command."""
+ choices = sorted(set(GENDER_OPTIONS.values()))
+ options = "• " + "\n• ".join(choices)
+ embed = discord.Embed(
+ title="I have the following flags:",
+ description=options,
+ colour=Colours.soft_red
+ )
+ await ctx.send(embed=embed)
+
+ @avatar_modify.command(
+ aliases=("savatar", "spookify"),
+ root_aliases=("spookyavatar", "spookify", "savatar"),
+ brief="Spookify an user's avatar."
+ )
+ async def spookyavatar(self, ctx: commands.Context, member: discord.Member = None) -> None:
+ """This "spookifies" the given user's avatar, with a random *spooky* effect."""
+ if member is None:
+ member = ctx.author
+
+ user = await self._fetch_user(member.id)
+ if not user:
+ await ctx.send(f"{Emojis.cross_mark} Could not get user info.")
+ return
+
+ async with ctx.typing():
+ image_bytes = await user.avatar_url_as(size=1024).read()
+
+ file_name = file_safe_name("spooky_avatar", member.display_name)
+
+ file = await in_executor(
+ PfpEffects.apply_effect,
+ image_bytes,
+ spookifications.get_random_effect,
+ file_name
+ )
+
+ embed = discord.Embed(
+ title="Is this you or am I just really paranoid?",
+ colour=Colours.soft_red
+ )
+ embed.set_author(name=member.name, icon_url=member.avatar_url)
+ embed.set_image(url=f"attachment://{file_name}")
+ embed.set_footer(text=f"Made by {ctx.author.display_name}.", icon_url=ctx.author.avatar_url)
+
+ await ctx.send(file=file, embed=embed)
+
+ @avatar_modify.command(name="mosaic", root_aliases=("mosaic",))
+ async def mosaic_command(self, ctx: commands.Context, squares: int = 16) -> None:
+ """Splits your avatar into x squares, randomizes them and stitches them back into a new image!"""
+ async with ctx.typing():
+ user = await self._fetch_user(ctx.author.id)
+ if not user:
+ await ctx.send(f"{Emojis.cross_mark} Could not get user info.")
+ return
+
+ if not 1 <= squares <= MAX_SQUARES:
+ raise commands.BadArgument(f"Squares must be a positive number less than or equal to {MAX_SQUARES:,}.")
+
+ sqrt = math.sqrt(squares)
+
+ if not sqrt.is_integer():
+ squares = math.ceil(sqrt) ** 2 # Get the next perfect square
+
+ file_name = file_safe_name("mosaic_avatar", ctx.author.display_name)
+
+ img_bytes = await user.avatar_url_as(size=1024).read()
+
+ file = await in_executor(
+ PfpEffects.mosaic_effect,
+ img_bytes,
+ squares,
+ file_name
+ )
+
+ if squares == 1:
+ title = "Hooh... that was a lot of work"
+ description = "I present to you... Yourself!"
+ elif squares == MAX_SQUARES:
+ title = "Testing the limits I see..."
+ description = "What a masterpiece. :star:"
+ else:
+ title = "Your mosaic avatar"
+ description = f"Here is your avatar. I think it looks a bit *puzzling*\nMade with {squares} squares."
+
+ embed = discord.Embed(
+ title=title,
+ description=description,
+ colour=Colours.blue
+ )
+
+ embed.set_image(url=f"attachment://{file_name}")
+ embed.set_footer(text=f"Made by {ctx.author.display_name}", icon_url=user.avatar_url)
+
+ await ctx.send(file=file, embed=embed)
+
+
+def setup(bot: Bot) -> None:
+ """Load the AvatarModify cog."""
+ bot.add_cog(AvatarModify(bot))
diff --git a/bot/exts/evergreen/battleship.py b/bot/exts/evergreen/battleship.py
index 1681434f..c2f2079c 100644
--- a/bot/exts/evergreen/battleship.py
+++ b/bot/exts/evergreen/battleship.py
@@ -9,6 +9,7 @@ from functools import partial
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
@@ -30,8 +31,8 @@ EmojiSet = typing.Dict[typing.Tuple[bool, bool], str]
class Player:
"""Each player in the game - their messages for the boards and their current grid."""
- user: discord.Member
- board: discord.Message
+ user: typing.Optional[discord.Member]
+ board: typing.Optional[discord.Message]
opponent_board: discord.Message
grid: Grid
@@ -95,7 +96,7 @@ class Game:
def __init__(
self,
- bot: commands.Bot,
+ bot: Bot,
channel: discord.TextChannel,
player1: discord.Member,
player2: discord.Member
@@ -237,7 +238,7 @@ class Game:
square = None
turn_message = await self.turn.user.send(
"It's your turn! Type the square you want to fire at. Format it like this: A1\n"
- "Type `surrender` to give up"
+ "Type `surrender` to give up."
)
await self.next.user.send("Their turn", delete_after=3.0)
while True:
@@ -321,7 +322,7 @@ class Game:
class Battleship(commands.Cog):
"""Play the classic game Battleship!"""
- def __init__(self, bot: commands.Bot) -> None:
+ def __init__(self, bot: Bot) -> None:
self.bot = bot
self.games: typing.List[Game] = []
self.waiting: typing.List[discord.Member] = []
@@ -378,10 +379,12 @@ class Battleship(commands.Cog):
Make sure you have your DMs open so that the bot can message you.
"""
if self.already_playing(ctx.author):
- return await ctx.send("You're already playing a game!")
+ await ctx.send("You're already playing a game!")
+ return
if ctx.author in self.waiting:
- return await ctx.send("You've already sent out a request for a player 2")
+ await ctx.send("You've already sent out a request for a player 2.")
+ return
announcement = await ctx.send(
"**Battleship**: A new game is about to start!\n"
@@ -401,20 +404,22 @@ class Battleship(commands.Cog):
except asyncio.TimeoutError:
self.waiting.remove(ctx.author)
await announcement.delete()
- return await ctx.send(f"{ctx.author.mention} Seems like there's no one here to play...")
+ await ctx.send(f"{ctx.author.mention} Seems like there's no one here to play...")
+ return
if str(reaction.emoji) == CROSS_EMOJI:
self.waiting.remove(ctx.author)
await announcement.delete()
- return await ctx.send(f"{ctx.author.mention} Game cancelled.")
+ await ctx.send(f"{ctx.author.mention} Game cancelled.")
+ return
await announcement.delete()
self.waiting.remove(ctx.author)
if self.already_playing(ctx.author):
return
+ game = Game(self.bot, ctx.channel, ctx.author, user)
+ self.games.append(game)
try:
- game = Game(self.bot, ctx.channel, ctx.author, user)
- self.games.append(game)
await game.start_game()
self.games.remove(game)
except discord.Forbidden:
@@ -425,11 +430,11 @@ class Battleship(commands.Cog):
self.games.remove(game)
except Exception:
# End the game in the event of an unforseen error so the players aren't stuck in a game
- await ctx.send(f"{ctx.author.mention} {user.mention} An error occurred. Game failed")
+ await ctx.send(f"{ctx.author.mention} {user.mention} An error occurred. Game failed.")
self.games.remove(game)
raise
- @battleship.command(name="ships", aliases=["boats"])
+ @battleship.command(name="ships", aliases=("boats",))
async def battleship_ships(self, ctx: commands.Context) -> None:
"""Lists the ships that are found on the battleship grid."""
embed = discord.Embed(colour=Colours.blue)
@@ -438,6 +443,6 @@ class Battleship(commands.Cog):
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Battleship Cog."""
bot.add_cog(Battleship(bot))
diff --git a/bot/exts/evergreen/bookmark.py b/bot/exts/evergreen/bookmark.py
index 5fa05d2e..29915627 100644
--- a/bot/exts/evergreen/bookmark.py
+++ b/bot/exts/evergreen/bookmark.py
@@ -1,21 +1,89 @@
+import asyncio
import logging
import random
import discord
from discord.ext import commands
-from bot.constants import Colours, ERROR_REPLIES, Emojis, Icons
+from bot.bot import Bot
+from bot.constants import Colours, ERROR_REPLIES, Icons
from bot.utils.converters import WrappedMessageConverter
log = logging.getLogger(__name__)
+# Number of seconds to wait for other users to bookmark the same message
+TIMEOUT = 120
+BOOKMARK_EMOJI = "📌"
+
class Bookmark(commands.Cog):
"""Creates personal bookmarks by relaying a message link to the user's DMs."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
+ @staticmethod
+ def build_bookmark_dm(target_message: discord.Message, title: str) -> discord.Embed:
+ """Build the embed to DM the bookmark requester."""
+ embed = discord.Embed(
+ title=title,
+ description=target_message.content,
+ colour=Colours.soft_green
+ )
+ embed.add_field(
+ name="Wanna give it a visit?",
+ value=f"[Visit original message]({target_message.jump_url})"
+ )
+ embed.set_author(name=target_message.author, icon_url=target_message.author.avatar_url)
+ embed.set_thumbnail(url=Icons.bookmark)
+
+ return embed
+
+ @staticmethod
+ def build_error_embed(user: discord.Member) -> discord.Embed:
+ """Builds an error embed for when a bookmark requester has DMs disabled."""
+ return discord.Embed(
+ title=random.choice(ERROR_REPLIES),
+ description=f"{user.mention}, please enable your DMs to receive the bookmark.",
+ colour=Colours.soft_red
+ )
+
+ async def action_bookmark(
+ self,
+ channel: discord.TextChannel,
+ user: discord.Member,
+ target_message: discord.Message,
+ title: str
+ ) -> None:
+ """Sends the bookmark DM, or sends an error embed when a user bookmarks a message."""
+ try:
+ embed = self.build_bookmark_dm(target_message, title)
+ await user.send(embed=embed)
+ except discord.Forbidden:
+ error_embed = self.build_error_embed(user)
+ await channel.send(embed=error_embed)
+ else:
+ log.info(f"{user} bookmarked {target_message.jump_url} with title '{title}'")
+
+ @staticmethod
+ async def send_reaction_embed(
+ channel: discord.TextChannel,
+ target_message: discord.Message
+ ) -> discord.Message:
+ """Sends an embed, with a reaction, so users can react to bookmark the message too."""
+ message = await channel.send(
+ embed=discord.Embed(
+ description=(
+ f"React with {BOOKMARK_EMOJI} to be sent your very own bookmark to "
+ f"[this message]({target_message.jump_url})."
+ ),
+ colour=Colours.soft_green
+ )
+ )
+
+ await message.add_reaction(BOOKMARK_EMOJI)
+ return message
+
@commands.command(name="bookmark", aliases=("bm", "pin"))
async def bookmark(
self,
@@ -28,7 +96,7 @@ class Bookmark(commands.Cog):
# Prevent users from bookmarking a message in a channel they don't have access to
permissions = ctx.author.permissions_in(target_message.channel)
if not permissions.read_messages:
- log.info(f"{ctx.author} tried to bookmark a message in #{target_message.channel} but has no permissions")
+ log.info(f"{ctx.author} tried to bookmark a message in #{target_message.channel} but has no permissions.")
embed = discord.Embed(
title=random.choice(ERROR_REPLIES),
color=Colours.soft_red,
@@ -37,29 +105,40 @@ class Bookmark(commands.Cog):
await ctx.send(embed=embed)
return
- embed = discord.Embed(
- title=title,
- colour=Colours.soft_green,
- description=target_message.content
- )
- embed.add_field(name="Wanna give it a visit?", value=f"[Visit original message]({target_message.jump_url})")
- embed.set_author(name=target_message.author, icon_url=target_message.author.avatar_url)
- embed.set_thumbnail(url=Icons.bookmark)
-
- try:
- await ctx.author.send(embed=embed)
- except discord.Forbidden:
- error_embed = discord.Embed(
- title=random.choice(ERROR_REPLIES),
- description=f"{ctx.author.mention}, please enable your DMs to receive the bookmark",
- colour=Colours.soft_red
+ def event_check(reaction: discord.Reaction, user: discord.Member) -> bool:
+ """Make sure that this reaction is what we want to operate on."""
+ return (
+ # Conditions for a successful pagination:
+ all((
+ # Reaction is on this message
+ reaction.message.id == reaction_message.id,
+ # User has not already bookmarked this message
+ user.id not in bookmarked_users,
+ # Reaction is the `BOOKMARK_EMOJI` emoji
+ str(reaction.emoji) == BOOKMARK_EMOJI,
+ # Reaction was not made by the Bot
+ user.id != self.bot.user.id
+ ))
)
- await ctx.send(embed=error_embed)
- else:
- log.info(f"{ctx.author} bookmarked {target_message.jump_url} with title '{title}'")
- await ctx.message.add_reaction(Emojis.envelope)
+ await self.action_bookmark(ctx.channel, ctx.author, target_message, title)
+
+ # Keep track of who has already bookmarked, so users can't spam reactions and cause loads of DMs
+ bookmarked_users = [ctx.author.id]
+ reaction_message = await self.send_reaction_embed(ctx.channel, target_message)
+
+ while True:
+ try:
+ _, user = await self.bot.wait_for("reaction_add", timeout=TIMEOUT, check=event_check)
+ except asyncio.TimeoutError:
+ log.debug("Timed out waiting for a reaction")
+ break
+ log.trace(f"{user} has successfully bookmarked from a reaction, attempting to DM them.")
+ await self.action_bookmark(ctx.channel, user, target_message, title)
+ bookmarked_users.append(user.id)
+
+ await reaction_message.delete()
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Bookmark cog."""
bot.add_cog(Bookmark(bot))
diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py
new file mode 100644
index 00000000..32dfae09
--- /dev/null
+++ b/bot/exts/evergreen/catify.py
@@ -0,0 +1,86 @@
+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.utils import helpers
+
+
+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:
+ """
+ Convert the provided text into a cat themed sentence by interspercing cats throughout text.
+
+ If no text is given then the users nickname is edited.
+ """
+ if not text:
+ display_name = ctx.author.display_name
+
+ if len(display_name) > 26:
+ embed = Embed(
+ title=random.choice(NEGATIVE_REPLIES),
+ description=(
+ "Your display name is too long to be catified! "
+ "Please change it to be under 26 characters."
+ ),
+ color=Colours.soft_red
+ )
+ await ctx.send(embed=embed)
+ return
+
+ else:
+ display_name += f" | {random.choice(Cats.cats)}"
+
+ await ctx.send(f"Your catified nickname is: `{display_name}`", allowed_mentions=AllowedMentions.none())
+
+ with suppress(Forbidden):
+ await ctx.author.edit(nick=display_name)
+ else:
+ if len(text) >= 1500:
+ embed = Embed(
+ title=random.choice(NEGATIVE_REPLIES),
+ description="Submitted text was too large! Please submit something under 1500 characters.",
+ color=Colours.soft_red
+ )
+ await ctx.send(embed=embed)
+ return
+
+ string_list = text.split()
+ for index, name in enumerate(string_list):
+ name = name.lower()
+ if "cat" in name:
+ if random.randint(0, 5) == 5:
+ string_list[index] = name.replace("cat", f"**{random.choice(Cats.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_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)}**")
+ else:
+ string_list.insert(random.randint(0, len(string_list)), random.choice(Cats.cats))
+
+ text = helpers.suppress_links(" ".join(string_list))
+ await ctx.send(
+ f">>> {text}",
+ allowed_mentions=AllowedMentions.none()
+ )
+
+
+def setup(bot: Bot) -> None:
+ """Loads the catify cog."""
+ bot.add_cog(Catify())
diff --git a/bot/exts/evergreen/cheatsheet.py b/bot/exts/evergreen/cheatsheet.py
index 3fe709d5..ae7793c9 100644
--- a/bot/exts/evergreen/cheatsheet.py
+++ b/bot/exts/evergreen/cheatsheet.py
@@ -8,6 +8,7 @@ from discord.ext import commands
from discord.ext.commands import BucketType, Context
from bot import constants
+from bot.bot import Bot
from bot.constants import Categories, Channels, Colours, ERROR_REPLIES
from bot.utils.decorators import whitelist_override
@@ -23,17 +24,17 @@ Unknown cheat sheet. Please try to reformulate your query.
If the problem persists send a message in <#{Channels.dev_contrib}>
"""
-URL = 'https://cheat.sh/python/{search}'
+URL = "https://cheat.sh/python/{search}"
ESCAPE_TT = str.maketrans({"`": "\\`"})
ANSI_RE = re.compile(r"\x1b\[.*?m")
# We need to pass headers as curl otherwise it would default to aiohttp which would return raw html.
-HEADERS = {'User-Agent': 'curl/7.68.0'}
+HEADERS = {"User-Agent": "curl/7.68.0"}
class CheatSheet(commands.Cog):
"""Commands that sends a result of a cht.sh search in code blocks."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@staticmethod
@@ -60,14 +61,18 @@ class CheatSheet(commands.Cog):
body_space = min(1986 - len(url), 1000)
if len(body_text) > body_space:
- description = (f"**Result Of cht.sh**\n"
- f"```python\n{body_text[:body_space]}\n"
- f"... (truncated - too many lines)```\n"
- f"Full results: {url} ")
+ description = (
+ f"**Result Of cht.sh**\n"
+ f"```python\n{body_text[:body_space]}\n"
+ f"... (truncated - too many lines)```\n"
+ f"Full results: {url} "
+ )
else:
- description = (f"**Result Of cht.sh**\n"
- f"```python\n{body_text}```\n"
- f"{url}")
+ description = (
+ f"**Result Of cht.sh**\n"
+ f"```python\n{body_text}```\n"
+ f"{url}"
+ )
return False, description
@commands.command(
@@ -102,6 +107,6 @@ class CheatSheet(commands.Cog):
await ctx.send(content=description)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the CheatSheet cog."""
bot.add_cog(CheatSheet(bot))
diff --git a/bot/exts/evergreen/connect_four.py b/bot/exts/evergreen/connect_four.py
index 7e3ec42b..5c82ffee 100644
--- a/bot/exts/evergreen/connect_four.py
+++ b/bot/exts/evergreen/connect_four.py
@@ -8,6 +8,7 @@ import emojis
from discord.ext import commands
from discord.ext.commands import guild_only
+from bot.bot import Bot
from bot.constants import Emojis
NUMBERS = list(Emojis.number_emojis.values())
@@ -21,13 +22,13 @@ class Game:
"""A Connect 4 Game."""
def __init__(
- self,
- bot: commands.Bot,
- channel: discord.TextChannel,
- player1: discord.Member,
- player2: typing.Optional[discord.Member],
- tokens: typing.List[str],
- size: int = 7
+ self,
+ bot: Bot,
+ channel: discord.TextChannel,
+ player1: discord.Member,
+ player2: typing.Optional[discord.Member],
+ tokens: typing.List[str],
+ size: int = 7
) -> None:
self.bot = bot
@@ -54,8 +55,8 @@ class Game:
async def print_grid(self) -> None:
"""Formats and outputs the Connect Four grid to the channel."""
title = (
- f'Connect 4: {self.player1.display_name}'
- f' VS {self.bot.user.display_name if isinstance(self.player2, AI) else self.player2.display_name}'
+ f"Connect 4: {self.player1.display_name}"
+ f" VS {self.bot.user.display_name if isinstance(self.player2, AI) else self.player2.display_name}"
)
rows = [" ".join(self.tokens[s] for s in row) for row in self.grid]
@@ -66,7 +67,7 @@ class Game:
if self.message:
await self.message.edit(embed=embed)
else:
- self.message = await self.channel.send(content='Loading...')
+ self.message = await self.channel.send(content="Loading...")
for emoji in self.unicode_numbers:
await self.message.add_reaction(emoji)
await self.message.add_reaction(CROSS_EMOJI)
@@ -180,7 +181,7 @@ class Game:
class AI:
"""The Computer Player for Single-Player games."""
- def __init__(self, bot: commands.Bot, game: Game) -> None:
+ def __init__(self, bot: Bot, game: Game) -> None:
self.game = game
self.mention = bot.user.mention
@@ -255,7 +256,7 @@ class AI:
class ConnectFour(commands.Cog):
"""Connect Four. The Classic Vertical Four-in-a-row Game!"""
- def __init__(self, bot: commands.Bot) -> None:
+ def __init__(self, bot: Bot) -> None:
self.bot = bot
self.games: typing.List[Game] = []
self.waiting: typing.List[discord.Member] = []
@@ -276,27 +277,29 @@ class ConnectFour(commands.Cog):
return False
if not self.min_board_size <= board_size <= self.max_board_size:
- await ctx.send(f"{board_size} is not a valid board size. A valid board size is "
- f"between `{self.min_board_size}` and `{self.max_board_size}`.")
+ await ctx.send(
+ f"{board_size} is not a valid board size. A valid board size is "
+ f"between `{self.min_board_size}` and `{self.max_board_size}`."
+ )
return False
return True
def get_player(
- self,
- ctx: commands.Context,
- announcement: discord.Message,
- reaction: discord.Reaction,
- user: discord.Member
+ self,
+ ctx: commands.Context,
+ announcement: discord.Message,
+ reaction: discord.Reaction,
+ user: discord.Member
) -> bool:
"""Predicate checking the criteria for the announcement message."""
if self.already_playing(ctx.author): # If they've joined a game since requesting a player 2
return True # Is dealt with later on
if (
- user.id not in (ctx.me.id, ctx.author.id)
- and str(reaction.emoji) == Emojis.hand_raised
- and reaction.message.id == announcement.id
+ user.id not in (ctx.me.id, ctx.author.id)
+ and str(reaction.emoji) == Emojis.hand_raised
+ and reaction.message.id == announcement.id
):
if self.already_playing(user):
self.bot.loop.create_task(ctx.send(f"{user.mention} You're already playing a game!"))
@@ -313,9 +316,9 @@ class ConnectFour(commands.Cog):
return True
if (
- user.id == ctx.author.id
- and str(reaction.emoji) == CROSS_EMOJI
- and reaction.message.id == announcement.id
+ user.id == ctx.author.id
+ and str(reaction.emoji) == CROSS_EMOJI
+ and reaction.message.id == announcement.id
):
return True
return False
@@ -326,7 +329,7 @@ class ConnectFour(commands.Cog):
@staticmethod
def check_emojis(
- e1: EMOJI_CHECK, e2: EMOJI_CHECK
+ e1: EMOJI_CHECK, e2: EMOJI_CHECK
) -> typing.Tuple[bool, typing.Optional[str]]:
"""Validate the emojis, the user put."""
if isinstance(e1, str) and emojis.count(e1) != 1:
@@ -336,12 +339,12 @@ class ConnectFour(commands.Cog):
return True, None
async def _play_game(
- self,
- ctx: commands.Context,
- user: typing.Optional[discord.Member],
- board_size: int,
- emoji1: str,
- emoji2: str
+ self,
+ ctx: commands.Context,
+ user: typing.Optional[discord.Member],
+ board_size: int,
+ emoji1: str,
+ emoji2: str
) -> None:
"""Helper for playing a game of connect four."""
self.tokens = [":white_circle:", str(emoji1), str(emoji2)]
@@ -354,7 +357,7 @@ class ConnectFour(commands.Cog):
self.games.remove(game)
except Exception:
# End the game in the event of an unforeseen error so the players aren't stuck in a game
- await ctx.send(f"{ctx.author.mention} {user.mention if user else ''} An error occurred. Game failed")
+ await ctx.send(f"{ctx.author.mention} {user.mention if user else ''} An error occurred. Game failed.")
if game in self.games:
self.games.remove(game)
raise
@@ -362,15 +365,15 @@ class ConnectFour(commands.Cog):
@guild_only()
@commands.group(
invoke_without_command=True,
- aliases=["4inarow", "connect4", "connectfour", "c4"],
+ aliases=("4inarow", "connect4", "connectfour", "c4"),
case_insensitive=True
)
async def connect_four(
- self,
- ctx: commands.Context,
- board_size: int = 7,
- emoji1: EMOJI_CHECK = "\U0001f535",
- emoji2: EMOJI_CHECK = "\U0001f534"
+ self,
+ ctx: commands.Context,
+ board_size: int = 7,
+ emoji1: EMOJI_CHECK = "\U0001f535",
+ emoji2: EMOJI_CHECK = "\U0001f534"
) -> None:
"""
Play the classic game of Connect Four with someone!
@@ -425,13 +428,13 @@ class ConnectFour(commands.Cog):
await self._play_game(ctx, user, board_size, str(emoji1), str(emoji2))
@guild_only()
- @connect_four.command(aliases=["bot", "computer", "cpu"])
+ @connect_four.command(aliases=("bot", "computer", "cpu"))
async def ai(
- self,
- ctx: commands.Context,
- board_size: int = 7,
- emoji1: EMOJI_CHECK = "\U0001f535",
- emoji2: EMOJI_CHECK = "\U0001f534"
+ self,
+ ctx: commands.Context,
+ board_size: int = 7,
+ emoji1: EMOJI_CHECK = "\U0001f535",
+ emoji2: EMOJI_CHECK = "\U0001f534"
) -> None:
"""Play Connect Four against a computer player."""
check, emoji = self.check_emojis(emoji1, emoji2)
@@ -445,6 +448,6 @@ class ConnectFour(commands.Cog):
await self._play_game(ctx, None, board_size, str(emoji1), str(emoji2))
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load ConnectFour Cog."""
bot.add_cog(ConnectFour(bot))
diff --git a/bot/exts/evergreen/conversationstarters.py b/bot/exts/evergreen/conversationstarters.py
index e7058961..fdc4467a 100644
--- a/bot/exts/evergreen/conversationstarters.py
+++ b/bot/exts/evergreen/conversationstarters.py
@@ -4,11 +4,12 @@ import yaml
from discord import Color, Embed
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import WHITELISTED_CHANNELS
from bot.utils.decorators import whitelist_override
from bot.utils.randomization import RandomCycle
-SUGGESTION_FORM = 'https://forms.gle/zw6kkJqv8U43Nfjg9'
+SUGGESTION_FORM = "https://forms.gle/zw6kkJqv8U43Nfjg9"
with Path("bot/resources/evergreen/starter.yaml").open("r", encoding="utf8") as f:
STARTERS = yaml.load(f, Loader=yaml.FullLoader)
@@ -24,9 +25,9 @@ with Path("bot/resources/evergreen/py_topics.yaml").open("r", encoding="utf8") a
ALL_ALLOWED_CHANNELS = list(PY_TOPICS.keys()) + list(WHITELISTED_CHANNELS)
# Putting all topics into one dictionary and shuffling lists to reduce same-topic repetitions.
-ALL_TOPICS = {'default': STARTERS, **PY_TOPICS}
+ALL_TOPICS = {"default": STARTERS, **PY_TOPICS}
TOPICS = {
- channel: RandomCycle(topics or ['No topics found for this channel.'])
+ channel: RandomCycle(topics or ["No topics found for this channel."])
for channel, topics in ALL_TOPICS.items()
}
@@ -34,9 +35,6 @@ TOPICS = {
class ConvoStarters(commands.Cog):
"""Evergreen conversation topics."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@commands.command()
@whitelist_override(channels=ALL_ALLOWED_CHANNELS)
async def topic(self, ctx: commands.Context) -> None:
@@ -48,7 +46,7 @@ class ConvoStarters(commands.Cog):
Otherwise, a random conversation topic will be received by the user.
"""
# No matter what, the form will be shown.
- embed = Embed(description=f'Suggest more topics [here]({SUGGESTION_FORM})!', color=Color.blurple())
+ embed = Embed(description=f"Suggest more topics [here]({SUGGESTION_FORM})!", color=Color.blurple())
try:
# Fetching topics.
@@ -56,16 +54,16 @@ class ConvoStarters(commands.Cog):
# If the channel isn't Python-related.
except KeyError:
- embed.title = f'**{next(TOPICS["default"])}**'
+ embed.title = f"**{next(TOPICS['default'])}**"
# If the channel ID doesn't have any topics.
else:
- embed.title = f'**{next(channel_topics)}**'
+ embed.title = f"**{next(channel_topics)}**"
finally:
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Conversation starters Cog load."""
- bot.add_cog(ConvoStarters(bot))
+def setup(bot: Bot) -> None:
+ """Load the ConvoStarters cog."""
+ bot.add_cog(ConvoStarters())
diff --git a/bot/exts/evergreen/emoji.py b/bot/exts/evergreen/emoji.py
index fa3044e3..11615214 100644
--- a/bot/exts/evergreen/emoji.py
+++ b/bot/exts/evergreen/emoji.py
@@ -8,6 +8,7 @@ from typing import List, Optional, Tuple
from discord import Color, Embed, Emoji
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours, ERROR_REPLIES
from bot.utils.extensions import invoke_help_command
from bot.utils.pagination import LinePaginator
@@ -19,9 +20,6 @@ log = logging.getLogger(__name__)
class Emojis(commands.Cog):
"""A collection of commands related to emojis in the server."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@staticmethod
def embed_builder(emoji: dict) -> Tuple[Embed, List[str]]:
"""Generates an embed with the emoji names and count."""
@@ -48,9 +46,9 @@ class Emojis(commands.Cog):
else:
emoji_info = f"There is **{len(category_emojis)}** emoji in the **{category_name}** category."
if emoji_choice.animated:
- msg.append(f'<a:{emoji_choice.name}:{emoji_choice.id}> {emoji_info}')
+ msg.append(f"<a:{emoji_choice.name}:{emoji_choice.id}> {emoji_info}")
else:
- msg.append(f'<:{emoji_choice.name}:{emoji_choice.id}> {emoji_info}')
+ msg.append(f"<:{emoji_choice.name}:{emoji_choice.id}> {emoji_info}")
return embed, msg
@staticmethod
@@ -66,7 +64,7 @@ class Emojis(commands.Cog):
for emoji in emojis:
emoji_dict[emoji.name.split("_")[0]].append(emoji)
- error_comp = ', '.join(emoji_dict)
+ error_comp = ", ".join(emoji_dict)
msg.append(f"These are the valid emoji categories:\n```{error_comp}```")
return embed, msg
@@ -86,7 +84,7 @@ class Emojis(commands.Cog):
if not ctx.guild.emojis:
await ctx.send("No emojis found.")
return
- log.trace(f"Emoji Category {'' if category_query else 'not '}provided by the user")
+ log.trace(f"Emoji Category {'' if category_query else 'not '}provided by the user.")
for emoji in ctx.guild.emojis:
emoji_category = emoji.name.split("_")[0]
@@ -120,6 +118,6 @@ class Emojis(commands.Cog):
await ctx.send(embed=emoji_information)
-def setup(bot: commands.Bot) -> None:
- """Add the Emojis cog into the bot."""
- bot.add_cog(Emojis(bot))
+def setup(bot: Bot) -> None:
+ """Load the Emojis cog."""
+ bot.add_cog(Emojis())
diff --git a/bot/exts/evergreen/error_handler.py b/bot/exts/evergreen/error_handler.py
index 8db49748..de8e53d0 100644
--- a/bot/exts/evergreen/error_handler.py
+++ b/bot/exts/evergreen/error_handler.py
@@ -7,6 +7,7 @@ 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
from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure
from bot.utils.exceptions import UserNotPlayingError
@@ -17,9 +18,6 @@ log = logging.getLogger(__name__)
class CommandErrorHandler(commands.Cog):
"""A error handler for the PythonDiscord server."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@staticmethod
def revert_cooldown_counter(command: commands.Command, message: Message) -> None:
"""Undoes the last cooldown counter for user-error cases."""
@@ -41,8 +39,8 @@ class CommandErrorHandler(commands.Cog):
@commands.Cog.listener()
async def on_command_error(self, ctx: commands.Context, error: commands.CommandError) -> None:
- """Activates when a command opens an error."""
- if getattr(error, 'handled', False):
+ """Activates when a command raises an error."""
+ if getattr(error, "handled", False):
logging.debug(f"Command {ctx.command} had its error already handled locally; ignoring.")
return
@@ -51,7 +49,7 @@ class CommandErrorHandler(commands.Cog):
parent_command = f"{ctx.command} "
ctx = subctx
- error = getattr(error, 'original', error)
+ error = getattr(error, "original", error)
logging.debug(
f"Error Encountered: {type(error).__name__} - {str(error)}, "
f"Command: {ctx.command}, "
@@ -127,14 +125,11 @@ class CommandErrorHandler(commands.Cog):
scope.set_extra("full_message", ctx.message.content)
if ctx.guild is not None:
- scope.set_extra(
- "jump_to",
- f"https://discordapp.com/channels/{ctx.guild.id}/{ctx.channel.id}/{ctx.message.id}"
- )
+ scope.set_extra("jump_to", ctx.message.jump_url)
log.exception(f"Unhandled command error: {str(error)}", exc_info=error)
-def setup(bot: commands.Bot) -> None:
- """Error handler Cog load."""
- bot.add_cog(CommandErrorHandler(bot))
+def setup(bot: Bot) -> None:
+ """Load the ErrorHandler cog."""
+ bot.add_cog(CommandErrorHandler())
diff --git a/bot/exts/evergreen/fun.py b/bot/exts/evergreen/fun.py
index 101725da..3b266e1b 100644
--- a/bot/exts/evergreen/fun.py
+++ b/bot/exts/evergreen/fun.py
@@ -7,10 +7,12 @@ from typing import Callable, Iterable, Tuple, Union
from discord import Embed, Message
from discord.ext import commands
-from discord.ext.commands import BadArgument, Bot, Cog, Context, MessageConverter, clean_content
+from discord.ext.commands import BadArgument, Cog, Context, MessageConverter, clean_content
from bot import utils
+from bot.bot import Bot
from bot.constants import Client, Colours, Emojis
+from bot.utils import helpers
log = logging.getLogger(__name__)
@@ -54,8 +56,7 @@ class Fun(Cog):
def __init__(self, bot: Bot) -> None:
self.bot = bot
- with Path("bot/resources/evergreen/caesar_info.json").open("r", encoding="UTF-8") as f:
- self._caesar_cipher_embed = json.load(f)
+ self._caesar_cipher_embed = json.loads(Path("bot/resources/evergreen/caesar_info.json").read_text("UTF-8"))
@staticmethod
def _get_random_die() -> str:
@@ -83,6 +84,7 @@ class Fun(Cog):
if embed is not None:
embed = Fun._convert_embed(conversion_func, embed)
converted_text = conversion_func(text)
+ converted_text = helpers.suppress_links(converted_text)
# Don't put >>> if only embed present
if converted_text:
converted_text = f">>> {converted_text.lstrip('> ')}"
@@ -101,6 +103,7 @@ class Fun(Cog):
if embed is not None:
embed = Fun._convert_embed(conversion_func, embed)
converted_text = conversion_func(text)
+ converted_text = helpers.suppress_links(converted_text)
# Don't put >>> if only embed present
if converted_text:
converted_text = f">>> {converted_text.lstrip('> ')}"
@@ -239,6 +242,6 @@ class Fun(Cog):
return Embed.from_dict(embed_dict)
-def setup(bot: commands.Bot) -> None:
- """Fun Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Fun cog."""
bot.add_cog(Fun(bot))
diff --git a/bot/exts/evergreen/game.py b/bot/exts/evergreen/game.py
index 068d3f68..32fe9263 100644
--- a/bot/exts/evergreen/game.py
+++ b/bot/exts/evergreen/game.py
@@ -176,7 +176,7 @@ class Games(Cog):
"Invalid OAuth credentials. Unloading Games cog. "
f"OAuth response message: {result['message']}"
)
- self.bot.remove_cog('Games')
+ self.bot.remove_cog("Games")
return
@@ -224,8 +224,8 @@ class Games(Cog):
else:
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) -> None:
+ @group(name="games", aliases=("game",), invoke_without_command=True)
+ async def games(self, ctx: Context, amount: Optional[int] = 5, *, genre: Optional[str]) -> None:
"""
Get random game(s) by genre from IGDB. Use .games genres command to get all available genres.
@@ -277,7 +277,7 @@ class Games(Cog):
await ImagePaginator.paginate(pages, ctx, Embed(title=f"Random {genre.title()} Games"))
- @games.command(name="top", aliases=["t"])
+ @games.command(name="top", aliases=("t",))
async def top(self, ctx: Context, amount: int = 10) -> None:
"""
Get current Top games in IGDB.
@@ -294,19 +294,19 @@ class Games(Cog):
pages = [await self.create_page(game) for game in games]
await ImagePaginator.paginate(pages, ctx, Embed(title=f"Top {amount} Games"))
- @games.command(name="genres", aliases=["genre", "g"])
+ @games.command(name="genres", aliases=("genre", "g"))
async def genres(self, ctx: Context) -> None:
"""Get all available genres."""
await ctx.send(f"Currently available genres: {', '.join(f'`{genre}`' for genre in self.genres)}")
- @games.command(name="search", aliases=["s"])
+ @games.command(name="search", aliases=("s",))
async def search(self, ctx: Context, *, search_term: str) -> None:
"""Find games by name."""
lines = await self.search_games(search_term)
await LinePaginator.paginate(lines, ctx, Embed(title=f"Game Search Results: {search_term}"), empty=False)
- @games.command(name="company", aliases=["companies"])
+ @games.command(name="company", aliases=("companies",))
async def company(self, ctx: Context, amount: int = 5) -> None:
"""
Get random Game Companies companies from IGDB API.
@@ -325,7 +325,7 @@ class Games(Cog):
await ImagePaginator.paginate(pages, ctx, Embed(title="Random Game Companies"))
@with_role(*STAFF_ROLES)
- @games.command(name="refresh", aliases=["r"])
+ @games.command(name="refresh", aliases=("r",))
async def refresh_genres_command(self, ctx: Context) -> None:
"""Refresh .games command genres."""
try:
@@ -335,13 +335,14 @@ class Games(Cog):
return
await ctx.send("Successfully refreshed genres.")
- async def get_games_list(self,
- amount: int,
- genre: Optional[str] = None,
- sort: Optional[str] = None,
- additional_body: str = "",
- offset: int = 0
- ) -> List[Dict[str, Any]]:
+ async def get_games_list(
+ self,
+ amount: int,
+ genre: Optional[str] = None,
+ sort: Optional[str] = None,
+ additional_body: str = "",
+ offset: int = 0
+ ) -> List[Dict[str, Any]]:
"""
Get list of games from IGDB API by parameters that is provided.
@@ -373,8 +374,10 @@ class Games(Cog):
release_date = dt.utcfromtimestamp(data["first_release_date"]).date() if "first_release_date" in data else "?"
# Create Age Ratings value
- rating = ", ".join(f"{AgeRatingCategories(age['category']).name} {AgeRatings(age['rating']).name}"
- for age in data["age_ratings"]) if "age_ratings" in data else "?"
+ rating = ", ".join(
+ f"{AgeRatingCategories(age['category']).name} {AgeRatings(age['rating']).name}"
+ for age in data["age_ratings"]
+ ) if "age_ratings" in data else "?"
companies = [c["company"]["name"] for c in data["involved_companies"]] if "involved_companies" in data else "?"
@@ -471,7 +474,7 @@ class Games(Cog):
def setup(bot: Bot) -> None:
- """Add/Load Games cog."""
+ """Load the Games cog."""
# Check does IGDB API key exist, if not, log warning and don't load cog
if not Tokens.igdb_client_id:
logger.warning("No IGDB client ID. Not loading Games cog.")
diff --git a/bot/exts/evergreen/githubinfo.py b/bot/exts/evergreen/githubinfo.py
index c8a6b3f7..27e607e5 100644
--- a/bot/exts/evergreen/githubinfo.py
+++ b/bot/exts/evergreen/githubinfo.py
@@ -5,8 +5,8 @@ from urllib.parse import quote
import discord
from discord.ext import commands
-from discord.ext.commands.cooldowns import BucketType
+from bot.bot import Bot
from bot.constants import Colours, NEGATIVE_REPLIES
from bot.exts.utils.extensions import invoke_help_command
@@ -18,7 +18,7 @@ GITHUB_API_URL = "https://api.github.com"
class GithubInfo(commands.Cog):
"""Fetches info from GitHub."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
async def fetch_data(self, url: str) -> dict:
@@ -26,14 +26,14 @@ class GithubInfo(commands.Cog):
async with self.bot.http_session.get(url) as r:
return await r.json()
- @commands.group(name='github', aliases=('gh', 'git'))
- @commands.cooldown(1, 10, BucketType.user)
+ @commands.group(name="github", aliases=("gh", "git"))
+ @commands.cooldown(1, 10, commands.BucketType.user)
async def github_group(self, ctx: commands.Context) -> None:
"""Commands for finding information related to GitHub."""
if ctx.invoked_subcommand is None:
await invoke_help_command(ctx)
- @github_group.command(name='user', aliases=('userinfo',))
+ @github_group.command(name="user", aliases=("userinfo",))
async def github_user_info(self, ctx: commands.Context, username: str) -> None:
"""Fetches a user's GitHub information."""
async with ctx.typing():
@@ -50,31 +50,31 @@ class GithubInfo(commands.Cog):
await ctx.send(embed=embed)
return
- org_data = await self.fetch_data(user_data['organizations_url'])
+ org_data = await self.fetch_data(user_data["organizations_url"])
orgs = [f"[{org['login']}](https://github.com/{org['login']})" for org in org_data]
- orgs_to_add = ' | '.join(orgs)
+ orgs_to_add = " | ".join(orgs)
- gists = user_data['public_gists']
+ gists = user_data["public_gists"]
# Forming blog link
- if user_data['blog'].startswith("http"): # Blog link is complete
- blog = user_data['blog']
- elif user_data['blog']: # Blog exists but the link is not complete
+ if user_data["blog"].startswith("http"): # Blog link is complete
+ blog = user_data["blog"]
+ elif user_data["blog"]: # Blog exists but the link is not complete
blog = f"https://{user_data['blog']}"
else:
blog = "No website link available"
embed = discord.Embed(
title=f"`{user_data['login']}`'s GitHub profile info",
- description=f"```{user_data['bio']}```\n" if user_data['bio'] is not None else "",
+ description=f"```{user_data['bio']}```\n" if user_data["bio"] else "",
colour=discord.Colour.blurple(),
- url=user_data['html_url'],
- timestamp=datetime.strptime(user_data['created_at'], "%Y-%m-%dT%H:%M:%SZ")
+ url=user_data["html_url"],
+ timestamp=datetime.strptime(user_data["created_at"], "%Y-%m-%dT%H:%M:%SZ")
)
- embed.set_thumbnail(url=user_data['avatar_url'])
+ embed.set_thumbnail(url=user_data["avatar_url"])
embed.set_footer(text="Account created at")
- if user_data['type'] == "User":
+ if user_data["type"] == "User":
embed.add_field(
name="Followers",
@@ -90,12 +90,12 @@ class GithubInfo(commands.Cog):
value=f"[{user_data['public_repos']}]({user_data['html_url']}?tab=repositories)"
)
- if user_data['type'] == "User":
+ if user_data["type"] == "User":
embed.add_field(name="Gists", value=f"[{gists}](https://gist.github.com/{quote(username, safe='')})")
embed.add_field(
name=f"Organization{'s' if len(orgs)!=1 else ''}",
- value=orgs_to_add if orgs else "No organizations"
+ value=orgs_to_add if orgs else "No organizations."
)
embed.add_field(name="Website", value=blog)
@@ -108,8 +108,8 @@ class GithubInfo(commands.Cog):
The repository should look like `user/reponame` or `user reponame`.
"""
- repo = '/'.join(repo)
- if repo.count('/') != 1:
+ repo = "/".join(repo)
+ if repo.count("/") != 1:
embed = discord.Embed(
title=random.choice(NEGATIVE_REPLIES),
description="The repository should look like `user/reponame` or `user reponame`.",
@@ -134,10 +134,10 @@ class GithubInfo(commands.Cog):
return
embed = discord.Embed(
- title=repo_data['name'],
+ title=repo_data["name"],
description=repo_data["description"],
colour=discord.Colour.blurple(),
- url=repo_data['html_url']
+ url=repo_data["html_url"]
)
# If it's a fork, then it will have a parent key
@@ -147,7 +147,7 @@ class GithubInfo(commands.Cog):
except KeyError:
log.debug("Repository is not a fork.")
- repo_owner = repo_data['owner']
+ repo_owner = repo_data["owner"]
embed.set_author(
name=repo_owner["login"],
@@ -155,8 +155,8 @@ 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").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")
embed.set_footer(
text=(
@@ -170,6 +170,6 @@ class GithubInfo(commands.Cog):
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Adding the cog to the bot."""
+def setup(bot: Bot) -> None:
+ """Load the GithubInfo cog."""
bot.add_cog(GithubInfo(bot))
diff --git a/bot/exts/evergreen/help.py b/bot/exts/evergreen/help.py
index 91147243..3c9ba4d2 100644
--- a/bot/exts/evergreen/help.py
+++ b/bot/exts/evergreen/help.py
@@ -2,9 +2,8 @@
import asyncio
import itertools
import logging
-from collections import namedtuple
from contextlib import suppress
-from typing import Union
+from typing import List, NamedTuple, Union
from discord import Colour, Embed, HTTPException, Message, Reaction, User
from discord.ext import commands
@@ -22,14 +21,21 @@ from bot.utils.pagination import (
DELETE_EMOJI = Emojis.trashcan
REACTIONS = {
- FIRST_EMOJI: 'first',
- LEFT_EMOJI: 'back',
- RIGHT_EMOJI: 'next',
- LAST_EMOJI: 'end',
- DELETE_EMOJI: 'stop',
+ FIRST_EMOJI: "first",
+ LEFT_EMOJI: "back",
+ RIGHT_EMOJI: "next",
+ LAST_EMOJI: "end",
+ DELETE_EMOJI: "stop",
}
-Cog = namedtuple('Cog', ['name', 'description', 'commands'])
+
+class Cog(NamedTuple):
+ """Show information about a Cog's name, description and commands."""
+
+ name: str
+ description: str
+ commands: List[Command]
+
log = logging.getLogger(__name__)
@@ -87,7 +93,7 @@ class HelpSession:
# set the query details for the session
if command:
- query_str = ' '.join(command)
+ query_str = " ".join(command)
self.query = self._get_query(query_str)
self.description = self.query.description or self.query.help
else:
@@ -191,7 +197,7 @@ class HelpSession:
self.reset_timeout()
# Run relevant action method
- action = getattr(self, f'do_{REACTIONS[emoji]}', None)
+ action = getattr(self, f"do_{REACTIONS[emoji]}", None)
if action:
await action()
@@ -234,11 +240,11 @@ class HelpSession:
if cmd.cog:
try:
if cmd.cog.category:
- return f'**{cmd.cog.category}**'
+ return f"**{cmd.cog.category}**"
except AttributeError:
pass
- return f'**{cmd.cog_name}**'
+ return f"**{cmd.cog_name}**"
else:
return "**\u200bNo Category:**"
@@ -262,139 +268,143 @@ class HelpSession:
# if default is not an empty string or None
if show_default:
- results.append(f'[{name}={param.default}]')
+ results.append(f"[{name}={param.default}]")
else:
- results.append(f'[{name}]')
+ results.append(f"[{name}]")
# if variable length argument
elif param.kind == param.VAR_POSITIONAL:
- results.append(f'[{name}...]')
+ results.append(f"[{name}...]")
# if required
else:
- results.append(f'<{name}>')
+ results.append(f"<{name}>")
return f"{cmd.name} {' '.join(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."""
# Use LinePaginator to restrict embed line height
- paginator = LinePaginator(prefix='', suffix='', max_lines=self._max_lines)
-
- prefix = constants.Client.prefix
+ paginator = LinePaginator(prefix="", suffix="", max_lines=self._max_lines)
# show signature if query is a command
if isinstance(self.query, commands.Command):
- signature = self._get_command_params(self.query)
- parent = self.query.full_parent_name + ' ' if self.query.parent else ''
- paginator.add_line(f'**```{prefix}{parent}{signature}```**')
-
- aliases = ', '.join(f'`{a}`' for a in self.query.aliases)
- if aliases:
- paginator.add_line(f'**Can also use:** {aliases}\n')
-
- if not await self.query.can_run(self._ctx):
- paginator.add_line('***You cannot run this command.***\n')
+ await self._add_command_signature(paginator)
if isinstance(self.query, Cog):
- paginator.add_line(f'**{self.query.name}**')
+ paginator.add_line(f"**{self.query.name}**")
if self.description:
- paginator.add_line(f'*{self.description}*')
+ paginator.add_line(f"*{self.description}*")
# list all children commands of the queried object
if isinstance(self.query, (commands.GroupMixin, Cog)):
+ await self._list_child_commands(paginator)
- # remove hidden commands if session is not wanting hiddens
- if not self._show_hidden:
- filtered = [c for c in self.query.commands if not c.hidden]
- else:
- filtered = self.query.commands
-
- # if after filter there are no commands, finish up
- if not filtered:
- self._pages = paginator.pages
- return
-
- if isinstance(self.query, Cog):
- grouped = (('**Commands:**', self.query.commands),)
-
- elif isinstance(self.query, commands.Command):
- grouped = (('**Subcommands:**', self.query.commands),)
-
- # don't show prefix for subcommands
- prefix = ''
+ self._pages = paginator.pages
- # otherwise sort and organise all commands into categories
- else:
- cat_sort = sorted(filtered, key=self._category_key)
- grouped = itertools.groupby(cat_sort, key=self._category_key)
+ async def _add_command_signature(self, paginator: LinePaginator) -> None:
+ prefix = constants.Client.prefix
- for category, cmds in grouped:
- cmds = sorted(cmds, key=lambda c: c.name)
+ signature = self._get_command_params(self.query)
+ parent = self.query.full_parent_name + " " if self.query.parent else ""
+ paginator.add_line(f"**```{prefix}{parent}{signature}```**")
+ aliases = [f"`{alias}`" if not parent else f"`{parent} {alias}`" for alias in self.query.aliases]
+ aliases += [f"`{alias}`" for alias in getattr(self.query, "root_aliases", ())]
+ aliases = ", ".join(sorted(aliases))
+ if aliases:
+ paginator.add_line(f"**Can also use:** {aliases}\n")
+ if not await self.query.can_run(self._ctx):
+ paginator.add_line("***You cannot run this command.***\n")
+
+ async def _list_child_commands(self, paginator: LinePaginator) -> None:
+ # remove hidden commands if session is not wanting hiddens
+ if not self._show_hidden:
+ filtered = [c for c in self.query.commands if not c.hidden]
+ else:
+ filtered = self.query.commands
- if len(cmds) == 0:
- continue
+ # if after filter there are no commands, finish up
+ if not filtered:
+ self._pages = paginator.pages
+ return
- cat_cmds = []
+ if isinstance(self.query, Cog):
+ grouped = (("**Commands:**", self.query.commands),)
- for command in cmds:
+ elif isinstance(self.query, commands.Command):
+ grouped = (("**Subcommands:**", self.query.commands),)
- # skip if hidden and hide if session is set to
- if command.hidden and not self._show_hidden:
- continue
+ # otherwise sort and organise all commands into categories
+ else:
+ cat_sort = sorted(filtered, key=self._category_key)
+ grouped = itertools.groupby(cat_sort, key=self._category_key)
- # see if the user can run the command
- strikeout = ''
+ for category, cmds in grouped:
+ await self._format_command_category(paginator, category, list(cmds))
- # Patch to make the !help command work outside of #bot-commands again
- # This probably needs a proper rewrite, but this will make it work in
- # the mean time.
- try:
- can_run = await command.can_run(self._ctx)
- except CheckFailure:
- can_run = False
+ async def _format_command_category(self, paginator: LinePaginator, category: str, cmds: List[Command]) -> None:
+ cmds = sorted(cmds, key=lambda c: c.name)
+ cat_cmds = []
+ for command in cmds:
+ cat_cmds += await self._format_command(command)
- if not can_run:
- # skip if we don't show commands they can't run
- if self._only_can_run:
- continue
- strikeout = '~~'
+ # state var for if the category should be added next
+ print_cat = 1
+ new_page = True
- signature = self._get_command_params(command)
- info = f"{strikeout}**`{prefix}{signature}`**{strikeout}"
+ for details in cat_cmds:
- # handle if the command has no docstring
- if command.short_doc:
- cat_cmds.append(f'{info}\n*{command.short_doc}*')
- else:
- cat_cmds.append(f'{info}\n*No details provided.*')
+ # keep details together, paginating early if it won"t fit
+ lines_adding = len(details.split("\n")) + print_cat
+ if paginator._linecount + lines_adding > self._max_lines:
+ paginator._linecount = 0
+ new_page = True
+ paginator.close_page()
- # state var for if the category should be added next
+ # new page so print category title again
print_cat = 1
- new_page = True
- for details in cat_cmds:
+ if print_cat:
+ if new_page:
+ paginator.add_line("")
+ paginator.add_line(category)
+ print_cat = 0
+
+ paginator.add_line(details)
- # keep details together, paginating early if it won't fit
- lines_adding = len(details.split('\n')) + print_cat
- if paginator._linecount + lines_adding > self._max_lines:
- paginator._linecount = 0
- new_page = True
- paginator.close_page()
+ async def _format_command(self, command: Command) -> List[str]:
+ # skip if hidden and hide if session is set to
+ if command.hidden and not self._show_hidden:
+ return []
- # new page so print category title again
- print_cat = 1
+ # Patch to make the !help command work outside of #bot-commands again
+ # This probably needs a proper rewrite, but this will make it work in
+ # the mean time.
+ try:
+ can_run = await command.can_run(self._ctx)
+ except CheckFailure:
+ can_run = False
+
+ # see if the user can run the command
+ strikeout = ""
+ if not can_run:
+ # skip if we don't show commands they can't run
+ if self._only_can_run:
+ return []
+ strikeout = "~~"
- if print_cat:
- if new_page:
- paginator.add_line('')
- paginator.add_line(category)
- print_cat = 0
+ if isinstance(self.query, commands.Command):
+ prefix = ""
+ else:
+ prefix = constants.Client.prefix
- paginator.add_line(details)
+ signature = self._get_command_params(command)
+ info = f"{strikeout}**`{prefix}{signature}`**{strikeout}"
- self._pages = paginator.pages
+ # handle if the command has no docstring
+ short_doc = command.short_doc or "No details provided"
+ return [f"{info}\n*{short_doc}*"]
def embed_page(self, page_number: int = 0) -> Embed:
"""Returns an Embed with the requested page formatted within."""
@@ -410,7 +420,7 @@ class HelpSession:
page_count = len(self._pages)
if page_count > 1:
- embed.set_footer(text=f'Page {self._current_page+1} / {page_count}')
+ embed.set_footer(text=f"Page {self._current_page+1} / {page_count}")
return embed
@@ -494,7 +504,7 @@ class HelpSession:
class Help(DiscordCog):
"""Custom Embed Pagination Help feature."""
- @commands.command('help')
+ @commands.command("help")
async def new_help(self, ctx: Context, *commands) -> None:
"""Shows Command Help."""
try:
@@ -505,8 +515,8 @@ class Help(DiscordCog):
embed.title = str(error)
if error.possible_matches:
- matches = '\n'.join(error.possible_matches.keys())
- embed.description = f'**Did you mean:**\n`{matches}`'
+ matches = "\n".join(error.possible_matches.keys())
+ embed.description = f"**Did you mean:**\n`{matches}`"
await ctx.send(embed=embed)
@@ -517,7 +527,7 @@ def unload(bot: Bot) -> None:
This is run if the cog raises an exception on load, or if the extension is unloaded.
"""
- bot.remove_command('help')
+ bot.remove_command("help")
bot.add_command(bot._old_help)
@@ -532,8 +542,8 @@ def setup(bot: Bot) -> None:
If an exception is raised during the loading of the cog, `unload` will be called in order to
reinstate the original help command.
"""
- bot._old_help = bot.get_command('help')
- bot.remove_command('help')
+ bot._old_help = bot.get_command("help")
+ bot.remove_command("help")
try:
bot.add_cog(Help())
diff --git a/bot/exts/evergreen/issues.py b/bot/exts/evergreen/issues.py
index a0316080..b67aa4a6 100644
--- a/bot/exts/evergreen/issues.py
+++ b/bot/exts/evergreen/issues.py
@@ -7,6 +7,7 @@ from dataclasses import dataclass
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import (
Categories,
Channels,
@@ -91,7 +92,7 @@ class IssueState:
class Issues(commands.Cog):
"""Cog that allows users to retrieve issues from GitHub."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
self.repos = []
@@ -157,13 +158,13 @@ class Issues(commands.Cog):
issue_url = json_data.get("html_url")
- return IssueState(repository, number, issue_url, json_data.get('title', ''), emoji)
+ return IssueState(repository, number, issue_url, json_data.get("title", ""), emoji)
@staticmethod
def format_embed(
- results: t.List[t.Union[IssueState, FetchError]],
- user: str,
- repository: t.Optional[str] = None
+ results: t.List[t.Union[IssueState, FetchError]],
+ user: str,
+ repository: t.Optional[str] = None
) -> discord.Embed:
"""Take a list of IssueState or FetchError and format a Discord embed for them."""
description_list = []
@@ -176,7 +177,7 @@ class Issues(commands.Cog):
resp = discord.Embed(
colour=Colours.bright_green,
- description='\n'.join(description_list)
+ description="\n".join(description_list)
)
embed_url = f"https://github.com/{user}/{repository}" if repository else f"https://github.com/{user}"
@@ -186,11 +187,11 @@ class Issues(commands.Cog):
@whitelist_override(channels=WHITELISTED_CHANNELS, categories=WHITELISTED_CATEGORIES)
@commands.command(aliases=("pr",))
async def issue(
- self,
- ctx: commands.Context,
- numbers: commands.Greedy[int],
- repository: str = "sir-lancebot",
- user: str = "python-discord"
+ self,
+ ctx: commands.Context,
+ numbers: commands.Greedy[int],
+ repository: str = "sir-lancebot",
+ user: str = "python-discord"
) -> None:
"""Command to retrieve issue(s) from a GitHub repository."""
# Remove duplicates
@@ -269,6 +270,6 @@ class Issues(commands.Cog):
await message.channel.send(embed=resp)
-def setup(bot: commands.Bot) -> None:
- """Cog Retrieves Issues From Github."""
+def setup(bot: Bot) -> None:
+ """Load the Issues cog."""
bot.add_cog(Issues(bot))
diff --git a/bot/exts/evergreen/latex.py b/bot/exts/evergreen/latex.py
index c4a8597c..36c7e0ab 100644
--- a/bot/exts/evergreen/latex.py
+++ b/bot/exts/evergreen/latex.py
@@ -9,6 +9,8 @@ import discord
import matplotlib.pyplot as plt
from discord.ext import commands
+from bot.bot import Bot
+
# configure fonts and colors for matplotlib
plt.rcParams.update(
{
@@ -89,6 +91,11 @@ class Latex(commands.Cog):
await ctx.send(file=discord.File(image, "latex.png"))
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Latex Cog."""
- bot.add_cog(Latex(bot))
+ # As we have resource issues on this cog,
+ # we have it currently disabled while we fix it.
+ import logging
+ logging.info("Latex cog is currently disabled. It won't be loaded.")
+ return
+ bot.add_cog(Latex())
diff --git a/bot/exts/evergreen/magic_8ball.py b/bot/exts/evergreen/magic_8ball.py
index f974e487..28ddcea0 100644
--- a/bot/exts/evergreen/magic_8ball.py
+++ b/bot/exts/evergreen/magic_8ball.py
@@ -5,27 +5,26 @@ from pathlib import Path
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
+ANSWERS = json.loads(Path("bot/resources/evergreen/magic8ball.json").read_text("utf8"))
+
class Magic8ball(commands.Cog):
"""A Magic 8ball command to respond to a user's question."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- with open(Path("bot/resources/evergreen/magic8ball.json"), "r", encoding="utf8") as file:
- self.answers = json.load(file)
-
@commands.command(name="8ball")
async def output_answer(self, ctx: commands.Context, *, question: str) -> None:
"""Return a Magic 8ball answer from answers list."""
if len(question.split()) >= 3:
- answer = random.choice(self.answers)
+ answer = random.choice(ANSWERS)
await ctx.send(answer)
else:
await ctx.send("Usage: .8ball <question> (minimum length of 3 eg: `will I win?`)")
-def setup(bot: commands.Bot) -> None:
- """Magic 8ball Cog load."""
- bot.add_cog(Magic8ball(bot))
+def setup(bot: Bot) -> None:
+ """Load the Magic8Ball Cog."""
+ bot.add_cog(Magic8ball())
diff --git a/bot/exts/evergreen/minesweeper.py b/bot/exts/evergreen/minesweeper.py
index 3031debc..932358f9 100644
--- a/bot/exts/evergreen/minesweeper.py
+++ b/bot/exts/evergreen/minesweeper.py
@@ -6,7 +6,9 @@ from random import randint, random
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Client
+from bot.utils.converters import CoordinateConverter
from bot.utils.exceptions import UserNotPlayingError
from bot.utils.extensions import invoke_help_command
@@ -31,33 +33,6 @@ MESSAGE_MAPPING = {
log = logging.getLogger(__name__)
-class CoordinateConverter(commands.Converter):
- """Converter for Coordinates."""
-
- async def convert(self, ctx: commands.Context, coordinate: str) -> typing.Tuple[int, int]:
- """Take in a coordinate string and turn it into an (x, y) tuple."""
- if not 2 <= len(coordinate) <= 3:
- raise commands.BadArgument('Invalid co-ordinate provided')
-
- coordinate = coordinate.lower()
- if coordinate[0].isalpha():
- digit = coordinate[1:]
- letter = coordinate[0]
- else:
- digit = coordinate[:-1]
- letter = coordinate[-1]
-
- if not digit.isdigit():
- raise commands.BadArgument
-
- x = ord(letter) - ord('a')
- y = int(digit) - 1
-
- if (not 0 <= x <= 9) or (not 0 <= y <= 9):
- raise commands.BadArgument
- return x, y
-
-
GameBoard = typing.List[typing.List[typing.Union[str, int]]]
@@ -78,10 +53,10 @@ GamesDict = typing.Dict[int, Game]
class Minesweeper(commands.Cog):
"""Play a game of Minesweeper."""
- def __init__(self, bot: commands.Bot) -> None:
+ def __init__(self) -> None:
self.games: GamesDict = {} # Store the currently running games
- @commands.group(name='minesweeper', aliases=('ms',), invoke_without_command=True)
+ @commands.group(name="minesweeper", aliases=("ms",), invoke_without_command=True)
async def minesweeper_group(self, ctx: commands.Context) -> None:
"""Commands for Playing Minesweeper."""
await invoke_help_command(ctx)
@@ -148,7 +123,7 @@ class Minesweeper(commands.Cog):
f"Close the game with `{Client.prefix}ms end`\n"
)
except discord.errors.Forbidden:
- log.debug(f"{ctx.author.name} ({ctx.author.id}) has disabled DMs from server members")
+ log.debug(f"{ctx.author.name} ({ctx.author.id}) has disabled DMs from server members.")
await ctx.send(f":x: {ctx.author.mention}, please enable DMs to play minesweeper.")
return
@@ -158,7 +133,7 @@ class Minesweeper(commands.Cog):
dm_msg = await ctx.author.send(f"Here's your board!\n{self.format_for_discord(revealed_board)}")
if ctx.guild:
- await ctx.send(f"{ctx.author.mention} is playing Minesweeper")
+ await ctx.send(f"{ctx.author.mention} is playing Minesweeper.")
chat_msg = await ctx.send(f"Here's their board!\n{self.format_for_discord(revealed_board)}")
else:
chat_msg = None
@@ -237,17 +212,17 @@ class Minesweeper(commands.Cog):
return True
async def reveal_one(
- self,
- ctx: commands.Context,
- revealed: GameBoard,
- board: GameBoard,
- x: int,
- y: int
+ self,
+ ctx: commands.Context,
+ revealed: GameBoard,
+ board: GameBoard,
+ x: int,
+ y: int
) -> bool:
"""
Reveal one square.
- return is True if the game ended, breaking the loop in `reveal_command` and deleting the game
+ return is True if the game ended, breaking the loop in `reveal_command` and deleting the game.
"""
revealed[y][x] = board[y][x]
if board[y][x] == "bomb":
@@ -285,13 +260,13 @@ class Minesweeper(commands.Cog):
game = self.games[ctx.author.id]
game.revealed = game.board
await self.update_boards(ctx)
- new_msg = f":no_entry: Game canceled :no_entry:\n{game.dm_msg.content}"
+ new_msg = f":no_entry: Game canceled. :no_entry:\n{game.dm_msg.content}"
await game.dm_msg.edit(content=new_msg)
if game.activated_on_server:
await game.chat_msg.edit(content=new_msg)
del self.games[ctx.author.id]
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Minesweeper cog."""
- bot.add_cog(Minesweeper(bot))
+ bot.add_cog(Minesweeper())
diff --git a/bot/exts/evergreen/movie.py b/bot/exts/evergreen/movie.py
index b3bfe998..10638aea 100644
--- a/bot/exts/evergreen/movie.py
+++ b/bot/exts/evergreen/movie.py
@@ -6,8 +6,9 @@ from urllib.parse import urlencode
from aiohttp import ClientSession
from discord import Embed
-from discord.ext.commands import Bot, Cog, Context, group
+from discord.ext.commands import Cog, Context, group
+from bot.bot import Bot
from bot.constants import Tokens
from bot.utils.extensions import invoke_help_command
from bot.utils.pagination import ImagePaginator
@@ -50,10 +51,9 @@ class Movie(Cog):
"""Movie Cog contains movies command that grab random movies from TMDB."""
def __init__(self, bot: Bot):
- self.bot = bot
self.http_session: ClientSession = bot.http_session
- @group(name='movies', aliases=['movie'], invoke_without_command=True)
+ @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.
@@ -72,15 +72,17 @@ class Movie(Cog):
# Capitalize genre for getting data from Enum, get random page, send help when genre don't exist.
genre = genre.capitalize()
try:
- result = await self.get_movies_list(self.http_session, MovieGenres[genre].value, 1)
+ result = await self.get_movies_data(self.http_session, MovieGenres[genre].value, 1)
except KeyError:
await invoke_help_command(ctx)
return
# Check if "results" is in result. If not, throw error.
- if "results" not in result.keys():
- err_msg = f"There is problem while making TMDB API request. Response Code: {result['status_code']}, " \
- f"{result['status_message']}."
+ if "results" not in result:
+ err_msg = (
+ f"There is problem while making TMDB API request. Response Code: {result['status_code']}, "
+ f"{result['status_message']}."
+ )
await ctx.send(err_msg)
logger.warning(err_msg)
@@ -88,8 +90,8 @@ class Movie(Cog):
page = random.randint(1, result["total_pages"])
# Get movies list from TMDB, check if results key in result. When not, raise error.
- movies = await self.get_movies_list(self.http_session, MovieGenres[genre].value, page)
- if 'results' not in movies.keys():
+ movies = await self.get_movies_data(self.http_session, MovieGenres[genre].value, page)
+ if "results" not in movies:
err_msg = f"There is problem while making TMDB API request. Response Code: {result['status_code']}, " \
f"{result['status_message']}."
await ctx.send(err_msg)
@@ -101,12 +103,12 @@ class Movie(Cog):
await ImagePaginator.paginate(pages, ctx, embed)
- @movies.command(name='genres', aliases=['genre', 'g'])
+ @movies.command(name="genres", aliases=("genre", "g"))
async def genres(self, ctx: Context) -> None:
"""Show all currently available genres for .movies command."""
await ctx.send(f"Current available genres: {', '.join('`' + genre.name + '`' for genre in MovieGenres)}")
- async def get_movies_list(self, client: ClientSession, genre_id: str, page: int) -> Dict[str, Any]:
+ async def get_movies_data(self, client: ClientSession, genre_id: str, page: int) -> List[Dict[str, Any]]:
"""Return JSON of TMDB discover request."""
# Define params of request
params = {
@@ -130,7 +132,7 @@ class Movie(Cog):
pages = []
for i in range(amount):
- movie_id = movies['results'][i]['id']
+ movie_id = movies["results"][i]["id"]
movie = await self.get_movie(client, movie_id)
page, img = await self.create_page(movie)
@@ -151,7 +153,7 @@ class Movie(Cog):
# Add title + tagline (if not empty)
text += f"**{movie['title']}**\n"
- if movie['tagline']:
+ if movie["tagline"]:
text += f"{movie['tagline']}\n\n"
else:
text += "\n"
@@ -162,8 +164,8 @@ class Movie(Cog):
text += "__**Production Information**__\n"
- companies = movie['production_companies']
- countries = movie['production_countries']
+ companies = movie["production_companies"]
+ countries = movie["production_countries"]
text += f"**Made by:** {', '.join(company['name'] for company in companies)}\n"
text += f"**Made in:** {', '.join(country['name'] for country in countries)}\n\n"
@@ -173,8 +175,8 @@ class Movie(Cog):
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)
+ if movie["runtime"] is not None:
+ duration = divmod(movie["runtime"], 60)
else:
duration = ("?", "?")
@@ -182,7 +184,7 @@ class Movie(Cog):
text += f"**Revenue:** ${revenue}\n"
text += f"**Duration:** {f'{duration[0]} hour(s) {duration[1]} minute(s)'}\n\n"
- text += movie['overview']
+ text += movie["overview"]
img = f"http://image.tmdb.org/t/p/w200{movie['poster_path']}"
@@ -198,5 +200,5 @@ class Movie(Cog):
def setup(bot: Bot) -> None:
- """Load Movie Cog."""
+ """Load the Movie Cog."""
bot.add_cog(Movie(bot))
diff --git a/bot/exts/evergreen/ping.py b/bot/exts/evergreen/ping.py
index 97f8b34d..6be78117 100644
--- a/bot/exts/evergreen/ping.py
+++ b/bot/exts/evergreen/ping.py
@@ -1,13 +1,17 @@
+import arrow
+from dateutil.relativedelta import relativedelta
from discord import Embed
from discord.ext import commands
+from bot import start_time
+from bot.bot import Bot
from bot.constants import Colours
class Ping(commands.Cog):
- """Ping the bot to see its latency and state."""
+ """Get info about the bot's ping and uptime."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@commands.command(name="ping")
@@ -21,7 +25,21 @@ class Ping(commands.Cog):
await ctx.send(embed=embed)
+ # Originally made in 70d2170a0a6594561d59c7d080c4280f1ebcd70b by lemon & gdude2002
+ @commands.command(name="uptime")
+ async def uptime(self, ctx: commands.Context) -> None:
+ """Get the current uptime of the bot."""
+ difference = relativedelta(start_time - arrow.utcnow())
+ uptime_string = start_time.shift(
+ seconds=-difference.seconds,
+ minutes=-difference.minutes,
+ hours=-difference.hours,
+ days=-difference.days
+ ).humanize()
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
+ await ctx.send(f"I started up {uptime_string}.")
+
+
+def setup(bot: Bot) -> None:
+ """Load the Ping cog."""
bot.add_cog(Ping(bot))
diff --git a/bot/exts/evergreen/pythonfacts.py b/bot/exts/evergreen/pythonfacts.py
index 457c2fd3..80a8da5d 100644
--- a/bot/exts/evergreen/pythonfacts.py
+++ b/bot/exts/evergreen/pythonfacts.py
@@ -3,31 +3,34 @@ import itertools
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
-with open('bot/resources/evergreen/python_facts.txt') as file:
+with open("bot/resources/evergreen/python_facts.txt") as file:
FACTS = itertools.cycle(list(file))
COLORS = itertools.cycle([Colours.python_blue, Colours.python_yellow])
+PYFACTS_DISCUSSION = "https://github.com/python-discord/meta/discussions/93"
class PythonFacts(commands.Cog):
"""Sends a random fun fact about Python."""
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
-
- @commands.command(name='pythonfact', aliases=['pyfact'])
+ @commands.command(name="pythonfact", aliases=("pyfact",))
async def get_python_fact(self, ctx: commands.Context) -> None:
"""Sends a Random fun fact about Python."""
- embed = discord.Embed(title='Python Facts',
- description=next(FACTS),
- colour=next(COLORS))
- embed.add_field(name='Suggestions',
- value="Suggest more facts [here!](https://github.com/python-discord/meta/discussions/93)")
+ embed = discord.Embed(
+ title="Python Facts",
+ description=next(FACTS),
+ colour=next(COLORS)
+ )
+ embed.add_field(
+ name="Suggestions",
+ value=f"Suggest more facts [here!]({PYFACTS_DISCUSSION})"
+ )
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Load PythonFacts Cog."""
- bot.add_cog(PythonFacts(bot))
+def setup(bot: Bot) -> None:
+ """Load the PythonFacts Cog."""
+ bot.add_cog(PythonFacts())
diff --git a/bot/exts/evergreen/recommend_game.py b/bot/exts/evergreen/recommend_game.py
index 5e262a5b..35d60128 100644
--- a/bot/exts/evergreen/recommend_game.py
+++ b/bot/exts/evergreen/recommend_game.py
@@ -6,13 +6,14 @@ from random import shuffle
import discord
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
game_recs = []
# Populate the list `game_recs` with resource files
for rec_path in Path("bot/resources/evergreen/game_recs").glob("*.json"):
- with rec_path.open(encoding='utf8') as file:
- data = json.load(file)
+ data = json.loads(rec_path.read_text("utf8"))
game_recs.append(data)
shuffle(game_recs)
@@ -20,11 +21,11 @@ shuffle(game_recs)
class RecommendGame(commands.Cog):
"""Commands related to recommending games."""
- def __init__(self, bot: commands.Bot) -> None:
+ def __init__(self, bot: Bot) -> None:
self.bot = bot
self.index = 0
- @commands.command(name="recommendgame", aliases=['gamerec'])
+ @commands.command(name="recommendgame", aliases=("gamerec",))
async def recommend_game(self, ctx: commands.Context) -> None:
"""Sends an Embed of a random game recommendation."""
if self.index >= len(game_recs):
@@ -33,18 +34,18 @@ class RecommendGame(commands.Cog):
game = game_recs[self.index]
self.index += 1
- author = self.bot.get_user(int(game['author']))
+ author = self.bot.get_user(int(game["author"]))
# Creating and formatting Embed
embed = discord.Embed(color=discord.Colour.blue())
if author is not None:
embed.set_author(name=author.name, icon_url=author.avatar_url)
- embed.set_image(url=game['image'])
- embed.add_field(name='Recommendation: ' + game['title'] + '\n' + game['link'], value=game['description'])
+ embed.set_image(url=game["image"])
+ embed.add_field(name=f"Recommendation: {game['title']}\n{game['link']}", value=game["description"])
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Loads the RecommendGame cog."""
bot.add_cog(RecommendGame(bot))
diff --git a/bot/exts/evergreen/reddit.py b/bot/exts/evergreen/reddit.py
index 49127bea..e57fa2c0 100644
--- a/bot/exts/evergreen/reddit.py
+++ b/bot/exts/evergreen/reddit.py
@@ -1,128 +1,367 @@
+import asyncio
import logging
import random
+import textwrap
+from collections import namedtuple
+from datetime import datetime, timedelta
+from typing import List, Union
-import discord
-from discord.ext import commands
-from discord.ext.commands.cooldowns import BucketType
+from aiohttp import BasicAuth, ClientError
+from discord import Colour, Embed, TextChannel
+from discord.ext.commands import Cog, Context, group, has_any_role
+from discord.ext.tasks import loop
+from discord.utils import escape_markdown, sleep_until
-from bot.utils.pagination import ImagePaginator
+from bot.bot import Bot
+from bot.constants import Channels, ERROR_REPLIES, Emojis, Reddit as RedditConfig, STAFF_ROLES
+from bot.utils.converters import Subreddit
+from bot.utils.extensions import invoke_help_command
+from bot.utils.messages import sub_clyde
+from bot.utils.pagination import ImagePaginator, LinePaginator
log = logging.getLogger(__name__)
+AccessToken = namedtuple("AccessToken", ["token", "expires_at"])
-class Reddit(commands.Cog):
- """Fetches reddit posts."""
- def __init__(self, bot: commands.Bot):
+class Reddit(Cog):
+ """Track subreddit posts and show detailed statistics about them."""
+
+ HEADERS = {"User-Agent": "python3:python-discord/bot:1.0.0 (by /u/PythonDiscord)"}
+ URL = "https://www.reddit.com"
+ OAUTH_URL = "https://oauth.reddit.com"
+ MAX_RETRIES = 3
+
+ def __init__(self, bot: Bot):
self.bot = bot
- async def fetch(self, url: str) -> dict:
- """Send a get request to the reddit API and get json response."""
- session = self.bot.http_session
- params = {
- 'limit': 50
- }
- headers = {
- 'User-Agent': 'Iceman'
- }
-
- async with session.get(url=url, params=params, headers=headers) as response:
- return await response.json()
-
- @commands.command(name='reddit')
- @commands.cooldown(1, 10, BucketType.user)
- async def get_reddit(self, ctx: commands.Context, subreddit: str = 'python', sort: str = "hot") -> None:
- """
- Fetch reddit posts by using this command.
+ self.webhook = None
+ self.access_token = None
+ self.client_auth = BasicAuth(RedditConfig.client_id, RedditConfig.secret)
- Gets a post from r/python by default.
- Usage:
- --> .reddit [subreddit_name] [hot/top/new]
- """
+ bot.loop.create_task(self.init_reddit_ready())
+ self.auto_poster_loop.start()
+
+ 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())
+
+ async def init_reddit_ready(self) -> None:
+ """Sets the reddit webhook when the cog is loaded."""
+ await self.bot.wait_until_guild_available()
+ if not self.webhook:
+ self.webhook = await self.bot.fetch_webhook(RedditConfig.webhook)
+
+ @property
+ def channel(self) -> TextChannel:
+ """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]:
+ """Build embed pages required for Paginator."""
pages = []
- sort_list = ["hot", "new", "top", "rising"]
- if sort.lower() not in sort_list:
- await ctx.send(f"Invalid sorting: {sort}\nUsing default sorting: `Hot`")
- sort = "hot"
+ first_page = ""
+ for post in posts:
+ post_page = ""
+ image_url = ""
- data = await self.fetch(f'https://www.reddit.com/r/{subreddit}/{sort}/.json')
+ data = post["data"]
- try:
- posts = data["data"]["children"]
- except KeyError:
- return await ctx.send('Subreddit not found!')
- if not posts:
- return await ctx.send('No posts available!')
+ title = textwrap.shorten(data["title"], width=50, placeholder="...")
+
+ # Normal brackets interfere with Markdown.
+ title = escape_markdown(title).replace("[", "⦋").replace("]", "⦌")
+ link = self.URL + data["permalink"]
+
+ first_page += f"**[{title.replace('*', '')}]({link})**\n"
+
+ text = data["selftext"]
+ if text:
+ first_page += textwrap.shorten(text, width=100, placeholder="...").replace("*", "") + "\n"
+
+ ups = data["ups"]
+ comments = data["num_comments"]
+ author = data["author"]
+
+ content_type = Emojis.reddit_post_text
+ if data["is_video"] or {"youtube", "youtu.be"}.issubset(set(data["url"].split("."))):
+ # This means the content type in the post is a video.
+ content_type = f"{Emojis.reddit_post_video}"
+
+ elif data["url"].endswith(("jpg", "png", "gif")):
+ # This means the content type in the post is an image.
+ content_type = f"{Emojis.reddit_post_photo}"
+ image_url = data["url"]
- if posts[1]["data"]["over_18"] is True:
- return await ctx.send(
- "You cannot access this Subreddit as it is ment for those who "
- "are 18 years or older."
+ first_page += (
+ f"{content_type}\u2003{Emojis.reddit_upvote}{ups}\u2003{Emojis.reddit_comments}"
+ f"\u2002{comments}\u2003{Emojis.reddit_users}{author}\n\n"
)
- embed_titles = ""
+ if paginate:
+ post_page += f"**[{title}]({link})**\n\n"
+ if text:
+ post_page += textwrap.shorten(text, width=252, placeholder="...") + "\n\n"
+ post_page += (
+ f"{content_type}\u2003{Emojis.reddit_upvote}{ups}\u2003{Emojis.reddit_comments}\u2002"
+ f"{comments}\u2003{Emojis.reddit_users}{author}"
+ )
- # Chooses k unique random elements from a population sequence or set.
- random_posts = random.sample(posts, k=5)
+ pages.append((post_page, image_url))
- # -----------------------------------------------------------
- # This code below is bound of change when the emojis are added.
+ if not paginate:
+ # Return the first summery page if pagination is not required
+ return first_page
- upvote_emoji = self.bot.get_emoji(755845219890757644)
- comment_emoji = self.bot.get_emoji(755845255001014384)
- user_emoji = self.bot.get_emoji(755845303822974997)
- text_emoji = self.bot.get_emoji(676030265910493204)
- video_emoji = self.bot.get_emoji(676030265839190047)
- image_emoji = self.bot.get_emoji(676030265734201344)
- reddit_emoji = self.bot.get_emoji(676030265734332427)
+ pages.insert(0, (first_page, "")) # Using image paginator, hence settings image url to empty string
+ return pages
- # ------------------------------------------------------------
+ async def get_access_token(self) -> None:
+ """
+ Get a Reddit API OAuth2 access token and assign it to self.access_token.
- for i, post in enumerate(random_posts, start=1):
- post_title = post["data"]["title"][0:50]
- post_url = post['data']['url']
- if post_title == "":
- post_title = "No Title."
- elif post_title == post_url:
- post_title = "Title is itself a link."
+ A token is valid for 1 hour. There will be MAX_RETRIES to get a token, after which the cog
+ will be unloaded and a ClientError raised if retrieval was still unsuccessful.
+ """
+ for i in range(1, self.MAX_RETRIES + 1):
+ response = await self.bot.http_session.post(
+ url=f"{self.URL}/api/v1/access_token",
+ headers=self.HEADERS,
+ auth=self.client_auth,
+ data={
+ "grant_type": "client_credentials",
+ "duration": "temporary"
+ }
+ )
- # ------------------------------------------------------------------
- # Embed building.
+ if response.status == 200 and response.content_type == "application/json":
+ content = await response.json()
+ 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)
+ )
- embed_titles += f"**{i}.[{post_title}]({post_url})**\n"
- image_url = " "
- post_stats = f"{text_emoji}" # Set default content type to text.
+ 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})"
+ )
- if post["data"]["is_video"] is True or "youtube" in post_url.split("."):
- # This means the content type in the post is a video.
- post_stats = f"{video_emoji} "
+ await asyncio.sleep(3)
- elif post_url.endswith("jpg") or post_url.endswith("png") or post_url.endswith("gif"):
- # This means the content type in the post is an image.
- post_stats = f"{image_emoji} "
- image_url = post_url
-
- votes = f'{upvote_emoji}{post["data"]["ups"]}'
- comments = f'{comment_emoji}\u2002{ post["data"]["num_comments"]}'
- post_stats += (
- f"\u2002{votes}\u2003"
- f"{comments}"
- f'\u2003{user_emoji}\u2002{post["data"]["author"]}\n'
+ self.bot.remove_cog(self.qualified_name)
+ raise ClientError("Authentication with the Reddit API failed. Unloading the cog.")
+
+ async def revoke_access_token(self) -> None:
+ """
+ Revoke the OAuth2 access token for the Reddit API.
+
+ For security reasons, it's good practice to revoke the token when it's no longer being used.
+ """
+ response = await self.bot.http_session.post(
+ url=f"{self.URL}/api/v1/revoke_token",
+ headers=self.HEADERS,
+ auth=self.client_auth,
+ data={
+ "token": self.access_token.token,
+ "token_type_hint": "access_token"
+ }
+ )
+
+ if response.status in [200, 204] and response.content_type == "application/json":
+ self.access_token = None
+ else:
+ log.warning(f"Unable to revoke access token: status {response.status}.")
+
+ async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None) -> List[dict]:
+ """A helper method to fetch a certain amount of Reddit posts at a given route."""
+ # Reddit's JSON responses only provide 25 posts at most.
+ if not 25 >= amount > 0:
+ 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():
+ await self.get_access_token()
+
+ url = f"{self.OAUTH_URL}/{route}"
+ for _ in range(self.MAX_RETRIES):
+ response = await self.bot.http_session.get(
+ url=url,
+ headers={**self.HEADERS, "Authorization": f"bearer {self.access_token.token}"},
+ params=params
+ )
+ if response.status == 200 and response.content_type == 'application/json':
+ # Got appropriate response - process and return.
+ content = await response.json()
+ posts = content["data"]["children"]
+
+ filtered_posts = [post for post in posts if not post["data"]["over_18"]]
+
+ return filtered_posts[:amount]
+
+ 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.
+
+ async def get_top_posts(
+ self, subreddit: Subreddit, time: str = "all", amount: int = 5, paginate: bool = False
+ ) -> Union[Embed, List[tuple]]:
+ """
+ Get the top amount of posts for a given subreddit within a specified timeframe.
+
+ A time of "all" will get posts from all time, "day" will get top daily posts and "week" will get the top
+ weekly posts.
+
+ The amount should be between 0 and 25 as Reddit's JSON requests only provide 25 posts at most.
+ """
+ embed = Embed()
+
+ posts = await self.fetch_posts(
+ route=f"{subreddit}/top",
+ amount=amount,
+ params={"t": time}
+ )
+ if not posts:
+ embed.title = random.choice(ERROR_REPLIES)
+ embed.colour = Colour.red()
+ embed.description = (
+ "Sorry! We couldn't find any SFW posts from that subreddit. "
+ "If this problem persists, please let us know."
)
- embed_titles += f"{post_stats}\n"
- page_text = f"**[{post_title}]({post_url})**\n{post_stats}\n{post['data']['selftext'][0:200]}"
- embed = discord.Embed()
- page_tuple = (page_text, image_url)
- pages.append(page_tuple)
+ return embed
+
+ if paginate:
+ return self.build_pagination_pages(posts, paginate=True)
+
+ # Use only starting summary page for #reddit channel posts.
+ embed.description = self.build_pagination_pages(posts, paginate=False)
+ embed.colour = Colour.blurple()
+ return embed
+
+ @loop()
+ async def auto_poster_loop(self) -> None:
+ """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()
+ tomorrow = now + timedelta(days=1)
+ midnight_tomorrow = tomorrow.replace(hour=0, minute=0, second=0)
+
+ await sleep_until(midnight_tomorrow)
+
+ await self.bot.wait_until_guild_available()
+ if not self.webhook:
+ await self.bot.fetch_webhook(RedditConfig.webhook)
+
+ if datetime.utcnow().weekday() == 0:
+ await self.top_weekly_posts()
+ # if it's a monday send the top weekly posts
+
+ for subreddit in RedditConfig.subreddits:
+ top_posts = await self.get_top_posts(subreddit=subreddit, time="day")
+ username = sub_clyde(f"{subreddit} Top Daily Posts")
+ message = await self.webhook.send(username=username, embed=top_posts, wait=True)
+
+ if message.channel.is_news():
+ await message.publish()
+
+ async def top_weekly_posts(self) -> None:
+ """Post a summary of the top posts."""
+ for subreddit in RedditConfig.subreddits:
+ # Send and pin the new weekly posts.
+ top_posts = await self.get_top_posts(subreddit=subreddit, time="week")
+ username = sub_clyde(f"{subreddit} Top Weekly Posts")
+ message = await self.webhook.send(wait=True, username=username, embed=top_posts)
- # ------------------------------------------------------------------
+ if subreddit.lower() == "r/python":
+ if not self.channel:
+ log.warning("Failed to get #reddit channel to remove pins in the weekly loop.")
+ return
+
+ # Remove the oldest pins so that only 12 remain at most.
+ pins = await self.channel.pins()
+
+ while len(pins) >= 12:
+ await pins[-1].unpin()
+ del pins[-1]
+
+ await message.pin()
+
+ if message.channel.is_news():
+ await message.publish()
+
+ @group(name="reddit", invoke_without_command=True)
+ async def reddit_group(self, ctx: Context) -> None:
+ """View the top posts from various subreddits."""
+ await invoke_help_command(ctx)
+
+ @reddit_group.command(name="top")
+ async def top_command(self, ctx: Context, subreddit: Subreddit = "r/Python") -> None:
+ """Send the top posts of all time from a given subreddit."""
+ async with ctx.typing():
+ pages = await self.get_top_posts(subreddit=subreddit, time="all", paginate=True)
+
+ await ctx.send(f"Here are the top {subreddit} posts of all time!")
+ embed = Embed(
+ color=Colour.blurple()
+ )
- pages.insert(0, (embed_titles, " "))
- embed.set_author(name=f"r/{posts[0]['data']['subreddit']} - {sort}", icon_url=reddit_emoji.url)
await ImagePaginator.paginate(pages, ctx, embed)
+ @reddit_group.command(name="daily")
+ async def daily_command(self, ctx: Context, subreddit: Subreddit = "r/Python") -> None:
+ """Send the top posts of today from a given subreddit."""
+ async with ctx.typing():
+ pages = await self.get_top_posts(subreddit=subreddit, time="day", paginate=True)
+
+ await ctx.send(f"Here are today's top {subreddit} posts!")
+ embed = Embed(
+ color=Colour.blurple()
+ )
+
+ await ImagePaginator.paginate(pages, ctx, embed)
+
+ @reddit_group.command(name="weekly")
+ async def weekly_command(self, ctx: Context, subreddit: Subreddit = "r/Python") -> None:
+ """Send the top posts of this week from a given subreddit."""
+ async with ctx.typing():
+ pages = await self.get_top_posts(subreddit=subreddit, time="week", paginate=True)
+
+ await ctx.send(f"Here are this week's top {subreddit} posts!")
+ embed = Embed(
+ color=Colour.blurple()
+ )
+
+ await ImagePaginator.paginate(pages, ctx, embed)
+
+ @has_any_role(*STAFF_ROLES)
+ @reddit_group.command(name="subreddits", aliases=("subs",))
+ async def subreddits_command(self, ctx: Context) -> None:
+ """Send a paginated embed of all the subreddits we're relaying."""
+ embed = Embed()
+ embed.title = "Relayed subreddits."
+ embed.colour = Colour.blurple()
+
+ await LinePaginator.paginate(
+ RedditConfig.subreddits,
+ ctx, embed,
+ footer_text="Use the reddit commands along with these to view their posts.",
+ empty=False,
+ max_lines=15
+ )
+
-def setup(bot: commands.Bot) -> None:
- """Load the Cog."""
+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.")
+ return
bot.add_cog(Reddit(bot))
diff --git a/bot/exts/evergreen/snakes/__init__.py b/bot/exts/evergreen/snakes/__init__.py
index bc42f0c2..7740429b 100644
--- a/bot/exts/evergreen/snakes/__init__.py
+++ b/bot/exts/evergreen/snakes/__init__.py
@@ -1,12 +1,11 @@
import logging
-from discord.ext import commands
-
+from bot.bot import Bot
from bot.exts.evergreen.snakes._snakes_cog import Snakes
log = logging.getLogger(__name__)
-def setup(bot: commands.Bot) -> None:
- """Snakes Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Snakes Cog."""
bot.add_cog(Snakes(bot))
diff --git a/bot/exts/evergreen/snakes/_converter.py b/bot/exts/evergreen/snakes/_converter.py
index eee248cf..26bde611 100644
--- a/bot/exts/evergreen/snakes/_converter.py
+++ b/bot/exts/evergreen/snakes/_converter.py
@@ -24,8 +24,8 @@ class Snake(Converter):
await self.build_list()
name = name.lower()
- if name == 'python':
- return 'Python (programming language)'
+ if name == "python":
+ return "Python (programming language)"
def get_potential(iterable: Iterable, *, threshold: int = 80) -> List[str]:
nonlocal name
@@ -47,12 +47,12 @@ class Snake(Converter):
if name.lower() in self.special_cases:
return self.special_cases.get(name.lower(), name.lower())
- names = {snake['name']: snake['scientific'] for snake in self.snakes}
+ names = {snake["name"]: snake["scientific"] for snake in self.snakes}
all_names = names.keys() | names.values()
timeout = len(all_names) * (3 / 4)
embed = discord.Embed(
- title='Found multiple choices. Please choose the correct one.', colour=0x59982F)
+ title="Found multiple choices. Please choose the correct one.", colour=0x59982F)
embed.set_author(name=ctx.author.display_name, icon_url=ctx.author.avatar_url)
name = await disambiguate(ctx, get_potential(all_names), timeout=timeout, embed=embed)
@@ -63,14 +63,11 @@ class Snake(Converter):
"""Build list of snakes from the static snake resources."""
# Get all the snakes
if cls.snakes is None:
- with (SNAKE_RESOURCES / "snake_names.json").open(encoding="utf8") as snakefile:
- cls.snakes = json.load(snakefile)
-
+ cls.snakes = json.loads((SNAKE_RESOURCES / "snake_names.json").read_text("utf8"))
# Get the special cases
if cls.special_cases is None:
- with (SNAKE_RESOURCES / "special_snakes.json").open(encoding="utf8") as snakefile:
- special_cases = json.load(snakefile)
- cls.special_cases = {snake['name'].lower(): snake for snake in special_cases}
+ special_cases = json.loads((SNAKE_RESOURCES / "special_snakes.json").read_text("utf8"))
+ cls.special_cases = {snake["name"].lower(): snake for snake in special_cases}
@classmethod
async def random(cls) -> str:
@@ -81,5 +78,5 @@ class Snake(Converter):
so I can get it from here.
"""
await cls.build_list()
- names = [snake['scientific'] for snake in cls.snakes]
+ names = [snake["scientific"] for snake in cls.snakes]
return random.choice(names)
diff --git a/bot/exts/evergreen/snakes/_snakes_cog.py b/bot/exts/evergreen/snakes/_snakes_cog.py
index 3732b559..07d3c363 100644
--- a/bot/exts/evergreen/snakes/_snakes_cog.py
+++ b/bot/exts/evergreen/snakes/_snakes_cog.py
@@ -9,15 +9,15 @@ import textwrap
import urllib
from functools import partial
from io import BytesIO
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional
-import aiohttp
import async_timeout
from PIL import Image, ImageDraw, ImageFont
from discord import Colour, Embed, File, Member, Message, Reaction
from discord.errors import HTTPException
-from discord.ext.commands import Bot, Cog, CommandError, Context, bot_has_permissions, group
+from discord.ext.commands import Cog, CommandError, Context, bot_has_permissions, group
+from bot.bot import Bot
from bot.constants import ERROR_REPLIES, Tokens
from bot.exts.evergreen.snakes import _utils as utils
from bot.exts.evergreen.snakes._converter import Snake
@@ -143,8 +143,8 @@ class Snakes(Cog):
https://github.com/python-discord/code-jam-1
"""
- wiki_brief = re.compile(r'(.*?)(=+ (.*?) =+)', flags=re.DOTALL)
- valid_image_extensions = ('gif', 'png', 'jpeg', 'jpg', 'webp')
+ wiki_brief = re.compile(r"(.*?)(=+ (.*?) =+)", flags=re.DOTALL)
+ valid_image_extensions = ("gif", "png", "jpeg", "jpg", "webp")
def __init__(self, bot: Bot):
self.active_sal = {}
@@ -183,28 +183,28 @@ class Snakes(Cog):
# Get the size of the snake icon, configure the height of the image box (yes, it changes)
icon_width = 347 # Hardcoded, not much i can do about that
icon_height = int((icon_width / snake.width) * snake.height)
- frame_copies = icon_height // CARD['frame'].height + 1
+ frame_copies = icon_height // CARD["frame"].height + 1
snake.thumbnail((icon_width, icon_height))
# Get the dimensions of the final image
- main_height = icon_height + CARD['top'].height + CARD['bottom'].height
- main_width = CARD['frame'].width
+ main_height = icon_height + CARD["top"].height + CARD["bottom"].height
+ main_width = CARD["frame"].width
# Start creating the foreground
foreground = Image.new("RGBA", (main_width, main_height), (0, 0, 0, 0))
- foreground.paste(CARD['top'], (0, 0))
+ foreground.paste(CARD["top"], (0, 0))
# Generate the frame borders to the correct height
for offset in range(frame_copies):
- position = (0, CARD['top'].height + offset * CARD['frame'].height)
- foreground.paste(CARD['frame'], position)
+ position = (0, CARD["top"].height + offset * CARD["frame"].height)
+ foreground.paste(CARD["frame"], position)
# Add the image and bottom part of the image
- foreground.paste(snake, (36, CARD['top'].height)) # Also hardcoded :(
- foreground.paste(CARD['bottom'], (0, CARD['top'].height + icon_height))
+ foreground.paste(snake, (36, CARD["top"].height)) # Also hardcoded :(
+ foreground.paste(CARD["bottom"], (0, CARD["top"].height + icon_height))
# Setup the background
- back = random.choice(CARD['backs'])
+ back = random.choice(CARD["backs"])
back_copies = main_height // back.height + 1
full_image = Image.new("RGBA", (main_width, main_height), (0, 0, 0, 0))
@@ -216,11 +216,11 @@ class Snakes(Cog):
full_image.paste(foreground, (0, 0), foreground)
# Get the first two sentences of the info
- description = '.'.join(content['info'].split(".")[:2]) + '.'
+ description = ".".join(content["info"].split(".")[:2]) + "."
# Setup positioning variables
margin = 36
- offset = CARD['top'].height + icon_height + margin
+ offset = CARD["top"].height + icon_height + margin
# Create blank rectangle image which will be behind the text
rectangle = Image.new(
@@ -242,12 +242,12 @@ class Snakes(Cog):
# Draw the text onto the final image
draw = ImageDraw.Draw(full_image)
for line in textwrap.wrap(description, 36):
- draw.text([margin + 4, offset], line, font=CARD['font'])
- offset += CARD['font'].getsize(line)[1]
+ draw.text([margin + 4, offset], line, font=CARD["font"])
+ offset += CARD["font"].getsize(line)[1]
# Get the image contents as a BufferIO object
buffer = BytesIO()
- full_image.save(buffer, 'PNG')
+ full_image.save(buffer, "PNG")
buffer.seek(0)
return buffer
@@ -275,13 +275,13 @@ class Snakes(Cog):
return message
- async def _fetch(self, session: aiohttp.ClientSession, url: str, params: dict = None) -> dict:
+ async def _fetch(self, url: str, params: Optional[dict] = None) -> dict:
"""Asynchronous web request helper method."""
if params is None:
params = {}
async with async_timeout.timeout(10):
- async with session.get(url, params=params) as response:
+ async with self.bot.http_session.get(url, params=params) as response:
return await response.json()
def _get_random_long_message(self, messages: List[str], retries: int = 10) -> str:
@@ -309,96 +309,95 @@ class Snakes(Cog):
"""
snake_info = {}
- async with aiohttp.ClientSession() as session:
- params = {
- 'format': 'json',
- 'action': 'query',
- 'list': 'search',
- 'srsearch': name,
- 'utf8': '',
- 'srlimit': '1',
- }
-
- json = await self._fetch(session, URL, params=params)
-
- # Wikipedia does have a error page
- try:
- pageid = json["query"]["search"][0]["pageid"]
- except KeyError:
- # Wikipedia error page ID(?)
- pageid = 41118
- except IndexError:
- return None
-
- params = {
- 'format': 'json',
- 'action': 'query',
- 'prop': 'extracts|images|info',
- 'exlimit': 'max',
- 'explaintext': '',
- 'inprop': 'url',
- 'pageids': pageid
- }
+ params = {
+ "format": "json",
+ "action": "query",
+ "list": "search",
+ "srsearch": name,
+ "utf8": "",
+ "srlimit": "1",
+ }
- json = await self._fetch(session, URL, params=params)
+ json = await self._fetch(URL, params=params)
- # Constructing dict - handle exceptions later
- try:
- snake_info["title"] = json["query"]["pages"][f"{pageid}"]["title"]
- snake_info["extract"] = json["query"]["pages"][f"{pageid}"]["extract"]
- snake_info["images"] = json["query"]["pages"][f"{pageid}"]["images"]
- snake_info["fullurl"] = json["query"]["pages"][f"{pageid}"]["fullurl"]
- snake_info["pageid"] = json["query"]["pages"][f"{pageid}"]["pageid"]
- except KeyError:
- snake_info["error"] = True
-
- if snake_info["images"]:
- i_url = 'https://commons.wikimedia.org/wiki/Special:FilePath/'
- image_list = []
- map_list = []
- thumb_list = []
-
- # Wikipedia has arbitrary images that are not snakes
- banned = [
- 'Commons-logo.svg',
- 'Red%20Pencil%20Icon.png',
- 'distribution',
- 'The%20Death%20of%20Cleopatra%20arthur.jpg',
- 'Head%20of%20holotype',
- 'locator',
- 'Woma.png',
- '-map.',
- '.svg',
- 'ange.',
- 'Adder%20(PSF).png'
- ]
-
- for image in snake_info["images"]:
- # Images come in the format of `File:filename.extension`
- file, sep, filename = image["title"].partition(':')
- filename = filename.replace(" ", "%20") # Wikipedia returns good data!
-
- if not filename.startswith('Map'):
- if any(ban in filename for ban in banned):
- pass
- else:
- image_list.append(f"{i_url}{filename}")
- thumb_list.append(f"{i_url}{filename}?width=100")
+ # Wikipedia does have a error page
+ try:
+ pageid = json["query"]["search"][0]["pageid"]
+ except KeyError:
+ # Wikipedia error page ID(?)
+ pageid = 41118
+ except IndexError:
+ return None
+
+ params = {
+ "format": "json",
+ "action": "query",
+ "prop": "extracts|images|info",
+ "exlimit": "max",
+ "explaintext": "",
+ "inprop": "url",
+ "pageids": pageid
+ }
+
+ json = await self._fetch(URL, params=params)
+
+ # Constructing dict - handle exceptions later
+ try:
+ snake_info["title"] = json["query"]["pages"][f"{pageid}"]["title"]
+ snake_info["extract"] = json["query"]["pages"][f"{pageid}"]["extract"]
+ snake_info["images"] = json["query"]["pages"][f"{pageid}"]["images"]
+ snake_info["fullurl"] = json["query"]["pages"][f"{pageid}"]["fullurl"]
+ snake_info["pageid"] = json["query"]["pages"][f"{pageid}"]["pageid"]
+ except KeyError:
+ snake_info["error"] = True
+
+ if snake_info["images"]:
+ i_url = "https://commons.wikimedia.org/wiki/Special:FilePath/"
+ image_list = []
+ map_list = []
+ thumb_list = []
+
+ # Wikipedia has arbitrary images that are not snakes
+ banned = [
+ "Commons-logo.svg",
+ "Red%20Pencil%20Icon.png",
+ "distribution",
+ "The%20Death%20of%20Cleopatra%20arthur.jpg",
+ "Head%20of%20holotype",
+ "locator",
+ "Woma.png",
+ "-map.",
+ ".svg",
+ "ange.",
+ "Adder%20(PSF).png"
+ ]
+
+ for image in snake_info["images"]:
+ # Images come in the format of `File:filename.extension`
+ file, sep, filename = image["title"].partition(":")
+ filename = filename.replace(" ", "%20") # Wikipedia returns good data!
+
+ if not filename.startswith("Map"):
+ if any(ban in filename for ban in banned):
+ pass
else:
- map_list.append(f"{i_url}{filename}")
+ image_list.append(f"{i_url}{filename}")
+ thumb_list.append(f"{i_url}{filename}?width=100")
+ else:
+ map_list.append(f"{i_url}{filename}")
- snake_info["image_list"] = image_list
- snake_info["map_list"] = map_list
- snake_info["thumb_list"] = thumb_list
- snake_info["name"] = name
+ snake_info["image_list"] = image_list
+ snake_info["map_list"] = map_list
+ snake_info["thumb_list"] = thumb_list
+ snake_info["name"] = name
- match = self.wiki_brief.match(snake_info['extract'])
- info = match.group(1) if match else None
+ match = self.wiki_brief.match(snake_info["extract"])
+ info = match.group(1) if match else None
- if info:
- info = info.replace("\n", "\n\n") # Give us some proper paragraphs.
+ if info:
+ info = info.replace("\n", "\n\n") # Give us some proper paragraphs.
- snake_info["info"] = info
+ snake_info["info"] = info
return snake_info
@@ -423,7 +422,7 @@ class Snakes(Cog):
try:
reaction, user = await ctx.bot.wait_for("reaction_add", timeout=45.0, check=predicate)
except asyncio.TimeoutError:
- await ctx.channel.send(f"You took too long. The correct answer was **{options[answer]}**.")
+ await ctx.send(f"You took too long. The correct answer was **{options[answer]}**.")
await message.clear_reactions()
return
@@ -438,13 +437,13 @@ class Snakes(Cog):
# endregion
# region: Commands
- @group(name='snakes', aliases=('snake',), invoke_without_command=True)
+ @group(name="snakes", aliases=("snake",), invoke_without_command=True)
async def snakes_group(self, ctx: Context) -> None:
"""Commands from our first code jam."""
await invoke_help_command(ctx)
@bot_has_permissions(manage_messages=True)
- @snakes_group.command(name='antidote')
+ @snakes_group.command(name="antidote")
@locked()
async def antidote_command(self, ctx: Context) -> None:
"""
@@ -498,9 +497,11 @@ class Snakes(Cog):
for i in range(0, 10):
page_guess_list.append(f"{HOLE_EMOJI} {HOLE_EMOJI} {HOLE_EMOJI} {HOLE_EMOJI}")
page_result_list.append(f"{CROSS_EMOJI} {CROSS_EMOJI} {CROSS_EMOJI} {CROSS_EMOJI}")
- board.append(f"`{i+1:02d}` "
- f"{page_guess_list[i]} - "
- f"{page_result_list[i]}")
+ board.append(
+ f"`{i+1:02d}` "
+ f"{page_guess_list[i]} - "
+ f"{page_result_list[i]}"
+ )
board.append(EMPTY_UNICODE)
antidote_embed.add_field(name="10 guesses remaining", value="\n".join(board))
board_id = await ctx.send(embed=antidote_embed) # Display board
@@ -578,15 +579,19 @@ class Snakes(Cog):
antidote_embed = Embed(color=SNAKE_COLOR, title="Antidote")
antidote_embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar_url)
antidote_embed.set_image(url="https://media.giphy.com/media/ceeN6U57leAhi/giphy.gif")
- antidote_embed.add_field(name=EMPTY_UNICODE,
- value=f"Sorry you didnt make the antidote in time.\n"
- f"The formula was {' '.join(antidote_answer)}")
+ antidote_embed.add_field(
+ name=EMPTY_UNICODE,
+ value=(
+ f"Sorry you didnt make the antidote in time.\n"
+ f"The formula was {' '.join(antidote_answer)}"
+ )
+ )
await board_id.edit(embed=antidote_embed)
log.debug("Ending pagination and removing all reactions...")
await board_id.clear_reactions()
- @snakes_group.command(name='draw')
+ @snakes_group.command(name="draw")
async def draw_command(self, ctx: Context) -> None:
"""
Draws a random snek using Perlin noise.
@@ -621,10 +626,10 @@ class Snakes(Cog):
bg_color=bg_color
)
png_bytes = utils.frame_to_png_bytes(image_frame)
- file = File(png_bytes, filename='snek.png')
+ file = File(png_bytes, filename="snek.png")
await ctx.send(file=file)
- @snakes_group.command(name='get')
+ @snakes_group.command(name="get")
@bot_has_permissions(manage_messages=True)
@locked()
async def get_command(self, ctx: Context, *, name: Snake = None) -> None:
@@ -642,8 +647,9 @@ class Snakes(Cog):
else:
data = await self._get_snek(name)
- if data.get('error'):
- return await ctx.send('Could not fetch data from Wikipedia.')
+ if data.get("error"):
+ await ctx.send("Could not fetch data from Wikipedia.")
+ return
description = data["info"]
@@ -661,19 +667,25 @@ class Snakes(Cog):
# Build and send the embed.
embed = Embed(
- title=data.get("title", data.get('name')),
+ title=data.get("title", data.get("name")),
description=description,
colour=0x59982F,
)
- emoji = 'https://emojipedia-us.s3.amazonaws.com/thumbs/60/google/3/snake_1f40d.png'
- image = next((url for url in data['image_list']
- if url.endswith(self.valid_image_extensions)), emoji)
+ emoji = "https://emojipedia-us.s3.amazonaws.com/thumbs/60/google/3/snake_1f40d.png"
+
+ _iter = (
+ url
+ for url in data["image_list"]
+ if url.endswith(self.valid_image_extensions)
+ )
+ image = next(_iter, emoji)
+
embed.set_image(url=image)
await ctx.send(embed=embed)
- @snakes_group.command(name='guess', aliases=('identify',))
+ @snakes_group.command(name="guess", aliases=("identify",))
@locked()
async def guess_command(self, ctx: Context) -> None:
"""
@@ -693,11 +705,15 @@ class Snakes(Cog):
data = await self._get_snek(snake)
- image = next((url for url in data['image_list']
- if url.endswith(self.valid_image_extensions)), None)
+ _iter = (
+ url
+ for url in data["image_list"]
+ if url.endswith(self.valid_image_extensions)
+ )
+ image = next(_iter, None)
embed = Embed(
- title='Which of the following is the snake in the image?',
+ title="Which of the following is the snake in the image?",
description="\n".join(
f"{'ABCD'[snakes.index(snake)]}: {snake}" for snake in snakes),
colour=SNAKE_COLOR
@@ -708,7 +724,7 @@ class Snakes(Cog):
options = {f"{'abcd'[snakes.index(snake)]}": snake for snake in snakes}
await self._validate_answer(ctx, guess, answer, options)
- @snakes_group.command(name='hatch')
+ @snakes_group.command(name="hatch")
async def hatch_command(self, ctx: Context) -> None:
"""
Hatches your personal snake.
@@ -720,7 +736,7 @@ class Snakes(Cog):
snake_image = utils.snakes[snake_name]
# Hatch the snake
- message = await ctx.channel.send(embed=Embed(description="Hatching your snake :snake:..."))
+ message = await ctx.send(embed=Embed(description="Hatching your snake :snake:..."))
await asyncio.sleep(1)
for stage in utils.stages:
@@ -734,12 +750,12 @@ class Snakes(Cog):
my_snake_embed = Embed(description=":tada: Congrats! You hatched: **{0}**".format(snake_name))
my_snake_embed.set_thumbnail(url=snake_image)
my_snake_embed.set_footer(
- text=" Owner: {0}#{1}".format(ctx.message.author.name, ctx.message.author.discriminator)
+ text=" Owner: {0}#{1}".format(ctx.author.name, ctx.author.discriminator)
)
- await ctx.channel.send(embed=my_snake_embed)
+ await ctx.send(embed=my_snake_embed)
- @snakes_group.command(name='movie')
+ @snakes_group.command(name="movie")
async def movie_command(self, ctx: Context) -> None:
"""
Gets a random snake-related movie from TMDB.
@@ -800,12 +816,12 @@ class Snakes(Cog):
embed.set_thumbnail(url="https://i.imgur.com/LtFtC8H.png")
try:
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
except HTTPException as err:
- await ctx.channel.send("An error occurred while fetching a snake-related movie!")
+ await ctx.send("An error occurred while fetching a snake-related movie!")
raise err from None
- @snakes_group.command(name='quiz')
+ @snakes_group.command(name="quiz")
@locked()
async def quiz_command(self, ctx: Context) -> None:
"""
@@ -828,10 +844,10 @@ class Snakes(Cog):
)
)
- quiz = await ctx.channel.send("", embed=embed)
+ quiz = await ctx.send(embed=embed)
await self._validate_answer(ctx, quiz, answer, options)
- @snakes_group.command(name='name', aliases=('name_gen',))
+ @snakes_group.command(name="name", aliases=("name_gen",))
async def name_command(self, ctx: Context, *, name: str = None) -> None:
"""
Snakifies a username.
@@ -855,7 +871,7 @@ class Snakes(Cog):
This was written by Iceman, and modified for inclusion into the bot by lemon.
"""
snake_name = await self._get_snake_name()
- snake_name = snake_name['name']
+ snake_name = snake_name["name"]
snake_prefix = ""
# Set aside every word in the snake name except the last.
@@ -900,9 +916,10 @@ class Snakes(Cog):
color=SNAKE_COLOR
)
- return await ctx.send(embed=embed)
+ await ctx.send(embed=embed)
+ return
- @snakes_group.command(name='sal')
+ @snakes_group.command(name="sal")
@locked()
async def sal_command(self, ctx: Context) -> None:
"""
@@ -921,7 +938,7 @@ class Snakes(Cog):
await game.open_game()
- @snakes_group.command(name='about')
+ @snakes_group.command(name="about")
async def about_command(self, ctx: Context) -> None:
"""Show an embed with information about the event, its participants, and its winners."""
contributors = [
@@ -964,9 +981,9 @@ class Snakes(Cog):
)
)
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
- @snakes_group.command(name='card')
+ @snakes_group.command(name="card")
async def card_command(self, ctx: Context, *, name: Snake = None) -> None:
"""
Create an interesting little card from a snake.
@@ -976,7 +993,7 @@ class Snakes(Cog):
# Get the snake data we need
if not name:
name_obj = await self._get_snake_name()
- name = name_obj['scientific']
+ name = name_obj["scientific"]
content = await self._get_snek(name)
elif isinstance(name, dict):
@@ -990,7 +1007,7 @@ class Snakes(Cog):
stream = BytesIO()
async with async_timeout.timeout(10):
- async with self.bot.http_session.get(content['image_list'][0]) as response:
+ async with self.bot.http_session.get(content["image_list"][0]) as response:
stream.write(await response.read())
stream.seek(0)
@@ -1001,10 +1018,10 @@ class Snakes(Cog):
# Send it!
await ctx.send(
f"A wild {content['name'].title()} appears!",
- file=File(final_buffer, filename=content['name'].replace(" ", "") + ".png")
+ file=File(final_buffer, filename=content["name"].replace(" ", "") + ".png")
)
- @snakes_group.command(name='fact')
+ @snakes_group.command(name="fact")
async def fact_command(self, ctx: Context) -> None:
"""
Gets a snake-related fact.
@@ -1018,9 +1035,9 @@ class Snakes(Cog):
color=SNAKE_COLOR,
description=question
)
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
- @snakes_group.command(name='snakify')
+ @snakes_group.command(name="snakify")
async def snakify_command(self, ctx: Context, *, message: str = None) -> None:
"""
How would I talk if I were a snake?
@@ -1033,14 +1050,14 @@ class Snakes(Cog):
"""
with ctx.typing():
embed = Embed()
- user = ctx.message.author
+ user = ctx.author
if not message:
# Get a random message from the users history
messages = []
- async for message in ctx.channel.history(limit=500).filter(
- lambda msg: msg.author == ctx.message.author # Message was sent by author.
+ async for message in ctx.history(limit=500).filter(
+ lambda msg: msg.author == ctx.author # Message was sent by author.
):
messages.append(message.content)
@@ -1059,9 +1076,9 @@ class Snakes(Cog):
)
embed.description = f"*{self._snakify(message)}*"
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
- @snakes_group.command(name='video', aliases=('get_video',))
+ @snakes_group.command(name="video", aliases=("get_video",))
async def video_command(self, ctx: Context, *, search: str = None) -> None:
"""
Gets a YouTube video about snakes.
@@ -1072,13 +1089,13 @@ class Snakes(Cog):
"""
# Are we searching for anything specific?
if search:
- query = search + ' snake'
+ query = search + " snake"
else:
snake = await self._get_snake_name()
- query = snake['name']
+ query = snake["name"]
# Build the URL and make the request
- url = 'https://www.googleapis.com/youtube/v3/search'
+ url = "https://www.googleapis.com/youtube/v3/search"
response = await self.bot.http_session.get(
url,
params={
@@ -1094,14 +1111,14 @@ class Snakes(Cog):
# Send the user a video
if len(data) > 0:
num = random.randint(0, len(data) - 1)
- youtube_base_url = 'https://www.youtube.com/watch?v='
- await ctx.channel.send(
+ youtube_base_url = "https://www.youtube.com/watch?v="
+ await ctx.send(
content=f"{youtube_base_url}{data[num]['id']['videoId']}"
)
else:
log.warning(f"YouTube API error. Full response looks like {response}")
- @snakes_group.command(name='zen')
+ @snakes_group.command(name="zen")
async def zen_command(self, ctx: Context) -> None:
"""
Gets a random quote from the Zen of Python, except as if spoken by a snake.
@@ -1120,7 +1137,7 @@ class Snakes(Cog):
# Embed and send
embed.description = zen_quote
- await ctx.channel.send(
+ await ctx.send(
embed=embed
)
# endregion
diff --git a/bot/exts/evergreen/snakes/_utils.py b/bot/exts/evergreen/snakes/_utils.py
index 7d6caf04..0a5894b7 100644
--- a/bot/exts/evergreen/snakes/_utils.py
+++ b/bot/exts/evergreen/snakes/_utils.py
@@ -17,38 +17,38 @@ from bot.constants import Roles
SNAKE_RESOURCES = Path("bot/resources/snakes").absolute()
-h1 = r'''```
+h1 = r"""```
----
------
/--------\
|--------|
|--------|
\------/
- ----```'''
-h2 = r'''```
+ ----```"""
+h2 = r"""```
----
------
/---\-/--\
|-----\--|
|--------|
\------/
- ----```'''
-h3 = r'''```
+ ----```"""
+h3 = r"""```
----
------
/---\-/--\
|-----\--|
|-----/--|
\----\-/
- ----```'''
-h4 = r'''```
+ ----```"""
+h4 = r"""```
-----
----- \
/--| /---\
|--\ -\---|
|--\--/-- /
\------- /
- ------```'''
+ ------```"""
stages = [h1, h2, h3, h4]
snakes = {
"Baby Python": "https://i.imgur.com/SYOcmSa.png",
@@ -114,8 +114,7 @@ ANGLE_RANGE = math.pi * 2
def get_resource(file: str) -> List[dict]:
"""Load Snake resources JSON."""
- with (SNAKE_RESOURCES / f"{file}.json").open(encoding="utf-8") as snakefile:
- return json.load(snakefile)
+ return json.loads((SNAKE_RESOURCES / f"{file}.json").read_text("utf-8"))
def smoothstep(t: float) -> float:
@@ -191,8 +190,9 @@ class PerlinNoiseFactory(object):
def get_plain_noise(self, *point) -> float:
"""Get plain noise for a single point, without taking into account either octaves or tiling."""
if len(point) != self.dimension:
- raise ValueError("Expected {0} values, got {1}".format(
- self.dimension, len(point)))
+ raise ValueError(
+ f"Expected {self.dimension} values, got {len(point)}"
+ )
# Build a list of the (min, max) bounds in each dimension
grid_coords = []
@@ -321,7 +321,7 @@ def create_snek_frame(
image_dimensions[Y] / 2 - (dimension_range[Y] / 2 + min_dimensions[Y])
)
- image = Image.new(mode='RGB', size=image_dimensions, color=bg_color)
+ image = Image.new(mode="RGB", size=image_dimensions, color=bg_color)
draw = ImageDraw(image)
for index in range(1, len(points)):
point = points[index]
@@ -345,7 +345,7 @@ def create_snek_frame(
def frame_to_png_bytes(image: Image) -> io.BytesIO:
"""Convert image to byte stream."""
stream = io.BytesIO()
- image.save(stream, format='PNG')
+ image.save(stream, format="PNG")
stream.seek(0)
return stream
@@ -373,7 +373,7 @@ class SnakeAndLaddersGame:
self.snakes = snakes
self.ctx = context
self.channel = self.ctx.channel
- self.state = 'booting'
+ self.state = "booting"
self.started = False
self.author = self.ctx.author
self.players = []
@@ -413,7 +413,7 @@ class SnakeAndLaddersGame:
"**Snakes and Ladders**: A new game is about to start!",
file=File(
str(SNAKE_RESOURCES / "snakes_and_ladders" / "banner.jpg"),
- filename='Snakes and Ladders.jpg'
+ filename="Snakes and Ladders.jpg"
)
)
startup = await self.channel.send(
@@ -423,7 +423,7 @@ class SnakeAndLaddersGame:
for emoji in STARTUP_SCREEN_EMOJI:
await startup.add_reaction(emoji)
- self.state = 'waiting'
+ self.state = "waiting"
while not self.started:
try:
@@ -460,7 +460,7 @@ class SnakeAndLaddersGame:
self.players.append(user)
self.player_tiles[user.id] = 1
- avatar_bytes = await user.avatar_url_as(format='jpeg', size=PLAYER_ICON_IMAGE_SIZE).read()
+ avatar_bytes = await user.avatar_url_as(format="jpeg", size=PLAYER_ICON_IMAGE_SIZE).read()
im = Image.open(io.BytesIO(avatar_bytes)).resize((BOARD_PLAYER_SIZE, BOARD_PLAYER_SIZE))
self.avatar_images[user.id] = im
@@ -475,7 +475,7 @@ class SnakeAndLaddersGame:
if user == p:
await self.channel.send(user.mention + " You are already in the game.", delete_after=10)
return
- if self.state != 'waiting':
+ if self.state != "waiting":
await self.channel.send(user.mention + " You cannot join at this time.", delete_after=10)
return
if len(self.players) is MAX_PLAYERS:
@@ -510,7 +510,7 @@ class SnakeAndLaddersGame:
delete_after=10
)
- if self.state != 'waiting' and len(self.players) == 0:
+ if self.state != "waiting" and len(self.players) == 0:
await self.channel.send("**Snakes and Ladders**: The game has been surrendered!")
is_surrendered = True
self._destruct()
@@ -535,12 +535,12 @@ class SnakeAndLaddersGame:
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 not self.state == "waiting":
await self.channel.send(user.mention + " The game cannot be started at this time.", delete_after=10)
return
- self.state = 'starting'
- player_list = ', '.join(user.mention for user in self.players)
+ self.state = "starting"
+ player_list = ", ".join(user.mention for user in self.players)
await self.channel.send("**Snakes and Ladders**: The game is starting!\nPlayers: " + player_list)
await self.start_round()
@@ -556,10 +556,10 @@ class SnakeAndLaddersGame:
))
)
- self.state = 'roll'
+ self.state = "roll"
for user in self.players:
self.round_has_rolled[user.id] = False
- board_img = Image.open(str(SNAKE_RESOURCES / "snakes_and_ladders" / "board.jpg"))
+ board_img = Image.open(SNAKE_RESOURCES / "snakes_and_ladders" / "board.jpg")
player_row_size = math.ceil(MAX_PLAYERS / 2)
for i, player in enumerate(self.players):
@@ -574,8 +574,8 @@ class SnakeAndLaddersGame:
board_img.paste(self.avatar_images[player.id],
box=(x_offset, y_offset))
- board_file = File(frame_to_png_bytes(board_img), filename='Board.jpg')
- player_list = '\n'.join((user.mention + ": Tile " + str(self.player_tiles[user.id])) for user in self.players)
+ board_file = File(frame_to_png_bytes(board_img), filename="Board.jpg")
+ player_list = "\n".join((user.mention + ": Tile " + str(self.player_tiles[user.id])) for user in self.players)
# Store and send new messages
temp_board = await self.channel.send(
@@ -644,7 +644,7 @@ class SnakeAndLaddersGame:
if user.id not in self.player_tiles:
await self.channel.send(user.mention + " You are not in the match.", delete_after=10)
return
- if self.state != 'roll':
+ if self.state != "roll":
await self.channel.send(user.mention + " You may not roll at this time.", delete_after=10)
return
if self.round_has_rolled[user.id]:
@@ -673,7 +673,7 @@ class SnakeAndLaddersGame:
async def _complete_round(self) -> None:
"""At the conclusion of a round check to see if there's been a winner."""
- self.state = 'post_round'
+ self.state = "post_round"
# check for winner
winner = self._check_winner()
@@ -688,7 +688,7 @@ class SnakeAndLaddersGame:
def _check_winner(self) -> Member:
"""Return a winning member if we're in the post-round state and there's a winner."""
- if self.state != 'post_round':
+ if self.state != "post_round":
return None
return next((player for player in self.players if self.player_tiles[player.id] == 100),
None)
diff --git a/bot/exts/evergreen/source.py b/bot/exts/evergreen/source.py
index 45752bf9..8fb72143 100644
--- a/bot/exts/evergreen/source.py
+++ b/bot/exts/evergreen/source.py
@@ -1,39 +1,18 @@
import inspect
from pathlib import Path
-from typing import Optional, Tuple, Union
+from typing import Optional, Tuple
from discord import Embed
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Source
-
-SourceType = Union[commands.Command, commands.Cog, str, commands.ExtensionNotLoaded]
-
-
-class SourceConverter(commands.Converter):
- """Convert an argument into a help command, tag, command, or cog."""
-
- async def convert(self, ctx: commands.Context, argument: str) -> SourceType:
- """Convert argument into source object."""
- cog = ctx.bot.get_cog(argument)
- if cog:
- return cog
-
- cmd = ctx.bot.get_command(argument)
- if cmd:
- return cmd
-
- raise commands.BadArgument(
- f"Unable to convert `{argument}` to valid command or Cog."
- )
+from bot.utils.converters import SourceConverter, SourceType
class BotSource(commands.Cog):
"""Displays information about the bot's source code."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@commands.command(name="source", aliases=("src",))
async def source_command(self, ctx: commands.Context, *, source_item: SourceConverter = None) -> None:
"""Display information and a GitHub link to the source code of a command, tag, or cog."""
@@ -85,7 +64,7 @@ class BotSource(commands.Cog):
url, location, first_line = self.get_source_link(source_object)
if isinstance(source_object, commands.Command):
- if source_object.cog_name == 'Help':
+ if source_object.cog_name == "Help":
title = "Help Command"
description = source_object.__doc__.splitlines()[1]
else:
@@ -104,6 +83,6 @@ class BotSource(commands.Cog):
return embed
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the BotSource cog."""
- bot.add_cog(BotSource(bot))
+ bot.add_cog(BotSource())
diff --git a/bot/exts/evergreen/space.py b/bot/exts/evergreen/space.py
index 323ff659..5e87c6d5 100644
--- a/bot/exts/evergreen/space.py
+++ b/bot/exts/evergreen/space.py
@@ -1,15 +1,16 @@
import logging
import random
from datetime import date, datetime
-from typing import Any, Dict, Optional, Union
+from typing import Any, Dict, Optional
from urllib.parse import urlencode
from discord import Embed
from discord.ext import tasks
-from discord.ext.commands import BadArgument, Cog, Context, Converter, group
+from discord.ext.commands import Cog, Context, group
from bot.bot import Bot
from bot.constants import Tokens
+from bot.utils.converters import DateConverter
from bot.utils.extensions import invoke_help_command
logger = logging.getLogger(__name__)
@@ -21,25 +22,10 @@ NASA_EPIC_BASE_URL = "https://epic.gsfc.nasa.gov"
APOD_MIN_DATE = date(1995, 6, 16)
-class DateConverter(Converter):
- """Parse SOL or earth date (in format YYYY-MM-DD) into `int` or `datetime`. When invalid input, raise error."""
-
- async def convert(self, ctx: Context, argument: str) -> Union[int, datetime]:
- """Parse date (SOL or earth) into `datetime` or `int`. When invalid value, raise error."""
- if argument.isdigit():
- return int(argument)
- try:
- date = datetime.strptime(argument, "%Y-%m-%d")
- except ValueError:
- raise BadArgument(f"Can't convert `{argument}` to `datetime` in format `YYYY-MM-DD` or `int` in SOL.")
- return date
-
-
class Space(Cog):
"""Space Cog contains commands, that show images, facts or other information about space."""
def __init__(self, bot: Bot):
- self.bot = bot
self.http_session = bot.http_session
self.rovers = {}
@@ -67,7 +53,7 @@ class Space(Cog):
await invoke_help_command(ctx)
@space.command(name="apod")
- async def apod(self, ctx: Context, date: Optional[str] = None) -> None:
+ async def apod(self, ctx: Context, date: Optional[str]) -> None:
"""
Get Astronomy Picture of Day from NASA API. Date is optional parameter, what formatting is YYYY-MM-DD.
@@ -100,7 +86,7 @@ class Space(Cog):
)
@space.command(name="nasa")
- async def nasa(self, ctx: Context, *, search_term: Optional[str] = None) -> None:
+ async def nasa(self, ctx: Context, *, search_term: Optional[str]) -> None:
"""Get random NASA information/facts + image. Support `search_term` parameter for more specific search."""
params = {
"media_type": "image"
@@ -125,8 +111,8 @@ class Space(Cog):
)
@space.command(name="epic")
- async def epic(self, ctx: Context, date: Optional[str] = None) -> None:
- """Get one of latest random image of earth from NASA EPIC API. Support date parameter, format is YYYY-MM-DD."""
+ async def epic(self, ctx: Context, date: Optional[str]) -> 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()
@@ -161,8 +147,8 @@ class Space(Cog):
async def mars(
self,
ctx: Context,
- date: Optional[DateConverter] = None,
- rover: Optional[str] = "curiosity"
+ date: Optional[DateConverter],
+ rover: str = "curiosity"
) -> None:
"""
Get random Mars image by date. Support both SOL (martian solar day) and earth date and rovers.
@@ -207,7 +193,7 @@ class Space(Cog):
)
)
- @mars.command(name="dates", aliases=["d", "date", "rover", "rovers", "r"])
+ @mars.command(name="dates", aliases=("d", "date", "rover", "rovers", "r"))
async def dates(self, ctx: Context) -> None:
"""Get current available rovers photo date ranges."""
await ctx.send("\n".join(
@@ -242,7 +228,7 @@ class Space(Cog):
def setup(bot: Bot) -> None:
- """Load Space Cog."""
+ """Load the Space cog."""
if not Tokens.nasa:
logger.warning("Can't find NASA API key. Not loading Space Cog.")
return
diff --git a/bot/exts/evergreen/speedrun.py b/bot/exts/evergreen/speedrun.py
index 21aad5aa..774eff81 100644
--- a/bot/exts/evergreen/speedrun.py
+++ b/bot/exts/evergreen/speedrun.py
@@ -5,23 +5,22 @@ from random import choice
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-with Path('bot/resources/evergreen/speedrun_links.json').open(encoding="utf8") as file:
- LINKS = json.load(file)
+
+LINKS = json.loads(Path("bot/resources/evergreen/speedrun_links.json").read_text("utf8"))
class Speedrun(commands.Cog):
"""Commands about the video game speedrunning community."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@commands.command(name="speedrun")
async def get_speedrun(self, ctx: commands.Context) -> None:
"""Sends a link to a video of a random speedrun."""
await ctx.send(choice(LINKS))
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Speedrun cog."""
- bot.add_cog(Speedrun(bot))
+ bot.add_cog(Speedrun())
diff --git a/bot/exts/evergreen/status_codes.py b/bot/exts/evergreen/status_codes.py
index 7c00fe20..a866692e 100644
--- a/bot/exts/evergreen/status_codes.py
+++ b/bot/exts/evergreen/status_codes.py
@@ -3,6 +3,7 @@ from http import HTTPStatus
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.utils.extensions import invoke_help_command
HTTP_DOG_URL = "https://httpstatusdogs.com/img/{code}.jpg"
@@ -12,7 +13,7 @@ HTTP_CAT_URL = "https://http.cat/{code}.jpg"
class HTTPStatusCodes(commands.Cog):
"""Commands that give HTTP statuses described and visualized by cats and dogs."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@commands.group(name="http_status", aliases=("status", "httpstatus"))
@@ -21,10 +22,10 @@ class HTTPStatusCodes(commands.Cog):
if not ctx.invoked_subcommand:
await invoke_help_command(ctx)
- @http_status_group.command(name='cat')
+ @http_status_group.command(name="cat")
async def http_cat(self, ctx: commands.Context, code: int) -> None:
"""Sends an embed with an image of a cat, portraying the status code."""
- embed = discord.Embed(title=f'**Status: {code}**')
+ embed = discord.Embed(title=f"**Status: {code}**")
url = HTTP_CAT_URL.format(code=code)
try:
@@ -36,18 +37,18 @@ class HTTPStatusCodes(commands.Cog):
raise NotImplementedError
except ValueError:
- embed.set_footer(text='Inputted status code does not exist.')
+ embed.set_footer(text="Inputted status code does not exist.")
except NotImplementedError:
- embed.set_footer(text='Inputted status code is not implemented by http.cat yet.')
+ embed.set_footer(text="Inputted status code is not implemented by http.cat yet.")
finally:
await ctx.send(embed=embed)
- @http_status_group.command(name='dog')
+ @http_status_group.command(name="dog")
async def http_dog(self, ctx: commands.Context, code: int) -> None:
"""Sends an embed with an image of a dog, portraying the status code."""
- embed = discord.Embed(title=f'**Status: {code}**')
+ embed = discord.Embed(title=f"**Status: {code}**")
url = HTTP_DOG_URL.format(code=code)
try:
@@ -59,15 +60,15 @@ class HTTPStatusCodes(commands.Cog):
raise NotImplementedError
except ValueError:
- embed.set_footer(text='Inputted status code does not exist.')
+ embed.set_footer(text="Inputted status code does not exist.")
except NotImplementedError:
- embed.set_footer(text='Inputted status code is not implemented by httpstatusdogs.com yet.')
+ embed.set_footer(text="Inputted status code is not implemented by httpstatusdogs.com yet.")
finally:
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the HTTPStatusCodes cog."""
bot.add_cog(HTTPStatusCodes(bot))
diff --git a/bot/exts/evergreen/tic_tac_toe.py b/bot/exts/evergreen/tic_tac_toe.py
index 6e21528e..bd5e0102 100644
--- a/bot/exts/evergreen/tic_tac_toe.py
+++ b/bot/exts/evergreen/tic_tac_toe.py
@@ -58,7 +58,7 @@ class Player:
)
try:
- react, _ = await self.ctx.bot.wait_for('reaction_add', timeout=30.0, check=check_for_move)
+ react, _ = await self.ctx.bot.wait_for("reaction_add", timeout=30.0, check=check_for_move)
except asyncio.TimeoutError:
return True, None
else:
@@ -246,8 +246,7 @@ def is_requester_free() -> t.Callable:
class TicTacToe(Cog):
"""TicTacToe cog contains tic-tac-toe game commands."""
- def __init__(self, bot: Bot):
- self.bot = bot
+ def __init__(self):
self.games: t.List[Game] = []
@guild_only()
@@ -323,5 +322,5 @@ class TicTacToe(Cog):
def setup(bot: Bot) -> None:
- """Load TicTacToe Cog."""
- bot.add_cog(TicTacToe(bot))
+ """Load the TicTacToe cog."""
+ bot.add_cog(TicTacToe())
diff --git a/bot/exts/evergreen/timed.py b/bot/exts/evergreen/timed.py
index 5f177fd6..2ea6b419 100644
--- a/bot/exts/evergreen/timed.py
+++ b/bot/exts/evergreen/timed.py
@@ -4,6 +4,8 @@ from time import perf_counter
from discord import Message
from discord.ext import commands
+from bot.bot import Bot
+
class TimedCommands(commands.Cog):
"""Time the command execution of a command."""
@@ -16,7 +18,7 @@ class TimedCommands(commands.Cog):
return await ctx.bot.get_context(msg)
- @commands.command(name="timed", aliases=["time", "t"])
+ @commands.command(name="timed", aliases=("time", "t"))
async def timed(self, ctx: commands.Context, *, command: str) -> None:
"""Time the command execution of a command."""
new_ctx = await self.create_execution_context(ctx, command)
@@ -41,6 +43,6 @@ class TimedCommands(commands.Cog):
await ctx.send(f"Command execution for `{new_ctx.command}` finished in {(t_end - t_start):.4f} seconds.")
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
- bot.add_cog(TimedCommands(bot))
+def setup(bot: Bot) -> None:
+ """Load the Timed cog."""
+ bot.add_cog(TimedCommands())
diff --git a/bot/exts/evergreen/trivia_quiz.py b/bot/exts/evergreen/trivia_quiz.py
index fe692c2a..352d5ae8 100644
--- a/bot/exts/evergreen/trivia_quiz.py
+++ b/bot/exts/evergreen/trivia_quiz.py
@@ -8,6 +8,7 @@ import discord
from discord.ext import commands
from fuzzywuzzy import fuzz
+from bot.bot import Bot
from bot.constants import Roles
@@ -23,7 +24,7 @@ WRONG_ANS_RESPONSE = [
class TriviaQuiz(commands.Cog):
"""A cog for all quiz commands."""
- def __init__(self, bot: commands.Bot) -> None:
+ def __init__(self, bot: Bot) -> None:
self.bot = bot
self.questions = self.load_questions()
self.game_status = {} # A variable to store the game status: either running or not running.
@@ -40,11 +41,9 @@ class TriviaQuiz(commands.Cog):
def load_questions() -> dict:
"""Load the questions from the JSON file."""
p = Path("bot", "resources", "evergreen", "trivia_quiz.json")
- with p.open(encoding="utf8") as json_data:
- questions = json.load(json_data)
- return questions
+ return json.loads(p.read_text("utf8"))
- @commands.group(name="quiz", aliases=["trivia"], invoke_without_command=True)
+ @commands.group(name="quiz", aliases=("trivia",), invoke_without_command=True)
async def quiz_game(self, ctx: commands.Context, category: str = None) -> None:
"""
Start a quiz!
@@ -61,10 +60,11 @@ class TriviaQuiz(commands.Cog):
# Stop game if running.
if self.game_status[ctx.channel.id] is True:
- return await ctx.send(
+ await ctx.send(
f"Game is already running..."
f"do `{self.bot.command_prefix}quiz stop`"
)
+ return
# Send embed showing available categories if inputted category is invalid.
if category is None:
@@ -127,7 +127,7 @@ class TriviaQuiz(commands.Cog):
)
try:
- msg = await self.bot.wait_for('message', check=check, timeout=10)
+ msg = await self.bot.wait_for("message", check=check, timeout=10)
except asyncio.TimeoutError:
# In case of TimeoutError and the game has been stopped, then do nothing.
if self.game_status[ctx.channel.id] is False:
@@ -299,6 +299,6 @@ class TriviaQuiz(commands.Cog):
await channel.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Load the cog."""
+def setup(bot: Bot) -> None:
+ """Load the TriviaQuiz cog."""
bot.add_cog(TriviaQuiz(bot))
diff --git a/bot/exts/evergreen/uptime.py b/bot/exts/evergreen/uptime.py
deleted file mode 100644
index a9ad9dfb..00000000
--- a/bot/exts/evergreen/uptime.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import logging
-
-import arrow
-from dateutil.relativedelta import relativedelta
-from discord.ext import commands
-
-from bot import start_time
-
-log = logging.getLogger(__name__)
-
-
-class Uptime(commands.Cog):
- """A cog for posting the bot's uptime."""
-
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(name="uptime")
- async def uptime(self, ctx: commands.Context) -> None:
- """Responds with the uptime of the bot."""
- difference = relativedelta(start_time - arrow.utcnow())
- uptime_string = start_time.shift(
- seconds=-difference.seconds,
- minutes=-difference.minutes,
- hours=-difference.hours,
- days=-difference.days
- ).humanize()
- await ctx.send(f"I started up {uptime_string}.")
-
-
-def setup(bot: commands.Bot) -> None:
- """Uptime Cog load."""
- bot.add_cog(Uptime(bot))
diff --git a/bot/exts/evergreen/wikipedia.py b/bot/exts/evergreen/wikipedia.py
index 068c4f43..83937438 100644
--- a/bot/exts/evergreen/wikipedia.py
+++ b/bot/exts/evergreen/wikipedia.py
@@ -20,7 +20,7 @@ WIKI_THUMBNAIL = (
"https://upload.wikimedia.org/wikipedia/en/thumb/8/80/Wikipedia-logo-v2.svg"
"/330px-Wikipedia-logo-v2.svg.png"
)
-WIKI_SNIPPET_REGEX = r'(<!--.*?-->|<[^>]*>)'
+WIKI_SNIPPET_REGEX = r"(<!--.*?-->|<[^>]*>)"
WIKI_SEARCH_RESULT = (
"**[{name}]({url})**\n"
"{description}\n"
@@ -39,18 +39,18 @@ class WikipediaSearch(commands.Cog):
async with self.bot.http_session.get(url=url) as resp:
if resp.status == 200:
raw_data = await resp.json()
- number_of_results = raw_data['query']['searchinfo']['totalhits']
+ number_of_results = raw_data["query"]["searchinfo"]["totalhits"]
if number_of_results:
- results = raw_data['query']['search']
+ results = raw_data["query"]["search"]
lines = []
for article in results:
line = WIKI_SEARCH_RESULT.format(
- name=article['title'],
+ name=article["title"],
description=unescape(
re.sub(
- WIKI_SNIPPET_REGEX, '', article['snippet']
+ WIKI_SNIPPET_REGEX, "", article["snippet"]
)
),
url=f"https://en.wikipedia.org/?curid={article['pageid']}"
@@ -72,7 +72,7 @@ class WikipediaSearch(commands.Cog):
return
@commands.cooldown(1, 10, commands.BucketType.user)
- @commands.command(name="wikipedia", aliases=["wiki"])
+ @commands.command(name="wikipedia", aliases=("wiki",))
async def wikipedia_search_command(self, ctx: commands.Context, *, search: str) -> None:
"""Sends paginated top 10 results of Wikipedia search.."""
contents = await self.wiki_request(ctx.channel, search)
@@ -90,5 +90,5 @@ class WikipediaSearch(commands.Cog):
def setup(bot: Bot) -> None:
- """Wikipedia Cog load."""
+ """Load the WikipediaSearch cog."""
bot.add_cog(WikipediaSearch(bot))
diff --git a/bot/exts/evergreen/wolfram.py b/bot/exts/evergreen/wolfram.py
index 14ec1041..d23afd6f 100644
--- a/bot/exts/evergreen/wolfram.py
+++ b/bot/exts/evergreen/wolfram.py
@@ -9,6 +9,7 @@ from discord import Embed
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.utils.pagination import ImagePaginator
@@ -39,9 +40,11 @@ async def send_embed(
"""Generate & send a response embed with Wolfram as the author."""
embed = Embed(colour=colour)
embed.description = message_txt
- embed.set_author(name="Wolfram Alpha",
- icon_url=WOLF_IMAGE,
- url="https://www.wolframalpha.com/")
+ embed.set_author(
+ name="Wolfram Alpha",
+ icon_url=WOLF_IMAGE,
+ url="https://www.wolframalpha.com/"
+ )
if footer:
embed.set_footer(text=footer)
@@ -55,10 +58,10 @@ def custom_cooldown(*ignore: List[int]) -> Callable:
"""
Implement per-user and per-guild cooldowns for requests to the Wolfram API.
- A list of roles may be provided to ignore the per-user cooldown
+ A list of roles may be provided to ignore the per-user cooldown.
"""
async def predicate(ctx: Context) -> bool:
- if ctx.invoked_with == 'help':
+ 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
@@ -102,9 +105,9 @@ def custom_cooldown(*ignore: List[int]) -> Callable:
return check(predicate)
-async def get_pod_pages(ctx: Context, bot: commands.Bot, query: str) -> Optional[List[Tuple]]:
+async def get_pod_pages(ctx: Context, bot: Bot, query: str) -> Optional[List[Tuple]]:
"""Get the Wolfram API pod pages for the provided query."""
- async with ctx.channel.typing():
+ async with ctx.typing():
url_str = parse.urlencode({
"input": query,
"appid": APPID,
@@ -117,7 +120,7 @@ async def get_pod_pages(ctx: Context, bot: commands.Bot, query: str) -> Optional
request_url = QUERY.format(request="query", data=url_str)
async with bot.http_session.get(request_url) as response:
- json = await response.json(content_type='text/plain')
+ json = await response.json(content_type="text/plain")
result = json["queryresult"]
@@ -162,7 +165,7 @@ async def get_pod_pages(ctx: Context, bot: commands.Bot, query: str) -> Optional
class Wolfram(Cog):
"""Commands for interacting with the Wolfram|Alpha API."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@group(name="wolfram", aliases=("wolf", "wa"), invoke_without_command=True)
@@ -179,7 +182,7 @@ class Wolfram(Cog):
query = QUERY.format(request="simple", data=url_str)
# Give feedback that the bot is working.
- async with ctx.channel.typing():
+ async with ctx.typing():
async with self.bot.http_session.get(query) as response:
status = response.status
image_bytes = await response.read()
@@ -188,11 +191,11 @@ class Wolfram(Cog):
image_url = "attachment://image.png"
if status == 501:
- message = "Failed to get response"
+ message = "Failed to get response."
footer = ""
color = Colours.soft_red
elif status == 400:
- message = "No input found"
+ message = "No input found."
footer = ""
color = Colours.soft_red
elif status == 403:
@@ -221,9 +224,11 @@ class Wolfram(Cog):
return
embed = Embed()
- embed.set_author(name="Wolfram Alpha",
- icon_url=WOLF_IMAGE,
- url="https://www.wolframalpha.com/")
+ embed.set_author(
+ name="Wolfram Alpha",
+ icon_url=WOLF_IMAGE,
+ url="https://www.wolframalpha.com/"
+ )
embed.colour = Colours.soft_orange
await ImagePaginator.paginate(pages, ctx, embed)
@@ -262,18 +267,18 @@ class Wolfram(Cog):
query = QUERY.format(request="result", data=url_str)
# Give feedback that the bot is working.
- async with ctx.channel.typing():
+ async with ctx.typing():
async with self.bot.http_session.get(query) as response:
status = response.status
response_text = await response.text()
if status == 501:
- message = "Failed to get response"
+ message = "Failed to get response."
color = Colours.soft_red
elif status == 400:
- message = "No input found"
+ message = "No input found."
color = Colours.soft_red
- elif response_text == "Error 1: Invalid appid":
+ elif response_text == "Error 1: Invalid appid.":
message = "Wolfram API key is invalid or missing."
color = Colours.soft_red
else:
@@ -283,6 +288,6 @@ class Wolfram(Cog):
await send_embed(ctx, message, color)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Wolfram cog."""
bot.add_cog(Wolfram(bot))
diff --git a/bot/exts/evergreen/wonder_twins.py b/bot/exts/evergreen/wonder_twins.py
index afc5346e..40edf785 100644
--- a/bot/exts/evergreen/wonder_twins.py
+++ b/bot/exts/evergreen/wonder_twins.py
@@ -2,15 +2,15 @@ import random
from pathlib import Path
import yaml
-from discord.ext.commands import Bot, Cog, Context, command
+from discord.ext.commands import Cog, Context, command
+
+from bot.bot import Bot
class WonderTwins(Cog):
"""Cog for a Wonder Twins inspired command."""
- def __init__(self, bot: Bot):
- self.bot = bot
-
+ def __init__(self):
with open(Path.cwd() / "bot" / "resources" / "evergreen" / "wonder_twins.yaml", "r", encoding="utf-8") as f:
info = yaml.load(f, Loader=yaml.FullLoader)
self.water_types = info["water_types"]
@@ -38,7 +38,7 @@ class WonderTwins(Cog):
object_name = self.append_onto(adjective, object_name)
return f"{object_name} of {water_type}"
- @command(name="formof", aliases=["wondertwins", "wondertwin", "fo"])
+ @command(name="formof", aliases=("wondertwins", "wondertwin", "fo"))
async def form_of(self, ctx: Context) -> None:
"""Command to send a Wonder Twins inspired phrase to the user invoking the command."""
await ctx.send(f"Form of {self.format_phrase()}!")
@@ -46,4 +46,4 @@ class WonderTwins(Cog):
def setup(bot: Bot) -> None:
"""Load the WonderTwins cog."""
- bot.add_cog(WonderTwins(bot))
+ bot.add_cog(WonderTwins())
diff --git a/bot/exts/evergreen/xkcd.py b/bot/exts/evergreen/xkcd.py
index 1ff98ca2..c98830bc 100644
--- a/bot/exts/evergreen/xkcd.py
+++ b/bot/exts/evergreen/xkcd.py
@@ -53,7 +53,7 @@ class XKCD(Cog):
await ctx.send(embed=embed)
return
- comic = randint(1, self.latest_comic_info['num']) if comic is None else comic.group(0)
+ comic = randint(1, self.latest_comic_info["num"]) if comic is None else comic.group(0)
if comic == "latest":
info = self.latest_comic_info
@@ -69,7 +69,7 @@ class XKCD(Cog):
return
embed.title = f"XKCD comic #{info['num']}"
- embed.description = info['alt']
+ embed.description = info["alt"]
embed.url = f"{BASE_URL}/{info['num']}"
if info["img"][-3:] in ("jpg", "png", "gif"):
@@ -87,5 +87,5 @@ class XKCD(Cog):
def setup(bot: Bot) -> None:
- """Loading the XKCD cog."""
+ """Load the XKCD cog."""
bot.add_cog(XKCD(bot))