aboutsummaryrefslogtreecommitdiffstats
path: root/bot
diff options
context:
space:
mode:
authorGravatar Kieran Siek <[email protected]>2021-06-07 17:16:15 +0800
committerGravatar GitHub <[email protected]>2021-06-07 17:16:15 +0800
commite73fb4a2ced2fd264ec65a828c114cbbeda26d7a (patch)
treec468b60cbd00b8f6b00043635dc2d4393a001c4b /bot
parent(tic-tac-toe): Use embeds for showing previous game boards (diff)
parentMerge pull request #758 from OculusMode/master (diff)
Merge branch 'main' into fix/ttt
Diffstat (limited to 'bot')
-rw-r--r--bot/__init__.py11
-rw-r--r--bot/bot.py2
-rw-r--r--bot/constants.py30
-rw-r--r--bot/exts/christmas/advent_of_code/_cog.py32
-rw-r--r--bot/exts/christmas/advent_of_code/_helpers.py30
-rw-r--r--bot/exts/christmas/hanukkah_embed.py85
-rw-r--r--bot/exts/easter/april_fools_vids.py16
-rw-r--r--bot/exts/easter/bunny_name_generator.py42
-rw-r--r--bot/exts/easter/earth_photos.py11
-rw-r--r--bot/exts/easter/easter_riddle.py17
-rw-r--r--bot/exts/easter/egg_decorating.py29
-rw-r--r--bot/exts/easter/egg_facts.py22
-rw-r--r--bot/exts/easter/egghead_quiz.py31
-rw-r--r--bot/exts/easter/save_the_planet.py16
-rw-r--r--bot/exts/easter/traditions.py16
-rw-r--r--bot/exts/evergreen/avatar_modification/_effects.py12
-rw-r--r--bot/exts/evergreen/avatar_modification/avatar_modify.py18
-rw-r--r--bot/exts/evergreen/battleship.py35
-rw-r--r--bot/exts/evergreen/bookmark.py10
-rw-r--r--bot/exts/evergreen/catify.py10
-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.py56
-rw-r--r--bot/exts/evergreen/fun.py10
-rw-r--r--bot/exts/evergreen/game.py39
-rw-r--r--bot/exts/evergreen/githubinfo.py54
-rw-r--r--bot/exts/evergreen/help.py230
-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.py5
-rw-r--r--bot/exts/evergreen/pythonfacts.py29
-rw-r--r--bot/exts/evergreen/recommend_game.py17
-rw-r--r--bot/exts/evergreen/rps.py57
-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.py42
-rw-r--r--bot/exts/evergreen/space.py36
-rw-r--r--bot/exts/evergreen/speedrun.py13
-rw-r--r--bot/exts/evergreen/status_codes.py44
-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.py456
-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
-rw-r--r--bot/exts/halloween/8ball.py18
-rw-r--r--bot/exts/halloween/candy_collection.py59
-rw-r--r--bot/exts/halloween/hacktober-issue-finder.py77
-rw-r--r--bot/exts/halloween/hacktoberstats.py115
-rw-r--r--bot/exts/halloween/halloween_facts.py22
-rw-r--r--bot/exts/halloween/halloweenify.py30
-rw-r--r--bot/exts/halloween/monsterbio.py19
-rw-r--r--bot/exts/halloween/monstersurvey.py119
-rw-r--r--bot/exts/halloween/scarymovie.py92
-rw-r--r--bot/exts/halloween/spookygif.py26
-rw-r--r--bot/exts/halloween/spookynamerate.py50
-rw-r--r--bot/exts/halloween/spookyrating.py26
-rw-r--r--bot/exts/halloween/spookyreact.py43
-rw-r--r--bot/exts/halloween/timeleft.py11
-rw-r--r--bot/exts/internal_eval/_internal_eval.py10
-rw-r--r--bot/exts/pride/drag_queen_name.py24
-rw-r--r--bot/exts/pride/pride_anthem.py30
-rw-r--r--bot/exts/pride/pride_facts.py34
-rw-r--r--bot/exts/pride/pride_leader.py117
-rw-r--r--bot/exts/valentines/be_my_valentine.py70
-rw-r--r--bot/exts/valentines/lovecalculator.py28
-rw-r--r--bot/exts/valentines/movie_generator.py14
-rw-r--r--bot/exts/valentines/myvalenstate.py30
-rw-r--r--bot/exts/valentines/pickuplines.py23
-rw-r--r--bot/exts/valentines/savethedate.py17
-rw-r--r--bot/exts/valentines/valentine_zodiac.py52
-rw-r--r--bot/exts/valentines/whoisvalentine.py29
-rw-r--r--bot/resources/evergreen/trivia_quiz.json335
-rw-r--r--bot/resources/pride/prideleader.json100
-rw-r--r--bot/utils/__init__.py43
-rw-r--r--bot/utils/checks.py30
-rw-r--r--bot/utils/converters.py83
-rw-r--r--bot/utils/decorators.py2
-rw-r--r--bot/utils/extensions.py4
-rw-r--r--bot/utils/halloween/spookifications.py10
-rw-r--r--bot/utils/pagination.py37
89 files changed, 2605 insertions, 1620 deletions
diff --git a/bot/__init__.py b/bot/__init__.py
index 71b7c8a3..85ae4758 100644
--- a/bot/__init__.py
+++ b/bot/__init__.py
@@ -1,3 +1,10 @@
+try:
+ from dotenv import load_dotenv
+ print("Found .env file, loading environment variables from it.")
+ load_dotenv(override=True)
+except ModuleNotFoundError:
+ pass
+
import asyncio
import logging
import logging.handlers
@@ -64,12 +71,12 @@ logging.getLogger("matplotlib").setLevel(logging.ERROR)
# Setup new logging configuration
logging.basicConfig(
- format='%(asctime)s - %(name)s %(levelname)s: %(message)s',
+ format="%(asctime)s - %(name)s %(levelname)s: %(message)s",
datefmt="%D %H:%M:%S",
level=logging.TRACE if Client.debug else logging.DEBUG,
handlers=[console_handler, file_handler],
)
-logging.getLogger().info('Logging initialization complete')
+logging.getLogger().info("Logging initialization complete")
# On Windows, the selector event loop is required for aiodns.
diff --git a/bot/bot.py b/bot/bot.py
index 7e495940..b8de97aa 100644
--- a/bot/bot.py
+++ b/bot/bot.py
@@ -101,7 +101,7 @@ class Bot(commands.Bot):
all_channels_ids = [channel.id for channel in self.get_all_channels()]
for name, channel_id in vars(constants.Channels).items():
- if name.startswith('_'):
+ if name.startswith("_"):
continue
if channel_id not in all_channels_ids:
log.error(f'Channel "{name}" with ID {channel_id} missing')
diff --git a/bot/constants.py b/bot/constants.py
index d50dcc6e..f7fe216b 100644
--- a/bot/constants.py
+++ b/bot/constants.py
@@ -22,6 +22,7 @@ __all__ = (
"Wolfram",
"Reddit",
"RedisConfig",
+ "RedirectOutput",
"MODERATION_ROLES",
"STAFF_ROLES",
"WHITELISTED_CHANNELS",
@@ -140,19 +141,20 @@ class Client(NamedTuple):
class Colours:
- blue = 0x0279fd
- bright_green = 0x01d277
- dark_green = 0x1f8b4c
- orange = 0xe67e22
- pink = 0xcf84e0
- purple = 0xb734eb
- soft_green = 0x68c290
- soft_orange = 0xf9cb54
- soft_red = 0xcd6d6d
- yellow = 0xf9f586
+ blue = 0x0279FD
+ bright_green = 0x01D277
+ dark_green = 0x1F8B4C
+ orange = 0xE67E22
+ pink = 0xCF84E0
+ purple = 0xB734EB
+ soft_green = 0x68C290
+ soft_orange = 0xF9CB54
+ soft_red = 0xCD6D6D
+ yellow = 0xF9F586
python_blue = 0x4B8BBE
python_yellow = 0xFFD43B
- grass_green = 0x66ff00
+ grass_green = 0x66FF00
+ gold = 0xE6C200
easter_like_colours = [
(255, 247, 0),
@@ -179,7 +181,7 @@ class Emojis:
envelope = "\U0001F4E8"
trashcan = environ.get("TRASHCAN_EMOJI", "<:trashcan:637136429717389331>")
ok_hand = ":ok_hand:"
- hand_raised = "\U0001f64b"
+ hand_raised = "\U0001F64B"
dice_1 = "<:dice_1:755891608859443290>"
dice_2 = "<:dice_2:755891608741740635>"
@@ -307,6 +309,10 @@ class Source:
github_avatar_url = "https://avatars1.githubusercontent.com/u/9919"
+class RedirectOutput:
+ delete_delay: int = 10
+
+
class Reddit:
subreddits = ["r/Python"]
diff --git a/bot/exts/christmas/advent_of_code/_cog.py b/bot/exts/christmas/advent_of_code/_cog.py
index 8376987d..3d61753b 100644
--- a/bot/exts/christmas/advent_of_code/_cog.py
+++ b/bot/exts/christmas/advent_of_code/_cog.py
@@ -3,6 +3,7 @@ import logging
from datetime import datetime, timedelta
from pathlib import Path
+import arrow
import discord
from discord.ext import commands
@@ -72,11 +73,15 @@ class AdventOfCode(commands.Cog):
if role not in ctx.author.roles:
await ctx.author.add_roles(role)
- await ctx.send("Okay! You have been __subscribed__ to notifications about new Advent of Code tasks. "
- f"You can run `{unsubscribe_command}` to disable them again for you.")
+ await ctx.send(
+ "Okay! You have been __subscribed__ to notifications about new Advent of Code tasks. "
+ f"You can run `{unsubscribe_command}` to disable them again for you."
+ )
else:
- await ctx.send("Hey, you already are receiving notifications about new Advent of Code tasks. "
- f"If you don't want them any more, run `{unsubscribe_command}` instead.")
+ await ctx.send(
+ "Hey, you already are receiving notifications about new Advent of Code tasks. "
+ f"If you don't want them any more, run `{unsubscribe_command}` instead."
+ )
@in_month(Month.DECEMBER)
@adventofcode_group.command(name="unsubscribe", aliases=("unsub",), brief="Notifications for new days")
@@ -96,11 +101,11 @@ class AdventOfCode(commands.Cog):
async def aoc_countdown(self, ctx: commands.Context) -> None:
"""Return time left until next day."""
if not _helpers.is_in_advent():
- datetime_now = datetime.now(_helpers.EST)
+ datetime_now = arrow.now(_helpers.EST)
# Calculate the delta to this & next year's December 1st to see which one is closest and not in the past
- this_year = datetime(datetime_now.year, 12, 1, tzinfo=_helpers.EST)
- next_year = datetime(datetime_now.year + 1, 12, 1, tzinfo=_helpers.EST)
+ this_year = arrow.get(datetime(datetime_now.year, 12, 1), _helpers.EST)
+ next_year = arrow.get(datetime(datetime_now.year + 1, 12, 1), _helpers.EST)
deltas = (dec_first - datetime_now for dec_first in (this_year, next_year))
delta = min(delta for delta in deltas if delta >= timedelta()) # timedelta() gives 0 duration delta
@@ -110,8 +115,10 @@ class AdventOfCode(commands.Cog):
else:
delta_str = f"{delta.days} days"
- await ctx.send(f"The Advent of Code event is not currently running. "
- f"The next event will start in {delta_str}.")
+ await ctx.send(
+ "The Advent of Code event is not currently running. "
+ f"The next event will start in {delta_str}."
+ )
return
tomorrow, time_left = _helpers.time_left_to_est_midnight()
@@ -124,7 +131,7 @@ class AdventOfCode(commands.Cog):
@whitelist_override(channels=AOC_WHITELIST)
async def about_aoc(self, ctx: commands.Context) -> None:
"""Respond with an explanation of all things Advent of Code."""
- await ctx.send("", embed=self.cached_about_aoc)
+ await ctx.send(embed=self.cached_about_aoc)
@adventofcode_group.command(name="join", aliases=("j",), brief="Learn how to join the leaderboard (via DM)")
@whitelist_override(channels=AOC_WHITELIST)
@@ -135,7 +142,7 @@ class AdventOfCode(commands.Cog):
await ctx.send(f"The Python Discord leaderboard for {current_year} is not yet available!")
return
- author = ctx.message.author
+ author = ctx.author
log.info(f"{author.name} ({author.id}) has requested a PyDis AoC leaderboard code")
if AocConfig.staff_leaderboard_id and any(r.id == Roles.helpers for r in author.roles):
@@ -273,8 +280,7 @@ class AdventOfCode(commands.Cog):
def _build_about_embed(self) -> discord.Embed:
"""Build and return the informational "About AoC" embed from the resources file."""
- with self.about_aoc_filepath.open("r", encoding="utf8") as f:
- embed_fields = json.load(f)
+ embed_fields = json.loads(self.about_aoc_filepath.read_text("utf8"))
about_embed = discord.Embed(
title=self._base_url,
diff --git a/bot/exts/christmas/advent_of_code/_helpers.py b/bot/exts/christmas/advent_of_code/_helpers.py
index a16a4871..96de90c4 100644
--- a/bot/exts/christmas/advent_of_code/_helpers.py
+++ b/bot/exts/christmas/advent_of_code/_helpers.py
@@ -9,8 +9,8 @@ import typing
from typing import Tuple
import aiohttp
+import arrow
import discord
-import pytz
from bot.bot import Bot
from bot.constants import AdventOfCode, Channels, Colours
@@ -48,7 +48,7 @@ AOC_EMBED_THUMBNAIL = (
)
# Create an easy constant for the EST timezone
-EST = pytz.timezone("EST")
+EST = "America/New_York"
# Step size for the challenge countdown status
COUNTDOWN_STEP = 60 * 5
@@ -108,7 +108,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict:
# star view. We need that per star view to compute rank scores per star.
for member in raw_leaderboard_data.values():
name = member["name"] if member["name"] else f"Anonymous #{member['id']}"
- member_id = member['id']
+ member_id = member["id"]
leaderboard[member_id] = {"name": name, "score": 0, "star_1": 0, "star_2": 0}
# Iterate over all days for this participant
@@ -119,7 +119,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict:
leaderboard[member_id][f"star_{star}"] += 1
# Record completion datetime for this participant for this day/star
- completion_time = datetime.datetime.fromtimestamp(int(data['get_star_ts']))
+ completion_time = datetime.datetime.fromtimestamp(int(data["get_star_ts"]))
star_results[(day, star)].append(
StarResult(member_id=member_id, completion_time=completion_time)
)
@@ -133,7 +133,7 @@ def _parse_raw_leaderboard_data(raw_leaderboard_data: dict) -> dict:
if day in AdventOfCode.ignored_days:
continue
- sorted_result = sorted(results, key=operator.attrgetter('completion_time'))
+ sorted_result = sorted(results, key=operator.attrgetter("completion_time"))
for rank, star_result in enumerate(sorted_result):
leaderboard[star_result.member_id]["score"] += max_score - rank
@@ -307,7 +307,7 @@ async def fetch_leaderboard(invalidate_cache: bool = False) -> dict:
def get_summary_embed(leaderboard: dict) -> discord.Embed:
"""Get an embed with the current summary stats of the leaderboard."""
- leaderboard_url = leaderboard['full_leaderboard_url']
+ leaderboard_url = leaderboard["full_leaderboard_url"]
refresh_minutes = AdventOfCode.leaderboard_cache_expiry_seconds // 60
aoc_embed = discord.Embed(
@@ -395,13 +395,13 @@ def is_in_advent() -> bool:
something for the next Advent of Code challenge should run. As the puzzle
published on the 25th is the last puzzle, this check excludes that date.
"""
- return datetime.datetime.now(EST).day in range(1, 25) and datetime.datetime.now(EST).month == 12
+ return arrow.now(EST).day in range(1, 25) and arrow.now(EST).month == 12
def time_left_to_est_midnight() -> Tuple[datetime.datetime, datetime.timedelta]:
"""Calculate the amount of time left until midnight EST/UTC-5."""
# Change all time properties back to 00:00
- todays_midnight = datetime.datetime.now(EST).replace(
+ todays_midnight = arrow.now(EST).replace(
microsecond=0,
second=0,
minute=0,
@@ -412,7 +412,7 @@ def time_left_to_est_midnight() -> Tuple[datetime.datetime, datetime.timedelta]:
tomorrow = todays_midnight + datetime.timedelta(days=1)
# Calculate the timedelta between the current time and midnight
- return tomorrow, tomorrow - datetime.datetime.now(EST)
+ return tomorrow, tomorrow - arrow.now(EST)
async def wait_for_advent_of_code(*, hours_before: int = 1) -> None:
@@ -430,9 +430,9 @@ async def wait_for_advent_of_code(*, hours_before: int = 1) -> None:
if we're already past the Advent of Code edition the bot is currently
configured for.
"""
- start = datetime.datetime(AdventOfCode.year, 12, 1, 0, 0, 0, tzinfo=EST)
+ start = arrow.get(datetime.datetime(AdventOfCode.year, 12, 1), EST)
target = start - datetime.timedelta(hours=hours_before)
- now = datetime.datetime.now(EST)
+ now = arrow.now(EST)
# If we've already reached or passed to target, we
# simply return immediately.
@@ -474,10 +474,10 @@ async def countdown_status(bot: Bot) -> None:
# sleeping for the entire year, it will only wait in the currently
# configured year. This means that the task will only start hibernating once
# we start preparing the next event by changing environment variables.
- last_challenge = datetime.datetime(AdventOfCode.year, 12, 25, 0, 0, 0, tzinfo=EST)
+ last_challenge = arrow.get(datetime.datetime(AdventOfCode.year, 12, 25), EST)
end = last_challenge + datetime.timedelta(hours=1)
- while datetime.datetime.now(EST) < end:
+ while arrow.now(EST) < end:
_, time_left = time_left_to_est_midnight()
aligned_seconds = int(math.ceil(time_left.seconds / COUNTDOWN_STEP)) * COUNTDOWN_STEP
@@ -534,8 +534,8 @@ async def new_puzzle_notification(bot: Bot) -> None:
# The last event day is 25 December, so we only have to schedule
# a reminder if the current day is before 25 December.
- end = datetime.datetime(AdventOfCode.year, 12, 25, tzinfo=EST)
- while datetime.datetime.now(EST) < end:
+ end = arrow.get(datetime.datetime(AdventOfCode.year, 12, 25), EST)
+ while arrow.now(EST) < end:
log.trace("Started puzzle notification loop.")
tomorrow, time_left = time_left_to_est_midnight()
diff --git a/bot/exts/christmas/hanukkah_embed.py b/bot/exts/christmas/hanukkah_embed.py
index 4f470a34..119f2446 100644
--- a/bot/exts/christmas/hanukkah_embed.py
+++ b/bot/exts/christmas/hanukkah_embed.py
@@ -5,19 +5,23 @@ from typing import List
from discord import Embed
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours, Month
from bot.utils.decorators import in_month
log = logging.getLogger(__name__)
+HEBCAL_URL = (
+ "https://www.hebcal.com/hebcal/?v=1&cfg=json&maj=on&min=on&mod=on&nx=on&"
+ "year=now&month=x&ss=on&mf=on&c=on&geo=geoname&geonameid=3448439&m=50&s=on"
+)
+
class HanukkahEmbed(commands.Cog):
"""A cog that returns information about Hanukkah festival."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
- self.url = ("https://www.hebcal.com/hebcal/?v=1&cfg=json&maj=on&min=on&mod=on&nx=on&"
- "year=now&month=x&ss=on&mf=on&c=on&geo=geoname&geonameid=3448439&m=50&s=on")
self.hanukkah_days = []
self.hanukkah_months = []
self.hanukkah_years = []
@@ -25,17 +29,17 @@ class HanukkahEmbed(commands.Cog):
async def get_hanukkah_dates(self) -> List[str]:
"""Gets the dates for hanukkah festival."""
hanukkah_dates = []
- async with self.bot.http_session.get(self.url) as response:
+ async with self.bot.http_session.get(HEBCAL_URL) as response:
json_data = await response.json()
- festivals = json_data['items']
+ festivals = json_data["items"]
for festival in festivals:
- if festival['title'].startswith('Chanukah'):
- date = festival['date']
+ if festival["title"].startswith("Chanukah"):
+ date = festival["date"]
hanukkah_dates.append(date)
return hanukkah_dates
@in_month(Month.DECEMBER)
- @commands.command(name='hanukkah', aliases=['chanukah'])
+ @commands.command(name="hanukkah", aliases=("chanukah",))
async def hanukkah_festival(self, ctx: commands.Context) -> None:
"""Tells you about the Hanukkah Festivaltime of festival, festival day, etc)."""
hanukkah_dates = await self.get_hanukkah_dates()
@@ -54,49 +58,46 @@ class HanukkahEmbed(commands.Cog):
day = str(today.day)
month = str(today.month)
year = str(today.year)
- embed = Embed()
- embed.title = 'Hanukkah'
- embed.colour = Colours.blue
+ embed = Embed(title="Hanukkah", colour=Colours.blue)
if day in self.hanukkah_days and month in self.hanukkah_months and year in self.hanukkah_years:
if int(day) == hanukkah_start_day:
now = datetime.datetime.utcnow()
- now = str(now)
- hours = int(now[11:13]) + 4 # using only hours
+ hours = now.hour + 4 # using only hours
hanukkah_start_hour = 18
if hours < hanukkah_start_hour:
- embed.description = (f"Hanukkah hasnt started yet, "
- f"it will start in about {hanukkah_start_hour-hours} hour/s.")
- return await ctx.send(embed=embed)
+ embed.description = (
+ "Hanukkah hasnt started yet, "
+ f"it will start in about {hanukkah_start_hour - hours} hour/s."
+ )
+ await ctx.send(embed=embed)
+ return
elif hours > hanukkah_start_hour:
- embed.description = (f'It is the starting day of Hanukkah ! '
- f'Its been {hours-hanukkah_start_hour} hours hanukkah started !')
- return await ctx.send(embed=embed)
+ embed.description = (
+ "It is the starting day of Hanukkah! "
+ f"Its been {hours - hanukkah_start_hour} hours hanukkah started!"
+ )
+ await ctx.send(embed=embed)
+ return
festival_day = self.hanukkah_days.index(day)
- number_suffixes = ['st', 'nd', 'rd', 'th']
- suffix = ''
- if int(festival_day) == 1:
- suffix = number_suffixes[0]
- if int(festival_day) == 2:
- suffix = number_suffixes[1]
- if int(festival_day) == 3:
- suffix = number_suffixes[2]
- if int(festival_day) > 3:
- suffix = number_suffixes[3]
- message = ''
- for _ in range(1, festival_day + 1):
- message += ':menorah:'
- embed.description = f'It is the {festival_day}{suffix} day of Hanukkah ! \n {message}'
+ number_suffixes = ["st", "nd", "rd", "th"]
+ suffix = number_suffixes[festival_day - 1 if festival_day <= 3 else 3]
+ message = ":menorah:" * festival_day
+ embed.description = f"It is the {festival_day}{suffix} day of Hanukkah!\n{message}"
await ctx.send(embed=embed)
else:
if today < hanukkah_start:
- festival_starting_month = hanukkah_start.strftime('%B')
- embed.description = (f"Hanukkah has not started yet. "
- f"Hanukkah will start at sundown on {hanukkah_start_day}th "
- f"of {festival_starting_month}.")
+ festival_starting_month = hanukkah_start.strftime("%B")
+ embed.description = (
+ f"Hanukkah has not started yet. "
+ f"Hanukkah will start at sundown on {hanukkah_start_day}th "
+ f"of {festival_starting_month}."
+ )
else:
- festival_end_month = hanukkah_end.strftime('%B')
- embed.description = (f"Looks like you missed Hanukkah !"
- f"Hanukkah ended on {hanukkah_end_day}th of {festival_end_month}.")
+ festival_end_month = hanukkah_end.strftime("%B")
+ embed.description = (
+ f"Looks like you missed Hanukkah!"
+ f"Hanukkah ended on {hanukkah_end_day}th of {festival_end_month}."
+ )
await ctx.send(embed=embed)
@@ -108,6 +109,6 @@ class HanukkahEmbed(commands.Cog):
self.hanukkah_years.append(date[0:4])
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Hanukkah Embed Cog."""
bot.add_cog(HanukkahEmbed(bot))
diff --git a/bot/exts/easter/april_fools_vids.py b/bot/exts/easter/april_fools_vids.py
index c7a3c014..5ef40704 100644
--- a/bot/exts/easter/april_fools_vids.py
+++ b/bot/exts/easter/april_fools_vids.py
@@ -1,19 +1,21 @@
import logging
import random
-from json import load
+from json import loads
+from pathlib import Path
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-with open("bot/resources/easter/april_fools_vids.json", encoding="utf-8") as f:
- ALL_VIDS = load(f)
+ALL_VIDS = loads(Path("bot/resources/easter/april_fools_vids.json").read_text("utf-8"))
class AprilFoolVideos(commands.Cog):
"""A cog for April Fools' that gets a random April Fools' video from Youtube."""
- @commands.command(name='fool')
+ @commands.command(name="fool")
async def april_fools(self, ctx: commands.Context) -> None:
"""Get a random April Fools' video from Youtube."""
video = random.choice(ALL_VIDS)
@@ -23,6 +25,6 @@ class AprilFoolVideos(commands.Cog):
await ctx.send(f"Check out this April Fools' video by {channel}.\n\n{url}")
-def setup(bot: commands.Bot) -> None:
- """April Fools' Cog load."""
- bot.add_cog(AprilFoolVideos(bot))
+def setup(bot: Bot) -> None:
+ """Load the April Fools' Cog."""
+ bot.add_cog(AprilFoolVideos())
diff --git a/bot/exts/easter/bunny_name_generator.py b/bot/exts/easter/bunny_name_generator.py
index 3ecf9be9..3e97373f 100644
--- a/bot/exts/easter/bunny_name_generator.py
+++ b/bot/exts/easter/bunny_name_generator.py
@@ -7,25 +7,26 @@ from typing import List, Union
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-with Path("bot/resources/easter/bunny_names.json").open("r", encoding="utf8") as f:
- BUNNY_NAMES = json.load(f)
+BUNNY_NAMES = json.loads(Path("bot/resources/easter/bunny_names.json").read_text("utf8"))
class BunnyNameGenerator(commands.Cog):
"""Generate a random bunny name, or bunnify your Discord username!"""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- def find_separators(self, displayname: str) -> Union[List[str], None]:
+ @staticmethod
+ def find_separators(displayname: str) -> Union[List[str], None]:
"""Check if Discord name contains spaces so we can bunnify an individual word in the name."""
- new_name = re.split(r'[_.\s]', displayname)
+ new_name = re.split(r"[_.\s]", displayname)
if displayname not in new_name:
return new_name
+ return None
- def find_vowels(self, displayname: str) -> str:
+ @staticmethod
+ def find_vowels(displayname: str) -> str:
"""
Finds vowels in the user's display name.
@@ -34,11 +35,11 @@ class BunnyNameGenerator(commands.Cog):
Only the most recently matched pattern will apply the changes.
"""
expressions = [
- (r'a.+y', 'patchy'),
- (r'e.+y', 'ears'),
- (r'i.+y', 'ditsy'),
- (r'o.+y', 'oofy'),
- (r'u.+y', 'uffy'),
+ ("a.+y", "patchy"),
+ ("e.+y", "ears"),
+ ("i.+y", "ditsy"),
+ ("o.+y", "oofy"),
+ ("u.+y", "uffy"),
]
for exp, vowel_sub in expressions:
@@ -46,9 +47,10 @@ class BunnyNameGenerator(commands.Cog):
if new_name != displayname:
return new_name
- def append_name(self, displayname: str) -> str:
+ @staticmethod
+ def append_name(displayname: str) -> str:
"""Adds a suffix to the end of the Discord name."""
- extensions = ['foot', 'ear', 'nose', 'tail']
+ extensions = ["foot", "ear", "nose", "tail"]
suffix = random.choice(extensions)
appended_name = displayname + suffix
@@ -62,7 +64,7 @@ class BunnyNameGenerator(commands.Cog):
@commands.command()
async def bunnifyme(self, ctx: commands.Context) -> None:
"""Gets your Discord username and bunnifies it."""
- username = ctx.message.author.display_name
+ username = ctx.author.display_name
# If name contains spaces or other separators, get the individual words to randomly bunnify
spaces_in_name = self.find_separators(username)
@@ -75,7 +77,7 @@ class BunnyNameGenerator(commands.Cog):
unmatched_name = self.append_name(username)
if spaces_in_name is not None:
- replacements = ['Cotton', 'Fluff', 'Floof' 'Bounce', 'Snuffle', 'Nibble', 'Cuddle', 'Velvetpaw', 'Carrot']
+ replacements = ["Cotton", "Fluff", "Floof" "Bounce", "Snuffle", "Nibble", "Cuddle", "Velvetpaw", "Carrot"]
word_to_replace = random.choice(spaces_in_name)
substitute = random.choice(replacements)
bunnified_name = username.replace(word_to_replace, substitute)
@@ -87,6 +89,6 @@ class BunnyNameGenerator(commands.Cog):
await ctx.send(bunnified_name)
-def setup(bot: commands.Bot) -> None:
- """Bunny Name Generator Cog load."""
- bot.add_cog(BunnyNameGenerator(bot))
+def setup(bot: Bot) -> None:
+ """Load the Bunny Name Generator Cog."""
+ bot.add_cog(BunnyNameGenerator())
diff --git a/bot/exts/easter/earth_photos.py b/bot/exts/easter/earth_photos.py
index bf658391..f65790af 100644
--- a/bot/exts/easter/earth_photos.py
+++ b/bot/exts/easter/earth_photos.py
@@ -3,24 +3,27 @@ import logging
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
from bot.constants import Tokens
log = logging.getLogger(__name__)
+API_URL = "https://api.unsplash.com/photos/random"
+
class EarthPhotos(commands.Cog):
"""This cog contains the command for earth photos."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
- @commands.command(aliases=["earth"])
+ @commands.command(aliases=("earth",))
async def earth_photos(self, ctx: commands.Context) -> None:
"""Returns a random photo of earth, sourced from Unsplash."""
async with ctx.typing():
async with self.bot.http_session.get(
- 'https://api.unsplash.com/photos/random',
+ API_URL,
params={"query": "planet_earth", "client_id": Tokens.unsplash_access_key}
) as r:
jsondata = await r.json()
@@ -55,7 +58,7 @@ class EarthPhotos(commands.Cog):
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Load the Earth Photos cog."""
if not Tokens.unsplash_access_key:
log.warning("No Unsplash access key found. Cog not loading.")
diff --git a/bot/exts/easter/easter_riddle.py b/bot/exts/easter/easter_riddle.py
index a93b3745..88b3be2f 100644
--- a/bot/exts/easter/easter_riddle.py
+++ b/bot/exts/easter/easter_riddle.py
@@ -1,18 +1,18 @@
import asyncio
import logging
import random
-from json import load
+from json import loads
from pathlib import Path
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours, NEGATIVE_REPLIES
log = logging.getLogger(__name__)
-with Path("bot/resources/easter/easter_riddle.json").open("r", encoding="utf8") as f:
- RIDDLE_QUESTIONS = load(f)
+RIDDLE_QUESTIONS = loads(Path("bot/resources/easter/easter_riddle.json").read_text("utf8"))
TIMELIMIT = 10
@@ -20,13 +20,13 @@ TIMELIMIT = 10
class EasterRiddle(commands.Cog):
"""This cog contains the command for the Easter quiz!"""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
self.winners = set()
self.correct = ""
self.current_channel = None
- @commands.command(aliases=["riddlemethis", "riddleme"])
+ @commands.command(aliases=("riddlemethis", "riddleme"))
async def riddle(self, ctx: commands.Context) -> None:
"""
Gives a random riddle, then provides 2 hints at certain intervals before revealing the answer.
@@ -34,7 +34,8 @@ class EasterRiddle(commands.Cog):
The duration of the hint interval can be configured by changing the TIMELIMIT constant in this file.
"""
if self.current_channel:
- return await ctx.send(f"A riddle is already being solved in {self.current_channel.mention}!")
+ await ctx.send(f"A riddle is already being solved in {self.current_channel.mention}!")
+ return
# Don't let users start in a DM
if not ctx.guild:
@@ -47,7 +48,7 @@ class EasterRiddle(commands.Cog):
)
return
- self.current_channel = ctx.message.channel
+ self.current_channel = ctx.channel
random_question = random.choice(RIDDLE_QUESTIONS)
question = random_question["question"]
@@ -106,6 +107,6 @@ class EasterRiddle(commands.Cog):
self.winners.add(message.author.mention)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Easter Riddle Cog load."""
bot.add_cog(EasterRiddle(bot))
diff --git a/bot/exts/easter/egg_decorating.py b/bot/exts/easter/egg_decorating.py
index a847388d..fd7620d4 100644
--- a/bot/exts/easter/egg_decorating.py
+++ b/bot/exts/easter/egg_decorating.py
@@ -10,15 +10,14 @@ import discord
from PIL import Image
from discord.ext import commands
+from bot.bot import Bot
from bot.utils import helpers
log = logging.getLogger(__name__)
-with open(Path("bot/resources/evergreen/html_colours.json"), encoding="utf8") as f:
- HTML_COLOURS = json.load(f)
+HTML_COLOURS = json.loads(Path("bot/resources/evergreen/html_colours.json").read_text("utf8"))
-with open(Path("bot/resources/evergreen/xkcd_colours.json"), encoding="utf8") as f:
- XKCD_COLOURS = json.load(f)
+XKCD_COLOURS = json.loads(Path("bot/resources/evergreen/xkcd_colours.json").read_text("utf8"))
COLOURS = [
(255, 0, 0, 255), (255, 128, 0, 255), (255, 255, 0, 255), (0, 255, 0, 255),
@@ -33,9 +32,6 @@ IRREPLACEABLE = [
class EggDecorating(commands.Cog):
"""Decorate some easter eggs!"""
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
-
@staticmethod
def replace_invalid(colour: str) -> Union[int, None]:
"""Attempts to match with HTML or XKCD colour names, returning the int value."""
@@ -45,10 +41,10 @@ class EggDecorating(commands.Cog):
return int(XKCD_COLOURS[colour], 16)
return None
- @commands.command(aliases=["decorateegg"])
+ @commands.command(aliases=("decorateegg",))
async def eggdecorate(
self, ctx: commands.Context, *colours: Union[discord.Colour, str]
- ) -> Union[Image.Image, discord.Message]:
+ ) -> Union[Image.Image, None]:
"""
Picks a random egg design and decorates it using the given colours.
@@ -56,7 +52,8 @@ class EggDecorating(commands.Cog):
Discord colour names, HTML colour names, XKCD colour names and hex values are accepted.
"""
if len(colours) < 2:
- return await ctx.send("You must include at least 2 colours!")
+ await ctx.send("You must include at least 2 colours!")
+ return
invalid = []
colours = list(colours)
@@ -70,9 +67,11 @@ class EggDecorating(commands.Cog):
invalid.append(helpers.suppress_links(colour))
if len(invalid) > 1:
- return await ctx.send(f"Sorry, I don't know these colours: {' '.join(invalid)}")
+ await ctx.send(f"Sorry, I don't know these colours: {' '.join(invalid)}")
+ return
elif len(invalid) == 1:
- return await ctx.send(f"Sorry, I don't know the colour {invalid[0]}!")
+ await ctx.send(f"Sorry, I don't know the colour {invalid[0]}!")
+ return
async with ctx.typing():
# Expand list to 8 colours
@@ -115,6 +114,6 @@ class EggDecorating(commands.Cog):
return new_im
-def setup(bot: commands.bot) -> None:
- """Egg decorating Cog load."""
- bot.add_cog(EggDecorating(bot))
+def setup(bot: Bot) -> None:
+ """Load the Egg decorating Cog."""
+ bot.add_cog(EggDecorating())
diff --git a/bot/exts/easter/egg_facts.py b/bot/exts/easter/egg_facts.py
index 761e9059..486e735f 100644
--- a/bot/exts/easter/egg_facts.py
+++ b/bot/exts/easter/egg_facts.py
@@ -1,6 +1,6 @@
import logging
import random
-from json import load
+from json import loads
from pathlib import Path
import discord
@@ -12,6 +12,8 @@ from bot.utils.decorators import seasonal_task
log = logging.getLogger(__name__)
+EGG_FACTS = loads(Path("bot/resources/easter/easter_egg_facts.json").read_text("utf8"))
+
class EasterFacts(commands.Cog):
"""
@@ -22,17 +24,8 @@ class EasterFacts(commands.Cog):
def __init__(self, bot: Bot):
self.bot = bot
- self.facts = self.load_json()
-
self.daily_fact_task = self.bot.loop.create_task(self.send_egg_fact_daily())
- @staticmethod
- def load_json() -> dict:
- """Load a list of easter egg facts from the resource JSON file."""
- p = Path("bot/resources/easter/easter_egg_facts.json")
- with p.open(encoding="utf8") as f:
- return load(f)
-
@seasonal_task(Month.APRIL)
async def send_egg_fact_daily(self) -> None:
"""A background task that sends an easter egg fact in the event channel everyday."""
@@ -41,21 +34,22 @@ class EasterFacts(commands.Cog):
channel = self.bot.get_channel(Channels.community_bot_commands)
await channel.send(embed=self.make_embed())
- @commands.command(name='eggfact', aliases=['fact'])
+ @commands.command(name="eggfact", aliases=("fact",))
async def easter_facts(self, ctx: commands.Context) -> None:
"""Get easter egg facts."""
embed = self.make_embed()
await ctx.send(embed=embed)
- def make_embed(self) -> discord.Embed:
+ @staticmethod
+ def make_embed() -> discord.Embed:
"""Makes a nice embed for the message to be sent."""
return discord.Embed(
colour=Colours.soft_red,
title="Easter Egg Fact",
- description=random.choice(self.facts)
+ description=random.choice(EGG_FACTS)
)
def setup(bot: Bot) -> None:
- """Easter Egg facts cog load."""
+ """Load the Easter Egg facts Cog."""
bot.add_cog(EasterFacts(bot))
diff --git a/bot/exts/easter/egghead_quiz.py b/bot/exts/easter/egghead_quiz.py
index 0498d9db..7c4960cd 100644
--- a/bot/exts/easter/egghead_quiz.py
+++ b/bot/exts/easter/egghead_quiz.py
@@ -1,28 +1,28 @@
import asyncio
import logging
import random
-from json import load
+from json import loads
from pathlib import Path
from typing import Union
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-with open(Path("bot/resources/easter/egghead_questions.json"), "r", encoding="utf8") as f:
- EGGHEAD_QUESTIONS = load(f)
+EGGHEAD_QUESTIONS = loads(Path("bot/resources/easter/egghead_questions.json").read_text("utf8"))
EMOJIS = [
- '\U0001f1e6', '\U0001f1e7', '\U0001f1e8', '\U0001f1e9', '\U0001f1ea',
- '\U0001f1eb', '\U0001f1ec', '\U0001f1ed', '\U0001f1ee', '\U0001f1ef',
- '\U0001f1f0', '\U0001f1f1', '\U0001f1f2', '\U0001f1f3', '\U0001f1f4',
- '\U0001f1f5', '\U0001f1f6', '\U0001f1f7', '\U0001f1f8', '\U0001f1f9',
- '\U0001f1fa', '\U0001f1fb', '\U0001f1fc', '\U0001f1fd', '\U0001f1fe',
- '\U0001f1ff'
+ "\U0001f1e6", "\U0001f1e7", "\U0001f1e8", "\U0001f1e9", "\U0001f1ea",
+ "\U0001f1eb", "\U0001f1ec", "\U0001f1ed", "\U0001f1ee", "\U0001f1ef",
+ "\U0001f1f0", "\U0001f1f1", "\U0001f1f2", "\U0001f1f3", "\U0001f1f4",
+ "\U0001f1f5", "\U0001f1f6", "\U0001f1f7", "\U0001f1f8", "\U0001f1f9",
+ "\U0001f1fa", "\U0001f1fb", "\U0001f1fc", "\U0001f1fd", "\U0001f1fe",
+ "\U0001f1ff"
] # Regional Indicators A-Z (used for voting)
TIMELIMIT = 30
@@ -31,11 +31,10 @@ TIMELIMIT = 30
class EggheadQuiz(commands.Cog):
"""This cog contains the command for the Easter quiz!"""
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
+ def __init__(self) -> None:
self.quiz_messages = {}
- @commands.command(aliases=["eggheadquiz", "easterquiz"])
+ @commands.command(aliases=("eggheadquiz", "easterquiz"))
async def eggquiz(self, ctx: commands.Context) -> None:
"""
Gives a random quiz question, waits 30 seconds and then outputs the answer.
@@ -64,7 +63,7 @@ class EggheadQuiz(commands.Cog):
del self.quiz_messages[msg.id]
- msg = await ctx.channel.fetch_message(msg.id) # Refreshes message
+ msg = await ctx.fetch_message(msg.id) # Refreshes message
total_no = sum([len(await r.users().flatten()) for r in msg.reactions]) - len(valid_emojis) # - bot's reactions
@@ -114,6 +113,6 @@ class EggheadQuiz(commands.Cog):
return await reaction.message.remove_reaction(reaction, user)
-def setup(bot: commands.Bot) -> None:
- """Egghead Quiz Cog load."""
- bot.add_cog(EggheadQuiz(bot))
+def setup(bot: Bot) -> None:
+ """Load the Egghead Quiz Cog."""
+ bot.add_cog(EggheadQuiz())
diff --git a/bot/exts/easter/save_the_planet.py b/bot/exts/easter/save_the_planet.py
index 8f644259..1bd515f2 100644
--- a/bot/exts/easter/save_the_planet.py
+++ b/bot/exts/easter/save_the_planet.py
@@ -4,26 +4,22 @@ from pathlib import Path
from discord import Embed
from discord.ext import commands
+from bot.bot import Bot
from bot.utils.randomization import RandomCycle
-
-with Path("bot/resources/easter/save_the_planet.json").open('r', encoding='utf8') as f:
- EMBED_DATA = RandomCycle(json.load(f))
+EMBED_DATA = RandomCycle(json.loads(Path("bot/resources/easter/save_the_planet.json").read_text("utf8")))
class SaveThePlanet(commands.Cog):
"""A cog that teaches users how they can help our planet."""
- def __init__(self, bot: commands.Bot) -> None:
- self.bot = bot
-
- @commands.command(aliases=('savetheearth', 'saveplanet', 'saveearth'))
+ @commands.command(aliases=("savetheearth", "saveplanet", "saveearth"))
async def savetheplanet(self, ctx: commands.Context) -> None:
"""Responds with a random tip on how to be eco-friendly and help our planet."""
return_embed = Embed.from_dict(next(EMBED_DATA))
await ctx.send(embed=return_embed)
-def setup(bot: commands.Bot) -> None:
- """Save the Planet Cog load."""
- bot.add_cog(SaveThePlanet(bot))
+def setup(bot: Bot) -> None:
+ """Load the Save the Planet Cog."""
+ bot.add_cog(SaveThePlanet())
diff --git a/bot/exts/easter/traditions.py b/bot/exts/easter/traditions.py
index 85b4adfb..93404f3e 100644
--- a/bot/exts/easter/traditions.py
+++ b/bot/exts/easter/traditions.py
@@ -5,19 +5,17 @@ from pathlib import Path
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-with open(Path("bot/resources/easter/traditions.json"), "r", encoding="utf8") as f:
- traditions = json.load(f)
+traditions = json.loads(Path("bot/resources/easter/traditions.json").read_text("utf8"))
class Traditions(commands.Cog):
"""A cog which allows users to get a random easter tradition or custom from a random country."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=('eastercustoms',))
+ @commands.command(aliases=("eastercustoms",))
async def easter_tradition(self, ctx: commands.Context) -> None:
"""Responds with a random tradition or custom."""
random_country = random.choice(list(traditions))
@@ -25,6 +23,6 @@ class Traditions(commands.Cog):
await ctx.send(f"{random_country}:\n{traditions[random_country]}")
-def setup(bot: commands.Bot) -> None:
- """Traditions Cog load."""
- bot.add_cog(Traditions(bot))
+def setup(bot: Bot) -> None:
+ """Load the Traditions Cog."""
+ bot.add_cog(Traditions())
diff --git a/bot/exts/evergreen/avatar_modification/_effects.py b/bot/exts/evergreen/avatar_modification/_effects.py
index d2370b4b..b53b26f3 100644
--- a/bot/exts/evergreen/avatar_modification/_effects.py
+++ b/bot/exts/evergreen/avatar_modification/_effects.py
@@ -14,7 +14,7 @@ class PfpEffects:
"""
Implements various image modifying effects, for the PfpModify cog.
- All of these fuctions are slow, and blocking, so they should be ran in executors.
+ All of these functions are slow, and blocking, so they should be ran in executors.
"""
@staticmethod
@@ -102,7 +102,7 @@ class PfpEffects:
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 RGBvalue.
+ to the half-way RGB value.
We also then add an overlay image on top in middle right, a chocolate bunny by default.
"""
@@ -251,7 +251,7 @@ class PfpEffects:
total_width = multiplier * single_wdith
total_height = multiplier * single_height
- new_image = Image.new('RGBA', (total_width, total_height), (250, 250, 250))
+ new_image = Image.new("RGBA", (total_width, total_height), (250, 250, 250))
width_multiplier = 0
height = 0
@@ -273,15 +273,15 @@ class PfpEffects:
@staticmethod
def mosaic_effect(img_bytes: bytes, squares: int, file_name: str) -> discord.File:
- """Seperate function run from an executor which turns an image into a mosaic."""
+ """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))
+ 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')
+ 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
index 693d15c7..17f34ed4 100644
--- a/bot/exts/evergreen/avatar_modification/avatar_modify.py
+++ b/bot/exts/evergreen/avatar_modification/avatar_modify.py
@@ -6,12 +6,13 @@ 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 discord.ext.commands.errors import BadArgument
+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
@@ -27,13 +28,12 @@ MAX_SQUARES = 10_000
T = t.TypeVar("T")
-with open("bot/resources/pride/gender_options.json") as f:
- GENDER_OPTIONS = json.load(f)
+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 synchronus function `func` in an executor.
+ 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.
@@ -63,7 +63,7 @@ def file_safe_name(effect: str, display_name: str) -> str:
class AvatarModify(commands.Cog):
"""Various commands for users to apply affects to their own avatars."""
- def __init__(self, bot: commands.Bot) -> None:
+ def __init__(self, bot: Bot) -> None:
self.bot = bot
async def _fetch_user(self, user_id: int) -> t.Optional[discord.User]:
@@ -71,7 +71,7 @@ class AvatarModify(commands.Cog):
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 delted from the Discord CDN.
+ 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
"""
@@ -260,9 +260,9 @@ class AvatarModify(commands.Cog):
return
image_bytes = await response.read()
except client_exceptions.ClientConnectorError:
- raise BadArgument("Cannot connect to provided URL!")
+ raise commands.BadArgument("Cannot connect to provided URL!")
except client_exceptions.InvalidURL:
- raise BadArgument("Invalid URL!")
+ raise commands.BadArgument("Invalid URL!")
await self.send_pride_image(ctx, image_bytes, pixels, flag, option)
@@ -365,6 +365,6 @@ class AvatarModify(commands.Cog):
await ctx.send(file=file, embed=embed)
-def setup(bot: commands.Bot) -> None:
+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 7a97a40d..85c9b46f 100644
--- a/bot/exts/evergreen/bookmark.py
+++ b/bot/exts/evergreen/bookmark.py
@@ -1,6 +1,7 @@
import asyncio
import logging
import random
+import typing as t
import discord
from discord.ext import commands
@@ -88,15 +89,20 @@ class Bookmark(commands.Cog):
async def bookmark(
self,
ctx: commands.Context,
- target_message: WrappedMessageConverter,
+ target_message: t.Optional[WrappedMessageConverter],
*,
title: str = "Bookmark"
) -> None:
"""Send the author a link to `target_message` via DMs."""
+ if not target_message:
+ if not ctx.message.reference:
+ raise commands.UserInputError("You must either provide a valid message to bookmark, or reply to one.")
+ target_message = ctx.message.reference.resolved
+
# 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,
diff --git a/bot/exts/evergreen/catify.py b/bot/exts/evergreen/catify.py
index a175602f..32dfae09 100644
--- a/bot/exts/evergreen/catify.py
+++ b/bot/exts/evergreen/catify.py
@@ -5,6 +5,7 @@ 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
@@ -12,10 +13,7 @@ from bot.utils import helpers
class Catify(commands.Cog):
"""Cog for the catify command."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=["ᓚᘏᗢify", "ᓚᘏᗢ"])
+ @commands.command(aliases=("ᓚᘏᗢify", "ᓚᘏᗢ"))
@commands.cooldown(1, 5, commands.BucketType.user)
async def catify(self, ctx: commands.Context, *, text: Optional[str]) -> None:
"""
@@ -83,6 +81,6 @@ class Catify(commands.Cog):
)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Loads the catify cog."""
- bot.add_cog(Catify(bot))
+ 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..5873fb83 100644
--- a/bot/exts/evergreen/error_handler.py
+++ b/bot/exts/evergreen/error_handler.py
@@ -1,3 +1,4 @@
+import difflib
import logging
import math
import random
@@ -7,17 +8,21 @@ from discord import Embed, Message
from discord.ext import commands
from sentry_sdk import push_scope
-from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES
+from bot.bot import Bot
+from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES, RedirectOutput
from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure
from bot.utils.exceptions import UserNotPlayingError
log = logging.getLogger(__name__)
+QUESTION_MARK_ICON = "https://cdn.discordapp.com/emojis/512367613339369475.png"
+
+
class CommandErrorHandler(commands.Cog):
"""A error handler for the PythonDiscord server."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot) -> None:
self.bot = bot
@staticmethod
@@ -41,8 +46,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 +56,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}, "
@@ -60,6 +65,7 @@ class CommandErrorHandler(commands.Cog):
)
if isinstance(error, commands.CommandNotFound):
+ await self.send_command_suggestion(ctx, ctx.invoked_with)
return
if isinstance(error, (InChannelCheckFailure, InMonthCheckFailure)):
@@ -127,14 +133,40 @@ 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."""
+ async def send_command_suggestion(self, ctx: commands.Context, command_name: str) -> None:
+ """Sends user similar commands if any can be found."""
+ raw_commands = []
+ for cmd in self.bot.walk_commands():
+ if not cmd.hidden:
+ raw_commands += (cmd.name, *cmd.aliases)
+ if similar_command_data := difflib.get_close_matches(command_name, raw_commands, 1):
+ similar_command_name = similar_command_data[0]
+ similar_command = self.bot.get_command(similar_command_name)
+
+ if not similar_command:
+ return
+
+ log_msg = "Cancelling attempt to suggest a command due to failed checks."
+ try:
+ if not await similar_command.can_run(ctx):
+ log.debug(log_msg)
+ return
+ except commands.errors.CommandError as cmd_error:
+ log.debug(log_msg)
+ await self.on_command_error(ctx, cmd_error)
+ return
+
+ misspelled_content = ctx.message.content
+ e = Embed()
+ e.set_author(name="Did you mean:", icon_url=QUESTION_MARK_ICON)
+ e.description = misspelled_content.replace(command_name, similar_command_name, 1)
+ await ctx.send(embed=e, delete_after=RedirectOutput.delete_delay)
+
+
+def setup(bot: Bot) -> None:
+ """Load the ErrorHandler cog."""
bot.add_cog(CommandErrorHandler(bot))
diff --git a/bot/exts/evergreen/fun.py b/bot/exts/evergreen/fun.py
index 7152d0cb..3b266e1b 100644
--- a/bot/exts/evergreen/fun.py
+++ b/bot/exts/evergreen/fun.py
@@ -7,9 +7,10 @@ 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
@@ -55,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:
@@ -242,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 f557e42e..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,141 +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 = [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')
+ 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."""
@@ -412,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
@@ -496,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:
@@ -507,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)
@@ -519,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)
@@ -534,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 07c13524..6be78117 100644
--- a/bot/exts/evergreen/ping.py
+++ b/bot/exts/evergreen/ping.py
@@ -4,13 +4,14 @@ 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):
"""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")
@@ -39,6 +40,6 @@ class Ping(commands.Cog):
await ctx.send(f"I started up {uptime_string}.")
-def setup(bot: commands.Bot) -> None:
+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/rps.py b/bot/exts/evergreen/rps.py
new file mode 100644
index 00000000..c6bbff46
--- /dev/null
+++ b/bot/exts/evergreen/rps.py
@@ -0,0 +1,57 @@
+from random import choice
+
+from discord.ext import commands
+
+from bot.bot import Bot
+
+CHOICES = ["rock", "paper", "scissors"]
+SHORT_CHOICES = ["r", "p", "s"]
+
+# Using a dictionary instead of conditions to check for the winner.
+WINNER_DICT = {
+ "r": {
+ "r": 0,
+ "p": -1,
+ "s": 1,
+ },
+ "p": {
+ "r": 1,
+ "p": 0,
+ "s": -1,
+ },
+ "s": {
+ "r": -1,
+ "p": 1,
+ "s": 0,
+ }
+}
+
+
+class RPS(commands.Cog):
+ """Rock Paper Scissors. The Classic Game!"""
+
+ @commands.command(case_insensitive=True)
+ async def rps(self, ctx: commands.Context, move: str) -> None:
+ """Play the classic game of Rock Paper Scissors with your own sir-lancebot!"""
+ move = move.lower()
+ player_mention = ctx.author.mention
+
+ if move not in CHOICES and move not in SHORT_CHOICES:
+ raise commands.BadArgument(f"Invalid move. Please make move from options: {', '.join(CHOICES).upper()}.")
+
+ bot_move = choice(CHOICES)
+ # value of player_result will be from (-1, 0, 1) as (lost, tied, won).
+ player_result = WINNER_DICT[move[0]][bot_move[0]]
+
+ if player_result == 0:
+ message_string = f"{player_mention} You and Sir Lancebot played {bot_move}, it's a tie."
+ await ctx.send(message_string)
+ elif player_result == 1:
+ await ctx.send(f"Sir Lancebot played {bot_move}! {player_mention} won!")
+ else:
+ await ctx.send(f"Sir Lancebot played {bot_move}! {player_mention} lost!")
+
+
+def setup(bot: Bot) -> None:
+ """Load the RPS Cog."""
+ bot.add_cog(RPS(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..fc209bc3 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."""
@@ -54,7 +33,8 @@ class BotSource(commands.Cog):
Raise BadArgument if `source_item` is a dynamically-created object (e.g. via internal eval).
"""
if isinstance(source_item, commands.Command):
- src = source_item.callback.__code__
+ callback = inspect.unwrap(source_item.callback)
+ src = callback.__code__
filename = src.co_filename
else:
src = type(source_item)
@@ -85,12 +65,8 @@ 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':
- title = "Help Command"
- description = source_object.__doc__.splitlines()[1]
- else:
- description = source_object.short_doc
- title = f"Command: {source_object.qualified_name}"
+ description = source_object.short_doc
+ title = f"Command: {source_object.qualified_name}"
else:
title = f"Cog: {source_object.qualified_name}"
description = source_object.description.splitlines()[0]
@@ -104,6 +80,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..181c71ce 100644
--- a/bot/exts/evergreen/status_codes.py
+++ b/bot/exts/evergreen/status_codes.py
@@ -1,30 +1,35 @@
from http import HTTPStatus
+from random import choice
import discord
from discord.ext import commands
-from bot.utils.extensions import invoke_help_command
+from bot.bot import Bot
HTTP_DOG_URL = "https://httpstatusdogs.com/img/{code}.jpg"
HTTP_CAT_URL = "https://http.cat/{code}.jpg"
class HTTPStatusCodes(commands.Cog):
- """Commands that give HTTP statuses described and visualized by cats and dogs."""
+ """
+ Fetch an image depicting HTTP status codes as a dog or a cat.
- def __init__(self, bot: commands.Bot):
+ If neither animal is selected a cat or dog is chosen randomly for the given status code.
+ """
+
+ def __init__(self, bot: Bot):
self.bot = bot
- @commands.group(name="http_status", aliases=("status", "httpstatus"))
- async def http_status_group(self, ctx: commands.Context) -> None:
- """Group containing dog and cat http status code commands."""
- if not ctx.invoked_subcommand:
- await invoke_help_command(ctx)
+ @commands.group(name="http_status", aliases=("status", "httpstatus"), invoke_without_command=True)
+ async def http_status_group(self, ctx: commands.Context, code: int) -> None:
+ """Choose a cat or dog randomly for the given status code."""
+ subcmd = choice((self.http_cat, self.http_dog))
+ await subcmd(ctx, code)
- @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 +41,23 @@ 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}**')
+ # These codes aren't server-friendly.
+ if code in (304, 422):
+ await self.http_cat(ctx, code)
+ return
+
+ embed = discord.Embed(title=f"**Status: {code}**")
url = HTTP_DOG_URL.format(code=code)
try:
@@ -59,15 +69,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 6c33ebc4..48e8e142 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()
@@ -327,5 +326,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..a8d10afd 100644
--- a/bot/exts/evergreen/trivia_quiz.py
+++ b/bot/exts/evergreen/trivia_quiz.py
@@ -1,56 +1,235 @@
import asyncio
import json
import logging
+import operator
import random
+from dataclasses import dataclass
from pathlib import Path
+from typing import Callable, List, Optional
import discord
from discord.ext import commands
from fuzzywuzzy import fuzz
-from bot.constants import Roles
-
+from bot.bot import Bot
+from bot.constants import Colours, NEGATIVE_REPLIES, Roles
logger = logging.getLogger(__name__)
+DEFAULT_QUESTION_LIMIT = 6
+STANDARD_VARIATION_TOLERANCE = 83
+DYNAMICALLY_GEN_VARIATION_TOLERANCE = 95
WRONG_ANS_RESPONSE = [
"No one answered correctly!",
- "Better luck next time"
+ "Better luck next time...",
+]
+
+N_PREFIX_STARTS_AT = 5
+N_PREFIXES = [
+ "penta", "hexa", "hepta", "octa", "nona",
+ "deca", "hendeca", "dodeca", "trideca", "tetradeca",
+]
+
+PLANETS = [
+ ("1st", "Mercury"),
+ ("2nd", "Venus"),
+ ("3rd", "Earth"),
+ ("4th", "Mars"),
+ ("5th", "Jupiter"),
+ ("6th", "Saturn"),
+ ("7th", "Uranus"),
+ ("8th", "Neptune"),
+]
+
+TAXONOMIC_HIERARCHY = [
+ "species", "genus", "family", "order",
+ "class", "phylum", "kingdom", "domain",
]
+UNITS_TO_BASE_UNITS = {
+ "hertz": ("(unit of frequency)", "s^-1"),
+ "newton": ("(unit of force)", "m*kg*s^-2"),
+ "pascal": ("(unit of pressure & stress)", "m^-1*kg*s^-2"),
+ "joule": ("(unit of energy & quantity of heat)", "m^2*kg*s^-2"),
+ "watt": ("(unit of power)", "m^2*kg*s^-3"),
+ "coulomb": ("(unit of electric charge & quantity of electricity)", "s*A"),
+ "volt": ("(unit of voltage & electromotive force)", "m^2*kg*s^-3*A^-1"),
+ "farad": ("(unit of capacitance)", "m^-2*kg^-1*s^4*A^2"),
+ "ohm": ("(unit of electric resistance)", "m^2*kg*s^-3*A^-2"),
+ "weber": ("(unit of magnetic flux)", "m^2*kg*s^-2*A^-1"),
+ "tesla": ("(unit of magnetic flux density)", "kg*s^-2*A^-1"),
+}
+
+
+@dataclass(frozen=True)
+class QuizEntry:
+ """Dataclass for a quiz entry (a question and a string containing answers separated by commas)."""
+
+ question: str
+ answer: str
+
+
+def linear_system(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a system of linear equations with two unknowns."""
+ x, y = random.randint(2, 5), random.randint(2, 5)
+ answer = a_format.format(x, y)
+
+ coeffs = random.sample(range(1, 6), 4)
+
+ question = q_format.format(
+ coeffs[0],
+ coeffs[1],
+ coeffs[0] * x + coeffs[1] * y,
+ coeffs[2],
+ coeffs[3],
+ coeffs[2] * x + coeffs[3] * y,
+ )
+
+ return QuizEntry(question, answer)
+
+
+def mod_arith(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a basic modular arithmetic question."""
+ quotient, m, b = random.randint(30, 40), random.randint(10, 20), random.randint(200, 350)
+ ans = random.randint(0, 9) # max remainder is 9, since the minimum modulus is 10
+ a = quotient * m + ans - b
+
+ question = q_format.format(a, b, m)
+ answer = a_format.format(ans)
+
+ return QuizEntry(question, answer)
+
+
+def ngonal_prism(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a question regarding vertices on n-gonal prisms."""
+ n = random.randint(0, len(N_PREFIXES) - 1)
+
+ question = q_format.format(N_PREFIXES[n])
+ answer = a_format.format((n + N_PREFIX_STARTS_AT) * 2)
+
+ return QuizEntry(question, answer)
+
+
+def imag_sqrt(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a negative square root question."""
+ ans_coeff = random.randint(3, 10)
+
+ question = q_format.format(ans_coeff ** 2)
+ answer = a_format.format(ans_coeff)
+
+ return QuizEntry(question, answer)
+
+
+def binary_calc(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a binary calculation question."""
+ a = random.randint(15, 20)
+ b = random.randint(10, a)
+ oper = random.choice(
+ (
+ ("+", operator.add),
+ ("-", operator.sub),
+ ("*", operator.mul),
+ )
+ )
+
+ # if the operator is multiplication, lower the values of the two operands to make it easier
+ if oper[0] == "*":
+ a -= 5
+ b -= 5
+
+ question = q_format.format(a, oper[0], b)
+ answer = a_format.format(oper[1](a, b))
+
+ return QuizEntry(question, answer)
+
+
+def solar_system(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a question on the planets of the Solar System."""
+ planet = random.choice(PLANETS)
+
+ question = q_format.format(planet[0])
+ answer = a_format.format(planet[1])
+
+ return QuizEntry(question, answer)
+
+
+def taxonomic_rank(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a question on taxonomic classification."""
+ level = random.randint(0, len(TAXONOMIC_HIERARCHY) - 2)
+
+ question = q_format.format(TAXONOMIC_HIERARCHY[level])
+ answer = a_format.format(TAXONOMIC_HIERARCHY[level + 1])
+
+ return QuizEntry(question, answer)
+
+
+def base_units_convert(q_format: str, a_format: str) -> QuizEntry:
+ """Generate a SI base units conversion question."""
+ unit = random.choice(list(UNITS_TO_BASE_UNITS))
+
+ question = q_format.format(
+ unit + " " + UNITS_TO_BASE_UNITS[unit][0]
+ )
+ answer = a_format.format(
+ UNITS_TO_BASE_UNITS[unit][1]
+ )
+
+ return QuizEntry(question, answer)
+
+
+DYNAMIC_QUESTIONS_FORMAT_FUNCS = {
+ 201: linear_system,
+ 202: mod_arith,
+ 203: ngonal_prism,
+ 204: imag_sqrt,
+ 205: binary_calc,
+ 301: solar_system,
+ 302: taxonomic_rank,
+ 303: base_units_convert,
+}
+
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.
self.game_owners = {} # A variable to store the person's ID who started the quiz game in a channel.
- self.question_limit = 4
+
+ self.questions = self.load_questions()
+ self.question_limit = 0
+
self.player_scores = {} # A variable to store all player's scores for a bot session.
self.game_player_scores = {} # A variable to store temporary game player's scores.
+
self.categories = {
- "general": "Test your general knowledge"
- # "retro": "Questions related to retro gaming."
+ "general": "Test your general knowledge.",
+ "retro": "Questions related to retro gaming.",
+ "math": "General questions about mathematics ranging from grade 8 to grade 12.",
+ "science": "Put your understanding of science to the test!",
}
@staticmethod
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(encoding="utf-8"))
@commands.group(name="quiz", aliases=["trivia"], invoke_without_command=True)
- async def quiz_game(self, ctx: commands.Context, category: str = None) -> None:
+ async def quiz_game(self, ctx: commands.Context, category: Optional[str], questions: Optional[int]) -> None:
"""
Start a quiz!
Questions for the quiz can be selected from the following categories:
- - general : Test your general knowledge. (default)
+ - general: Test your general knowledge. (default)
+ - retro: Questions related to retro gaming.
+ - math: General questions about mathematics ranging from grade 8 to grade 12.
+ - science: Put your understanding of science to the test!
+
(More to come!)
"""
if ctx.channel.id not in self.game_status:
@@ -60,11 +239,12 @@ class TriviaQuiz(commands.Cog):
self.game_player_scores[ctx.channel.id] = {}
# Stop game if running.
- if self.game_status[ctx.channel.id] is True:
- return await ctx.send(
- f"Game is already running..."
+ if self.game_status[ctx.channel.id]:
+ await ctx.send(
+ "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:
@@ -76,8 +256,35 @@ class TriviaQuiz(commands.Cog):
await ctx.send(embed=embed)
return
+ topic = self.questions[category]
+ topic_length = len(topic)
+
+ if questions is None:
+ self.question_limit = DEFAULT_QUESTION_LIMIT
+ else:
+ if questions > topic_length:
+ await ctx.send(
+ embed=self.make_error_embed(
+ f"This category only has {topic_length} questions. "
+ "Please input a lower value!"
+ )
+ )
+ return
+
+ elif questions < 1:
+ await ctx.send(
+ embed=self.make_error_embed(
+ "You must choose to complete at least one question. "
+ f"(or enter nothing for the default value of {DEFAULT_QUESTION_LIMIT + 1} questions)"
+ )
+ )
+ return
+
+ else:
+ self.question_limit = questions - 1
+
# Start game if not running.
- if self.game_status[ctx.channel.id] is False:
+ if not self.game_status[ctx.channel.id]:
self.game_owners[ctx.channel.id] = ctx.author
self.game_status[ctx.channel.id] = True
start_embed = self.make_start_embed(category)
@@ -85,11 +292,10 @@ class TriviaQuiz(commands.Cog):
await ctx.send(embed=start_embed) # send an embed with the rules
await asyncio.sleep(1)
- topic = self.questions[category]
-
done_question = []
hint_no = 0
- answer = None
+ answers = None
+
while self.game_status[ctx.channel.id]:
# Exit quiz if number of questions for a round are already sent.
if len(done_question) > self.question_limit and hint_no == 0:
@@ -111,34 +317,58 @@ class TriviaQuiz(commands.Cog):
done_question.append(question_dict["id"])
break
- q = question_dict["question"]
- answer = question_dict["answer"]
+ if "dynamic_id" not in question_dict:
+ question = question_dict["question"]
+ answers = question_dict["answer"].split(", ")
+
+ var_tol = STANDARD_VARIATION_TOLERANCE
+ else:
+ format_func = DYNAMIC_QUESTIONS_FORMAT_FUNCS[question_dict["dynamic_id"]]
+
+ quiz_entry = format_func(
+ question_dict["question"],
+ question_dict["answer"],
+ )
- embed = discord.Embed(colour=discord.Colour.gold())
- embed.title = f"Question #{len(done_question)}"
- embed.description = q
- await ctx.send(embed=embed) # Send question embed.
+ question, answers = quiz_entry.question, quiz_entry.answer
+ answers = [answers]
- # A function to check whether user input is the correct answer(close to the right answer)
- def check(m: discord.Message) -> bool:
- return (
- m.channel == ctx.channel
- and fuzz.ratio(answer.lower(), m.content.lower()) > 85
+ var_tol = DYNAMICALLY_GEN_VARIATION_TOLERANCE
+
+ embed = discord.Embed(
+ colour=Colours.gold,
+ title=f"Question #{len(done_question)}",
+ description=question,
)
+ if img_url := question_dict.get("img_url"):
+ embed.set_image(url=img_url)
+
+ await ctx.send(embed=embed)
+
+ def check_func(variation_tolerance: int) -> Callable[[discord.Message], bool]:
+ def contains_correct_answer(m: discord.Message) -> bool:
+ return m.channel == ctx.channel and any(
+ fuzz.ratio(answer.lower(), m.content.lower()) > variation_tolerance
+ for answer in answers
+ )
+
+ return contains_correct_answer
+
try:
- msg = await self.bot.wait_for('message', check=check, timeout=10)
+ msg = await self.bot.wait_for("message", check=check_func(var_tol), 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:
+ if not self.game_status[ctx.channel.id]:
break
- # if number of hints sent or time alerts sent is less than 2, then send one.
if hint_no < 2:
hint_no += 1
+
if "hints" in question_dict:
hints = question_dict["hints"]
- await ctx.send(f"**Hint #{hint_no+1}\n**{hints[hint_no]}")
+
+ await ctx.send(f"**Hint #{hint_no}\n**{hints[hint_no - 1]}")
else:
await ctx.send(f"{30 - hint_no * 10}s left!")
@@ -151,10 +381,17 @@ class TriviaQuiz(commands.Cog):
response = random.choice(WRONG_ANS_RESPONSE)
await ctx.send(response)
- await self.send_answer(ctx.channel, question_dict)
+
+ await self.send_answer(
+ ctx.channel,
+ answers,
+ False,
+ question_dict,
+ self.question_limit - len(done_question) + 1,
+ )
await asyncio.sleep(1)
- hint_no = 0 # init hint_no = 0 so that 2 hints/time alerts can be sent for the new question.
+ hint_no = 0 # Reset the hint counter so that on the next round, it's in the initial state
await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id])
await asyncio.sleep(2)
@@ -162,8 +399,7 @@ class TriviaQuiz(commands.Cog):
if self.game_status[ctx.channel.id] is False:
break
- # Reduce points by 25 for every hint/time alert that has been sent.
- points = 100 - 25*hint_no
+ points = 100 - 25 * hint_no
if msg.author in self.game_player_scores[ctx.channel.id]:
self.game_player_scores[ctx.channel.id][msg.author] += points
else:
@@ -178,23 +414,50 @@ class TriviaQuiz(commands.Cog):
hint_no = 0
await ctx.send(f"{msg.author.mention} got the correct answer :tada: {points} points!")
- await self.send_answer(ctx.channel, question_dict)
+
+ await self.send_answer(
+ ctx.channel,
+ answers,
+ True,
+ question_dict,
+ self.question_limit - len(done_question) + 1,
+ )
await self.send_score(ctx.channel, self.game_player_scores[ctx.channel.id])
+
await asyncio.sleep(2)
- @staticmethod
- def make_start_embed(category: str) -> discord.Embed:
+ def make_start_embed(self, category: str) -> discord.Embed:
"""Generate a starting/introduction embed for the quiz."""
- start_embed = discord.Embed(colour=discord.Colour.red())
- start_embed.title = "Quiz game Starting!!"
- start_embed.description = "Each game consists of 5 questions.\n"
- start_embed.description += "**Rules :**\nNo cheating and have fun!"
- start_embed.description += f"\n **Category** : {category}"
+ start_embed = discord.Embed(
+ colour=Colours.blue,
+ title="Quiz game starting!",
+ description=(
+ f"This game consists of {self.question_limit + 1} questions.\n"
+ "**Rules: **No cheating and have fun!\n"
+ f"**Category**: {category}"
+ ),
+ )
+
start_embed.set_footer(
- text="Points for each question reduces by 25 after 10s or after a hint. Total time is 30s per question"
+ text=(
+ "Points for each question reduces by 25 after 10s or after a hint. "
+ "Total time is 30s per question"
+ )
)
+
return start_embed
+ @staticmethod
+ def make_error_embed(desc: str) -> discord.Embed:
+ """Generate an error embed with the given description."""
+ error_embed = discord.Embed(
+ colour=Colours.soft_red,
+ title=random.choice(NEGATIVE_REPLIES),
+ description=desc,
+ )
+
+ return error_embed
+
@quiz_game.command(name="stop")
async def stop_quiz(self, ctx: commands.Context) -> None:
"""
@@ -202,21 +465,24 @@ class TriviaQuiz(commands.Cog):
Note: Only mods or the owner of the quiz can stop it.
"""
- if self.game_status[ctx.channel.id] is True:
- # Check if the author is the game starter or a moderator.
- if (
- ctx.author == self.game_owners[ctx.channel.id]
- or any(Roles.moderator == role.id for role in ctx.author.roles)
- ):
- await ctx.send("Quiz stopped.")
- await self.declare_winner(ctx.channel, self.game_player_scores[ctx.channel.id])
+ try:
+ if self.game_status[ctx.channel.id]:
+ # Check if the author is the game starter or a moderator.
+ if ctx.author == self.game_owners[ctx.channel.id] or any(
+ Roles.moderator == role.id for role in ctx.author.roles
+ ):
+ self.game_status[ctx.channel.id] = False
+ del self.game_owners[ctx.channel.id]
+ self.game_player_scores[ctx.channel.id] = {}
+
+ await ctx.send("Quiz stopped.")
+ await self.declare_winner(ctx.channel, self.game_player_scores[ctx.channel.id])
- self.game_status[ctx.channel.id] = False
- del self.game_owners[ctx.channel.id]
- self.game_player_scores[ctx.channel.id] = {}
+ else:
+ await ctx.send(f"{ctx.author.mention}, you are not authorised to stop this game :ghost:!")
else:
- await ctx.send(f"{ctx.author.mention}, you are not authorised to stop this game :ghost:!")
- else:
+ await ctx.send("No quiz running.")
+ except KeyError:
await ctx.send("No quiz running.")
@quiz_game.command(name="leaderboard")
@@ -226,18 +492,20 @@ class TriviaQuiz(commands.Cog):
@staticmethod
async def send_score(channel: discord.TextChannel, player_data: dict) -> None:
- """A function which sends the score."""
+ """Send the current scores of players in the game channel."""
if len(player_data) == 0:
await channel.send("No one has made it onto the leaderboard yet.")
return
- embed = discord.Embed(colour=discord.Colour.blue())
- embed.title = "Score Board"
- embed.description = ""
+ embed = discord.Embed(
+ colour=Colours.blue,
+ title="Score Board",
+ description="",
+ )
- sorted_dict = sorted(player_data.items(), key=lambda a: a[1], reverse=True)
+ sorted_dict = sorted(player_data.items(), key=operator.itemgetter(1), reverse=True)
for item in sorted_dict:
- embed.description += f"{item[0]} : {item[1]}\n"
+ embed.description += f"{item[0]}: {item[1]}\n"
await channel.send(embed=embed)
@@ -250,7 +518,6 @@ class TriviaQuiz(commands.Cog):
# Check if more than 1 player has highest points.
if no_of_winners > 1:
- word = "You guys"
winners = []
points_copy = list(player_data.values()).copy()
@@ -261,44 +528,65 @@ class TriviaQuiz(commands.Cog):
winners_mention = " ".join(winner.mention for winner in winners)
else:
- word = "You"
author_index = list(player_data.values()).index(highest_points)
winner = list(player_data.keys())[author_index]
winners_mention = winner.mention
await channel.send(
f"Congratulations {winners_mention} :tada: "
- f"{word} have won this quiz game with a grand total of {highest_points} points!"
+ f"You have won this quiz game with a grand total of {highest_points} points!"
)
def category_embed(self) -> discord.Embed:
"""Build an embed showing all available trivia categories."""
- embed = discord.Embed(colour=discord.Colour.blue())
- embed.title = "The available question categories are:"
+ embed = discord.Embed(
+ colour=Colours.blue,
+ title="The available question categories are:",
+ description="",
+ )
+
embed.set_footer(text="If a category is not chosen, a random one will be selected.")
- embed.description = ""
for cat, description in self.categories.items():
- embed.description += f"**- {cat.capitalize()}**\n{description.capitalize()}\n"
+ embed.description += (
+ f"**- {cat.capitalize()}**\n"
+ f"{description.capitalize()}\n"
+ )
return embed
@staticmethod
- async def send_answer(channel: discord.TextChannel, question_dict: dict) -> None:
+ async def send_answer(
+ channel: discord.TextChannel,
+ answers: List[str],
+ answer_is_correct: bool,
+ question_dict: dict,
+ q_left: int,
+ ) -> None:
"""Send the correct answer of a question to the game channel."""
- answer = question_dict["answer"]
- info = question_dict["info"]
- embed = discord.Embed(color=discord.Colour.red())
- embed.title = f"The correct answer is **{answer}**\n"
- embed.description = ""
+ info = question_dict.get("info")
+
+ plurality = " is" if len(answers) == 1 else "s are"
- if info != "":
+ embed = discord.Embed(
+ color=Colours.bright_green,
+ title=(
+ ("You got it! " if answer_is_correct else "")
+ + f"The correct answer{plurality} **`{', '.join(answers)}`**\n"
+ ),
+ description="",
+ )
+
+ if info is not None:
embed.description += f"**Information**\n{info}\n\n"
- embed.description += "Let's move to the next question.\nRemaining questions: "
+ embed.description += (
+ ("Let's move to the next question." if q_left > 0 else "")
+ + f"\nRemaining questions: {q_left}"
+ )
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/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))
diff --git a/bot/exts/halloween/8ball.py b/bot/exts/halloween/8ball.py
index 1df48fbf..a2431190 100644
--- a/bot/exts/halloween/8ball.py
+++ b/bot/exts/halloween/8ball.py
@@ -6,28 +6,26 @@ from pathlib import Path
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-with open(Path("bot/resources/halloween/responses.json"), "r", encoding="utf8") as f:
- responses = json.load(f)
+RESPONSES = json.loads(Path("bot/resources/halloween/responses.json").read_text("utf8"))
class SpookyEightBall(commands.Cog):
"""Spooky Eightball answers."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=('spooky8ball',))
+ @commands.command(aliases=("spooky8ball",))
async def spookyeightball(self, ctx: commands.Context, *, question: str) -> None:
"""Responds with a random response to a question."""
- choice = random.choice(responses['responses'])
+ choice = random.choice(RESPONSES["responses"])
msg = await ctx.send(choice[0])
if len(choice) > 1:
await asyncio.sleep(random.randint(2, 5))
await msg.edit(content=f"{choice[0]} \n{choice[1]}")
-def setup(bot: commands.Bot) -> None:
- """Spooky Eight Ball Cog Load."""
- bot.add_cog(SpookyEightBall(bot))
+def setup(bot: Bot) -> None:
+ """Load the Spooky Eight Ball Cog."""
+ bot.add_cog(SpookyEightBall())
diff --git a/bot/exts/halloween/candy_collection.py b/bot/exts/halloween/candy_collection.py
index 40e21f40..4afd5913 100644
--- a/bot/exts/halloween/candy_collection.py
+++ b/bot/exts/halloween/candy_collection.py
@@ -6,6 +6,7 @@ import discord
from async_rediscache import RedisCache
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Channels, Month
from bot.utils.decorators import in_month
@@ -21,11 +22,11 @@ EMOJIS = dict(
CANDY="\N{CANDY}",
SKULL="\N{SKULL}",
MEDALS=(
- '\N{FIRST PLACE MEDAL}',
- '\N{SECOND PLACE MEDAL}',
- '\N{THIRD PLACE MEDAL}',
- '\N{SPORTS MEDAL}',
- '\N{SPORTS MEDAL}',
+ "\N{FIRST PLACE MEDAL}",
+ "\N{SECOND PLACE MEDAL}",
+ "\N{THIRD PLACE MEDAL}",
+ "\N{SPORTS MEDAL}",
+ "\N{SPORTS MEDAL}",
),
)
@@ -40,7 +41,7 @@ class CandyCollection(commands.Cog):
candy_messages = RedisCache()
skull_messages = RedisCache()
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@in_month(Month.OCTOBER)
@@ -60,15 +61,15 @@ class CandyCollection(commands.Cog):
# do random check for skull first as it has the lower chance
if random.randint(1, ADD_SKULL_REACTION_CHANCE) == 1:
await self.skull_messages.set(message.id, "skull")
- return await message.add_reaction(EMOJIS['SKULL'])
+ await message.add_reaction(EMOJIS["SKULL"])
# check for the candy chance next
- if random.randint(1, ADD_CANDY_REACTION_CHANCE) == 1:
+ elif random.randint(1, ADD_CANDY_REACTION_CHANCE) == 1:
await self.candy_messages.set(message.id, "candy")
- return await message.add_reaction(EMOJIS['CANDY'])
+ await message.add_reaction(EMOJIS["CANDY"])
@in_month(Month.OCTOBER)
@commands.Cog.listener()
- async def on_reaction_add(self, reaction: discord.Reaction, user: discord.Member) -> None:
+ async def on_reaction_add(self, reaction: discord.Reaction, user: Union[discord.User, discord.Member]) -> None:
"""Add/remove candies from a person if the reaction satisfies criteria."""
message = reaction.message
# check to ensure the reactor is human
@@ -81,7 +82,7 @@ class CandyCollection(commands.Cog):
# if its not a candy or skull, and it is one of 10 most recent messages,
# proceed to add a skull/candy with higher chance
- if str(reaction.emoji) not in (EMOJIS['SKULL'], EMOJIS['CANDY']):
+ if str(reaction.emoji) not in (EMOJIS["SKULL"], EMOJIS["CANDY"]):
recent_message_ids = map(
lambda m: m.id,
await self.hacktober_channel.history(limit=10).flatten()
@@ -90,14 +91,14 @@ class CandyCollection(commands.Cog):
await self.reacted_msg_chance(message)
return
- if await self.candy_messages.get(message.id) == "candy" and str(reaction.emoji) == EMOJIS['CANDY']:
+ if await self.candy_messages.get(message.id) == "candy" and str(reaction.emoji) == EMOJIS["CANDY"]:
await self.candy_messages.delete(message.id)
if await self.candy_records.contains(user.id):
await self.candy_records.increment(user.id)
else:
await self.candy_records.set(user.id, 1)
- elif await self.skull_messages.get(message.id) == "skull" and str(reaction.emoji) == EMOJIS['SKULL']:
+ elif await self.skull_messages.get(message.id) == "skull" and str(reaction.emoji) == EMOJIS["SKULL"]:
await self.skull_messages.delete(message.id)
if prev_record := await self.candy_records.get(user.id):
@@ -105,7 +106,7 @@ class CandyCollection(commands.Cog):
await self.candy_records.decrement(user.id, lost)
if lost == prev_record:
- await CandyCollection.send_spook_msg(user, message.channel, 'all of your')
+ await CandyCollection.send_spook_msg(user, message.channel, "all of your")
else:
await CandyCollection.send_spook_msg(user, message.channel, lost)
else:
@@ -124,11 +125,11 @@ class CandyCollection(commands.Cog):
"""
if random.randint(1, ADD_SKULL_EXISTING_REACTION_CHANCE) == 1:
await self.skull_messages.set(message.id, "skull")
- return await message.add_reaction(EMOJIS['SKULL'])
+ await message.add_reaction(EMOJIS["SKULL"])
- if random.randint(1, ADD_CANDY_EXISTING_REACTION_CHANCE) == 1:
+ elif random.randint(1, ADD_CANDY_EXISTING_REACTION_CHANCE) == 1:
await self.candy_messages.set(message.id, "candy")
- return await message.add_reaction(EMOJIS['CANDY'])
+ await message.add_reaction(EMOJIS["CANDY"])
@property
def hacktober_channel(self) -> discord.TextChannel:
@@ -141,8 +142,10 @@ class CandyCollection(commands.Cog):
) -> None:
"""Send a spooky message."""
e = discord.Embed(colour=author.colour)
- e.set_author(name="Ghosts and Ghouls and Jack o' lanterns at night; "
- f"I took {candies} candies and quickly took flight.")
+ e.set_author(
+ name="Ghosts and Ghouls and Jack o' lanterns at night; "
+ f"I took {candies} candies and quickly took flight."
+ )
await channel.send(embed=e)
@staticmethod
@@ -152,8 +155,12 @@ class CandyCollection(commands.Cog):
) -> None:
"""An alternative spooky message sent when user has no candies in the collection."""
embed = discord.Embed(color=author.color)
- embed.set_author(name="Ghosts and Ghouls and Jack o' lanterns at night; "
- "I tried to take your candies but you had none to begin with!")
+ embed.set_author(
+ name=(
+ "Ghosts and Ghouls and Jack o' lanterns at night; "
+ "I tried to take your candies but you had none to begin with!"
+ )
+ )
await channel.send(embed=embed)
@in_month(Month.OCTOBER)
@@ -170,10 +177,10 @@ class CandyCollection(commands.Cog):
)
top_five = top_sorted[:5]
- return '\n'.join(
+ return "\n".join(
f"{EMOJIS['MEDALS'][index]} <@{record[0]}>: {record[1]}"
for index, record in enumerate(top_five)
- ) if top_five else 'No Candies'
+ ) if top_five else "No Candies"
e = discord.Embed(colour=discord.Colour.blurple())
e.add_field(
@@ -182,7 +189,7 @@ class CandyCollection(commands.Cog):
inline=False
)
e.add_field(
- name='\u200b',
+ name="\u200b",
value="Candies will randomly appear on messages sent. "
"\nHit the candy when it appears as fast as possible to get the candy! "
"\nBut beware the ghosts...",
@@ -191,6 +198,6 @@ class CandyCollection(commands.Cog):
await ctx.send(embed=e)
-def setup(bot: commands.Bot) -> None:
- """Candy Collection game Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Candy Collection Cog."""
bot.add_cog(CandyCollection(bot))
diff --git a/bot/exts/halloween/hacktober-issue-finder.py b/bot/exts/halloween/hacktober-issue-finder.py
index 9deadde9..20a06770 100644
--- a/bot/exts/halloween/hacktober-issue-finder.py
+++ b/bot/exts/halloween/hacktober-issue-finder.py
@@ -3,10 +3,10 @@ import logging
import random
from typing import Dict, Optional
-import aiohttp
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Month, Tokens
from bot.utils.decorators import in_month
@@ -25,7 +25,7 @@ if GITHUB_TOKEN := Tokens.github:
class HacktoberIssues(commands.Cog):
"""Find a random hacktober python issue on GitHub."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
self.cache_normal = None
self.cache_timer_normal = datetime.datetime(1, 1, 1)
@@ -41,7 +41,7 @@ class HacktoberIssues(commands.Cog):
If the command is run with beginner (`.hacktoberissues beginner`):
It will also narrow it down to the "first good issue" label.
"""
- with ctx.typing():
+ async with ctx.typing():
issues = await self.get_issues(ctx, option)
if issues is None:
return
@@ -59,40 +59,41 @@ class HacktoberIssues(commands.Cog):
log.debug("using cache")
return self.cache_normal
- async with aiohttp.ClientSession() as session:
+ if option == "beginner":
+ url = URL + '+label:"good first issue"'
+ if self.cache_beginner is not None:
+ page = random.randint(1, min(1000, self.cache_beginner["total_count"]) // 100)
+ url += f"&page={page}"
+ else:
+ url = URL
+ if self.cache_normal is not None:
+ page = random.randint(1, min(1000, self.cache_normal["total_count"]) // 100)
+ url += f"&page={page}"
+
+ log.debug(f"making api request to url: {url}")
+ async with self.bot.http_session.get(url, headers=REQUEST_HEADERS) as response:
+ if response.status != 200:
+ log.error(f"expected 200 status (got {response.status}) by the GitHub api.")
+ await ctx.send(
+ f"ERROR: expected 200 status (got {response.status}) by the GitHub api.\n"
+ f"{await response.text()}"
+ )
+ return None
+ data = await response.json()
+
+ if len(data["items"]) == 0:
+ log.error(f"no issues returned by GitHub API, with url: {response.url}")
+ await ctx.send(f"ERROR: no issues returned by GitHub API, with url: {response.url}")
+ return None
+
if option == "beginner":
- url = URL + '+label:"good first issue"'
- if self.cache_beginner is not None:
- page = random.randint(1, min(1000, self.cache_beginner["total_count"]) // 100)
- url += f"&page={page}"
+ self.cache_beginner = data
+ self.cache_timer_beginner = ctx.message.created_at
else:
- url = URL
- if self.cache_normal is not None:
- page = random.randint(1, min(1000, self.cache_normal["total_count"]) // 100)
- url += f"&page={page}"
-
- log.debug(f"making api request to url: {url}")
- async with session.get(url, headers=REQUEST_HEADERS) as response:
- if response.status != 200:
- log.error(f"expected 200 status (got {response.status}) from the GitHub api.")
- await ctx.send(f"ERROR: expected 200 status (got {response.status}) from the GitHub api.")
- await ctx.send(await response.text())
- return None
- data = await response.json()
-
- if len(data["items"]) == 0:
- log.error(f"no issues returned from GitHub api. with url: {response.url}")
- await ctx.send(f"ERROR: no issues returned from GitHub api. with url: {response.url}")
- return None
-
- if option == "beginner":
- self.cache_beginner = data
- self.cache_timer_beginner = ctx.message.created_at
- else:
- self.cache_normal = data
- self.cache_timer_normal = ctx.message.created_at
-
- return data
+ self.cache_normal = data
+ self.cache_timer_normal = ctx.message.created_at
+
+ return data
@staticmethod
def format_embed(issue: Dict) -> discord.Embed:
@@ -103,7 +104,7 @@ class HacktoberIssues(commands.Cog):
labels = [label["name"] for label in issue["labels"]]
embed = discord.Embed(title=title)
- embed.description = body[:500] + '...' if len(body) > 500 else body
+ embed.description = body[:500] + "..." if len(body) > 500 else body
embed.add_field(name="labels", value="\n".join(labels))
embed.url = issue_url
embed.set_footer(text=issue_url)
@@ -111,6 +112,6 @@ class HacktoberIssues(commands.Cog):
return embed
-def setup(bot: commands.Bot) -> None:
- """Hacktober issue finder Cog Load."""
+def setup(bot: Bot) -> None:
+ """Load the HacktoberIssue finder."""
bot.add_cog(HacktoberIssues(bot))
diff --git a/bot/exts/halloween/hacktoberstats.py b/bot/exts/halloween/hacktoberstats.py
index d9fc0e8a..b74e680b 100644
--- a/bot/exts/halloween/hacktoberstats.py
+++ b/bot/exts/halloween/hacktoberstats.py
@@ -5,12 +5,12 @@ from collections import Counter
from datetime import datetime, timedelta
from typing import List, Optional, Tuple, Union
-import aiohttp
import discord
from async_rediscache import RedisCache
from discord.ext import commands
-from bot.constants import Channels, Month, NEGATIVE_REPLIES, Tokens, WHITELISTED_CHANNELS
+from bot.bot import Bot
+from bot.constants import Channels, Colours, Month, NEGATIVE_REPLIES, Tokens, WHITELISTED_CHANNELS
from bot.utils.decorators import in_month, whitelist_override
log = logging.getLogger(__name__)
@@ -39,7 +39,7 @@ class HacktoberStats(commands.Cog):
# Stores mapping of user IDs and GitHub usernames
linked_accounts = RedisCache()
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)
@@ -83,15 +83,15 @@ class HacktoberStats(commands.Cog):
if github_username:
if await self.linked_accounts.contains(author_id):
old_username = await self.linked_accounts.get(author_id)
- logging.info(f"{author_id} has changed their github link from '{old_username}' to '{github_username}'")
+ log.info(f"{author_id} has changed their github link from '{old_username}' to '{github_username}'")
await ctx.send(f"{author_mention}, your GitHub username has been updated to: '{github_username}'")
else:
- logging.info(f"{author_id} has added a github link to '{github_username}'")
+ log.info(f"{author_id} has added a github link to '{github_username}'")
await ctx.send(f"{author_mention}, your GitHub username has been added")
await self.linked_accounts.set(author_id, github_username)
else:
- logging.info(f"{author_id} tried to link a GitHub account but didn't provide a username")
+ log.info(f"{author_id} tried to link a GitHub account but didn't provide a username")
await ctx.send(f"{author_mention}, a GitHub username is required to link your account")
@in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER)
@@ -138,7 +138,7 @@ class HacktoberStats(commands.Cog):
if prs:
stats_embed = await self.build_embed(github_username, prs)
- await ctx.send('Here are some stats!', embed=stats_embed)
+ await ctx.send("Here are some stats!", embed=stats_embed)
else:
await ctx.send(f"No valid Hacktoberfest PRs found for '{github_username}'")
@@ -157,7 +157,7 @@ class HacktoberStats(commands.Cog):
stats_embed = discord.Embed(
title=f"{github_username}'s Hacktoberfest",
- color=discord.Color(0x9c4af7),
+ color=Colours.purple,
description=(
f"{github_username} has made {n} valid "
f"{self._contributionator(n)} in "
@@ -188,8 +188,7 @@ class HacktoberStats(commands.Cog):
logging.info(f"Hacktoberfest PR built for GitHub user '{github_username}'")
return stats_embed
- @staticmethod
- async def get_october_prs(github_username: str) -> Optional[List[dict]]:
+ async def get_october_prs(self, github_username: str) -> Optional[List[dict]]:
"""
Query GitHub's API for PRs created during the month of October by github_username.
@@ -212,7 +211,7 @@ class HacktoberStats(commands.Cog):
Otherwise, return empty list.
None will be returned when the GitHub user was not found.
"""
- logging.info(f"Fetching Hacktoberfest Stats for GitHub user: '{github_username}'")
+ log.info(f"Fetching Hacktoberfest Stats for GitHub user: '{github_username}'")
base_url = "https://api.github.com/search/issues?q="
action_type = "pr"
is_query = "public"
@@ -228,24 +227,24 @@ class HacktoberStats(commands.Cog):
f"+created:{date_range}"
f"&per_page={per_page}"
)
- logging.debug(f"GitHub query URL generated: {query_url}")
+ log.debug(f"GitHub query URL generated: {query_url}")
- jsonresp = await HacktoberStats._fetch_url(query_url, REQUEST_HEADERS)
- if "message" in jsonresp.keys():
+ jsonresp = await self._fetch_url(query_url, REQUEST_HEADERS)
+ if "message" in jsonresp:
# One of the parameters is invalid, short circuit for now
api_message = jsonresp["errors"][0]["message"]
# Ignore logging non-existent users or users we do not have permission to see
if api_message == GITHUB_NONEXISTENT_USER_MESSAGE:
- logging.debug(f"No GitHub user found named '{github_username}'")
+ log.debug(f"No GitHub user found named '{github_username}'")
return
else:
- logging.error(f"GitHub API request for '{github_username}' failed with message: {api_message}")
+ log.error(f"GitHub API request for '{github_username}' failed with message: {api_message}")
return [] # No October PRs were found due to error
if jsonresp["total_count"] == 0:
# Short circuit if there aren't any PRs
- logging.info(f"No October PRs found for GitHub user: '{github_username}'")
+ log.info(f"No October PRs found for GitHub user: '{github_username}'")
return []
logging.info(f"Found {len(jsonresp['items'])} Hacktoberfest PRs for GitHub user: '{github_username}'")
@@ -253,20 +252,20 @@ class HacktoberStats(commands.Cog):
oct3 = datetime(int(CURRENT_YEAR), 10, 3, 23, 59, 59, tzinfo=None)
hackto_topics = {} # cache whether each repo has the appropriate topic (bool values)
for item in jsonresp["items"]:
- shortname = HacktoberStats._get_shortname(item["repository_url"])
+ shortname = self._get_shortname(item["repository_url"])
itemdict = {
"repo_url": f"https://www.github.com/{shortname}",
"repo_shortname": shortname,
"created_at": datetime.strptime(
- item["created_at"], r"%Y-%m-%dT%H:%M:%SZ"
+ item["created_at"], "%Y-%m-%dT%H:%M:%SZ"
),
"number": item["number"]
}
# If the PR has 'invalid' or 'spam' labels, the PR must be
# either merged or approved for it to be included
- if HacktoberStats._has_label(item, ["invalid", "spam"]):
- if not await HacktoberStats._is_accepted(itemdict):
+ if self._has_label(item, ["invalid", "spam"]):
+ if not await self._is_accepted(itemdict):
continue
# PRs before oct 3 no need to check for topics
@@ -277,21 +276,20 @@ class HacktoberStats(commands.Cog):
continue
# Checking PR's labels for "hacktoberfest-accepted"
- if HacktoberStats._has_label(item, "hacktoberfest-accepted"):
+ if self._has_label(item, "hacktoberfest-accepted"):
outlist.append(itemdict)
continue
# No need to query GitHub if repo topics are fetched before already
- if shortname in hackto_topics.keys():
- if hackto_topics[shortname]:
- outlist.append(itemdict)
- continue
+ if hackto_topics.get(shortname):
+ outlist.append(itemdict)
+ continue
# Fetch topics for the PR's repo
topics_query_url = f"https://api.github.com/repos/{shortname}/topics"
- logging.debug(f"Fetching repo topics for {shortname} with url: {topics_query_url}")
- jsonresp2 = await HacktoberStats._fetch_url(topics_query_url, GITHUB_TOPICS_ACCEPT_HEADER)
+ log.debug(f"Fetching repo topics for {shortname} with url: {topics_query_url}")
+ jsonresp2 = await self._fetch_url(topics_query_url, GITHUB_TOPICS_ACCEPT_HEADER)
if jsonresp2.get("names") is None:
- logging.error(f"Error fetching topics for {shortname}: {jsonresp2['message']}")
+ log.error(f"Error fetching topics for {shortname}: {jsonresp2['message']}")
continue # Assume the repo doesn't have the `hacktoberfest` topic if API request errored
# PRs after oct 3 that doesn't have 'hacktoberfest-accepted' label
@@ -301,13 +299,10 @@ class HacktoberStats(commands.Cog):
outlist.append(itemdict)
return outlist
- @staticmethod
- async def _fetch_url(url: str, headers: dict) -> dict:
+ async def _fetch_url(self, url: str, headers: dict) -> dict:
"""Retrieve API response from URL."""
- async with aiohttp.ClientSession() as session:
- async with session.get(url, headers=headers) as resp:
- jsonresp = await resp.json()
- return jsonresp
+ async with self.bot.http_session.get(url, headers=headers) as resp:
+ return await resp.json()
@staticmethod
def _has_label(pr: dict, labels: Union[List[str], str]) -> bool:
@@ -319,40 +314,36 @@ class HacktoberStats(commands.Cog):
"""
if not pr.get("labels"): # if PR has no labels
return False
- if (isinstance(labels, str)) and (any(label["name"].casefold() == labels for label in pr["labels"])):
+ if isinstance(labels, str) and any(label["name"].casefold() == labels for label in pr["labels"]):
return True
for item in labels:
if any(label["name"].casefold() == item for label in pr["labels"]):
return True
return False
- @staticmethod
- async def _is_accepted(pr: dict) -> bool:
+ async def _is_accepted(self, pr: dict) -> bool:
"""Check if a PR is merged, approved, or labelled hacktoberfest-accepted."""
# checking for merge status
- query_url = f"https://api.github.com/repos/{pr['repo_shortname']}/pulls/"
- query_url += str(pr["number"])
- jsonresp = await HacktoberStats._fetch_url(query_url, REQUEST_HEADERS)
-
- if "message" in jsonresp.keys():
- logging.error(
- f"Error fetching PR stats for #{pr['number']} in repo {pr['repo_shortname']}:\n"
- f"{jsonresp['message']}"
- )
+ query_url = f"https://api.github.com/repos/{pr['repo_shortname']}/pulls/{pr['number']}"
+ jsonresp = await self._fetch_url(query_url, REQUEST_HEADERS)
+
+ if message := jsonresp.get("message"):
+ log.error(f"Error fetching PR stats for #{pr['number']} in repo {pr['repo_shortname']}:\n{message}")
return False
- if ("merged" in jsonresp.keys()) and jsonresp["merged"]:
+
+ if jsonresp.get("merged"):
return True
# checking for the label, using `jsonresp` which has the label information
- if HacktoberStats._has_label(jsonresp, "hacktoberfest-accepted"):
+ if self._has_label(jsonresp, "hacktoberfest-accepted"):
return True
# checking approval
query_url += "/reviews"
- jsonresp2 = await HacktoberStats._fetch_url(query_url, REQUEST_HEADERS)
+ jsonresp2 = await self._fetch_url(query_url, REQUEST_HEADERS)
if isinstance(jsonresp2, dict):
# if API request is unsuccessful it will be a dict with the error in 'message'
- logging.error(
+ log.error(
f"Error fetching PR reviews for #{pr['number']} in repo {pr['repo_shortname']}:\n"
f"{jsonresp2['message']}"
)
@@ -363,9 +354,8 @@ class HacktoberStats(commands.Cog):
# loop through reviews and check for approval
for item in jsonresp2:
- if "status" in item.keys():
- if item['status'] == "APPROVED":
- return True
+ if item.get("status") == "APPROVED":
+ return True
return False
@staticmethod
@@ -381,8 +371,7 @@ class HacktoberStats(commands.Cog):
exp = r"https?:\/\/api.github.com\/repos\/([/\-\_\.\w]+)"
return re.findall(exp, in_url)[0]
- @staticmethod
- async def _categorize_prs(prs: List[dict]) -> tuple:
+ async def _categorize_prs(self, prs: List[dict]) -> tuple:
"""
Categorize PRs into 'in_review' and 'accepted' and returns as a tuple.
@@ -397,9 +386,9 @@ class HacktoberStats(commands.Cog):
in_review = []
accepted = []
for pr in prs:
- if (pr['created_at'] + timedelta(REVIEW_DAYS)) > now:
+ if (pr["created_at"] + timedelta(REVIEW_DAYS)) > now:
in_review.append(pr)
- elif (pr['created_at'] <= oct3) or await HacktoberStats._is_accepted(pr):
+ elif (pr["created_at"] <= oct3) or await self._is_accepted(pr):
accepted.append(pr)
return in_review, accepted
@@ -438,14 +427,14 @@ class HacktoberStats(commands.Cog):
return "contributions"
@staticmethod
- def _author_mention_from_context(ctx: commands.Context) -> Tuple:
+ def _author_mention_from_context(ctx: commands.Context) -> Tuple[str, str]:
"""Return stringified Message author ID and mentionable string from commands.Context."""
- author_id = str(ctx.message.author.id)
- author_mention = ctx.message.author.mention
+ author_id = str(ctx.author.id)
+ author_mention = ctx.author.mention
return author_id, author_mention
-def setup(bot: commands.Bot) -> None:
- """Hacktoberstats Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Hacktober Stats Cog."""
bot.add_cog(HacktoberStats(bot))
diff --git a/bot/exts/halloween/halloween_facts.py b/bot/exts/halloween/halloween_facts.py
index 7eb6d56f..5ad8cc57 100644
--- a/bot/exts/halloween/halloween_facts.py
+++ b/bot/exts/halloween/halloween_facts.py
@@ -8,6 +8,8 @@ from typing import Tuple
import discord
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
SPOOKY_EMOJIS = [
@@ -20,23 +22,19 @@ SPOOKY_EMOJIS = [
"\N{SKULL AND CROSSBONES}",
"\N{SPIDER WEB}",
]
-PUMPKIN_ORANGE = discord.Color(0xFF7518)
+PUMPKIN_ORANGE = 0xFF7518
INTERVAL = timedelta(hours=6).total_seconds()
+FACTS = json.loads(Path("bot/resources/halloween/halloween_facts.json").read_text("utf8"))
+FACTS = list(enumerate(FACTS))
+
class HalloweenFacts(commands.Cog):
"""A Cog for displaying interesting facts about Halloween."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- with open(Path("bot/resources/halloween/halloween_facts.json"), "r", encoding="utf8") as file:
- self.halloween_facts = json.load(file)
- self.facts = list(enumerate(self.halloween_facts))
- random.shuffle(self.facts)
-
def random_fact(self) -> Tuple[int, str]:
"""Return a random fact from the loaded facts."""
- return random.choice(self.facts)
+ return random.choice(FACTS)
@commands.command(name="spookyfact", aliases=("halloweenfact",), brief="Get the most recent Halloween fact")
async def get_random_fact(self, ctx: commands.Context) -> None:
@@ -53,6 +51,6 @@ class HalloweenFacts(commands.Cog):
return discord.Embed(title=title, description=fact, color=PUMPKIN_ORANGE)
-def setup(bot: commands.Bot) -> None:
- """Halloween facts Cog load."""
- bot.add_cog(HalloweenFacts(bot))
+def setup(bot: Bot) -> None:
+ """Load the Halloween Facts Cog."""
+ bot.add_cog(HalloweenFacts())
diff --git a/bot/exts/halloween/halloweenify.py b/bot/exts/halloween/halloweenify.py
index 596c6682..83cfbaa7 100644
--- a/bot/exts/halloween/halloweenify.py
+++ b/bot/exts/halloween/halloweenify.py
@@ -1,42 +1,40 @@
import logging
-from json import load
+from json import loads
from pathlib import Path
from random import choice
import discord
from discord.errors import Forbidden
from discord.ext import commands
-from discord.ext.commands.cooldowns import BucketType
+from discord.ext.commands import BucketType
+
+from bot.bot import Bot
log = logging.getLogger(__name__)
+HALLOWEENIFY_DATA = loads(Path("bot/resources/halloween/halloweenify.json").read_text("utf8"))
+
class Halloweenify(commands.Cog):
"""A cog to change a invokers nickname to a spooky one!"""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@commands.cooldown(1, 300, BucketType.user)
@commands.command()
async def halloweenify(self, ctx: commands.Context) -> None:
"""Change your nickname into a much spookier one!"""
async with ctx.typing():
- with open(Path("bot/resources/halloween/halloweenify.json"), "r", encoding="utf8") as f:
- data = load(f)
-
# Choose a random character from our list we loaded above and set apart the nickname and image url.
- character = choice(data["characters"])
- nickname = ''.join([nickname for nickname in character])
- image = ''.join([character[nickname] for nickname in character])
+ character = choice(HALLOWEENIFY_DATA["characters"])
+ nickname = "".join(nickname for nickname in character)
+ image = "".join(character[nickname] for nickname in character)
# Build up a Embed
embed = discord.Embed()
embed.colour = discord.Colour.dark_orange()
embed.title = "Not spooky enough?"
embed.description = (
- f"**{ctx.author.display_name}** wasn\'t spooky enough for you? That\'s understandable, "
- f"{ctx.author.display_name} isn\'t scary at all! "
+ f"**{ctx.author.display_name}** wasn't spooky enough for you? That's understandable, "
+ f"{ctx.author.display_name} isn't scary at all! "
"Let me think of something better. Hmm... I got it!\n\n "
)
embed.set_image(url=image)
@@ -61,6 +59,6 @@ class Halloweenify(commands.Cog):
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Halloweenify Cog load."""
- bot.add_cog(Halloweenify(bot))
+def setup(bot: Bot) -> None:
+ """Load the Halloweenify Cog."""
+ bot.add_cog(Halloweenify())
diff --git a/bot/exts/halloween/monsterbio.py b/bot/exts/halloween/monsterbio.py
index 016a66d1..69e898cb 100644
--- a/bot/exts/halloween/monsterbio.py
+++ b/bot/exts/halloween/monsterbio.py
@@ -6,20 +6,19 @@ from pathlib import Path
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-with open(Path("bot/resources/halloween/monster.json"), "r", encoding="utf8") as f:
- TEXT_OPTIONS = json.load(f) # Data for a mad-lib style generation of text
+TEXT_OPTIONS = json.loads(
+ Path("bot/resources/halloween/monster.json").read_text("utf8")
+) # Data for a mad-lib style generation of text
class MonsterBio(commands.Cog):
"""A cog that generates a spooky monster biography."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
def generate_name(self, seeded_random: random.Random) -> str:
"""Generates a name (for either monster species or monster name)."""
n_candidate_strings = seeded_random.randint(2, len(TEXT_OPTIONS["monster_type"]))
@@ -28,7 +27,7 @@ class MonsterBio(commands.Cog):
@commands.command(brief="Sends your monster bio!")
async def monsterbio(self, ctx: commands.Context) -> None:
"""Sends a description of a monster."""
- seeded_random = random.Random(ctx.message.author.id) # Seed a local Random instance rather than the system one
+ seeded_random = random.Random(ctx.author.id) # Seed a local Random instance rather than the system one
name = self.generate_name(seeded_random)
species = self.generate_name(seeded_random)
@@ -39,7 +38,7 @@ class MonsterBio(commands.Cog):
continue
options = seeded_random.sample(TEXT_OPTIONS[key], value)
- words[key] = ' '.join(options)
+ words[key] = " ".join(options)
embed = discord.Embed(
title=f"{name}'s Biography",
@@ -50,6 +49,6 @@ class MonsterBio(commands.Cog):
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Monster bio Cog load."""
- bot.add_cog(MonsterBio(bot))
+def setup(bot: Bot) -> None:
+ """Load the Monster Bio Cog."""
+ bot.add_cog(MonsterBio())
diff --git a/bot/exts/halloween/monstersurvey.py b/bot/exts/halloween/monstersurvey.py
index 80196825..96cda11e 100644
--- a/bot/exts/halloween/monstersurvey.py
+++ b/bot/exts/halloween/monstersurvey.py
@@ -1,6 +1,6 @@
import json
import logging
-import os
+import pathlib
from discord import Embed
from discord.ext import commands
@@ -9,8 +9,8 @@ from discord.ext.commands import Bot, Cog, Context
log = logging.getLogger(__name__)
EMOJIS = {
- 'SUCCESS': u'\u2705',
- 'ERROR': u'\u274C'
+ "SUCCESS": u"\u2705",
+ "ERROR": u"\u274C"
}
@@ -23,18 +23,15 @@ class MonsterSurvey(Cog):
Users may change their vote, but only their current vote will be counted.
"""
- def __init__(self, bot: Bot):
+ def __init__(self):
"""Initializes values for the bot to use within the voting commands."""
- self.bot = bot
- self.registry_location = os.path.join(os.getcwd(), 'bot', 'resources', 'halloween', 'monstersurvey.json')
- with open(self.registry_location, 'r', encoding="utf8") as jason:
- self.voter_registry = json.load(jason)
+ self.registry_path = pathlib.Path("bot", "resources", "halloween", "monstersurvey.json")
+ self.voter_registry = json.loads(self.registry_path.read_text("utf8"))
def json_write(self) -> None:
"""Write voting results to a local JSON file."""
log.info("Saved Monster Survey Results")
- with open(self.registry_location, 'w', encoding="utf8") as jason:
- json.dump(self.voter_registry, jason, indent=2)
+ self.registry_path.write_text(json.dumps(self.voter_registry, indent=2))
def cast_vote(self, id: int, monster: str) -> None:
"""
@@ -43,54 +40,55 @@ class MonsterSurvey(Cog):
If the user has already voted, their existing vote is removed.
"""
vr = self.voter_registry
- for m in vr.keys():
- if id not in vr[m]['votes'] and m == monster:
- vr[m]['votes'].append(id)
+ for m in vr:
+ if id not in vr[m]["votes"] and m == monster:
+ vr[m]["votes"].append(id)
else:
- if id in vr[m]['votes'] and m != monster:
- vr[m]['votes'].remove(id)
+ if id in vr[m]["votes"] and m != monster:
+ vr[m]["votes"].remove(id)
def get_name_by_leaderboard_index(self, n: int) -> str:
"""Return the monster at the specified leaderboard index."""
n = n - 1
vr = self.voter_registry
- top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True)
+ top = sorted(vr, key=lambda k: len(vr[k]["votes"]), reverse=True)
name = top[n] if n >= 0 else None
return name
@commands.group(
- name='monster',
- aliases=('mon',)
+ name="monster",
+ aliases=("mon",)
)
async def monster_group(self, ctx: Context) -> None:
"""The base voting command. If nothing is called, then it will return an embed."""
if ctx.invoked_subcommand is None:
async with ctx.typing():
default_embed = Embed(
- title='Monster Voting',
+ title="Monster Voting",
color=0xFF6800,
- description='Vote for your favorite monster!'
+ description="Vote for your favorite monster!"
)
default_embed.add_field(
- name='.monster show monster_name(optional)',
- value='Show a specific monster. If none is listed, it will give you an error with valid choices.',
- inline=False)
+ name=".monster show monster_name(optional)",
+ value="Show a specific monster. If none is listed, it will give you an error with valid choices.",
+ inline=False
+ )
default_embed.add_field(
- name='.monster vote monster_name',
- value='Vote for a specific monster. You get one vote, but can change it at any time.',
+ name=".monster vote monster_name",
+ value="Vote for a specific monster. You get one vote, but can change it at any time.",
inline=False
)
default_embed.add_field(
- name='.monster leaderboard',
- value='Which monster has the most votes? This command will tell you.',
+ name=".monster leaderboard",
+ value="Which monster has the most votes? This command will tell you.",
inline=False
)
- default_embed.set_footer(text=f"Monsters choices are: {', '.join(self.voter_registry.keys())}")
+ default_embed.set_footer(text=f"Monsters choices are: {', '.join(self.voter_registry)}")
await ctx.send(embed=default_embed)
@monster_group.command(
- name='vote'
+ name="vote"
)
async def monster_vote(self, ctx: Context, name: str = None) -> None:
"""
@@ -111,37 +109,37 @@ class MonsterSurvey(Cog):
name = name.lower()
vote_embed = Embed(
- name='Monster Voting',
+ name="Monster Voting",
color=0xFF6800
)
m = self.voter_registry.get(name)
if m is None:
- vote_embed.description = f'You cannot vote for {name} because it\'s not in the running.'
+ vote_embed.description = f"You cannot vote for {name} because it's not in the running."
vote_embed.add_field(
- name='Use `.monster show {monster_name}` for more information on a specific monster',
- value='or use `.monster vote {monster}` to cast your vote for said monster.',
+ name="Use `.monster show {monster_name}` for more information on a specific monster",
+ value="or use `.monster vote {monster}` to cast your vote for said monster.",
inline=False
)
vote_embed.add_field(
- name='You may vote for or show the following monsters:',
- value=f"{', '.join(self.voter_registry.keys())}"
+ name="You may vote for or show the following monsters:",
+ value=", ".join(self.voter_registry.keys())
)
else:
self.cast_vote(ctx.author.id, name)
vote_embed.add_field(
- name='Vote successful!',
- value=f'You have successfully voted for {m["full_name"]}!',
+ name="Vote successful!",
+ value=f"You have successfully voted for {m['full_name']}!",
inline=False
)
- vote_embed.set_thumbnail(url=m['image'])
+ vote_embed.set_thumbnail(url=m["image"])
vote_embed.set_footer(text="Please note that any previous votes have been removed.")
self.json_write()
await ctx.send(embed=vote_embed)
@monster_group.command(
- name='show'
+ name="show"
)
async def monster_show(self, ctx: Context, name: str = None) -> None:
"""Shows the named monster. If one is not named, it sends the default voting embed instead."""
@@ -159,41 +157,43 @@ class MonsterSurvey(Cog):
m = self.voter_registry.get(name)
if not m:
- await ctx.send('That monster does not exist.')
+ await ctx.send("That monster does not exist.")
await ctx.invoke(self.monster_vote)
return
- embed = Embed(title=m['full_name'], color=0xFF6800)
- embed.add_field(name='Summary', value=m['summary'])
- embed.set_image(url=m['image'])
- embed.set_footer(text=f'To vote for this monster, type .monster vote {name}')
+ embed = Embed(title=m["full_name"], color=0xFF6800)
+ embed.add_field(name="Summary", value=m["summary"])
+ embed.set_image(url=m["image"])
+ embed.set_footer(text=f"To vote for this monster, type .monster vote {name}")
await ctx.send(embed=embed)
@monster_group.command(
- name='leaderboard',
- aliases=('lb',)
+ name="leaderboard",
+ aliases=("lb",)
)
async def monster_leaderboard(self, ctx: Context) -> None:
"""Shows the current standings."""
async with ctx.typing():
vr = self.voter_registry
- top = sorted(vr, key=lambda k: len(vr[k]['votes']), reverse=True)
- total_votes = sum(len(m['votes']) for m in self.voter_registry.values())
+ top = sorted(vr, key=lambda k: len(vr[k]["votes"]), reverse=True)
+ total_votes = sum(len(m["votes"]) for m in self.voter_registry.values())
embed = Embed(title="Monster Survey Leader Board", color=0xFF6800)
for rank, m in enumerate(top):
- votes = len(vr[m]['votes'])
+ votes = len(vr[m]["votes"])
percentage = ((votes / total_votes) * 100) if total_votes > 0 else 0
- embed.add_field(name=f"{rank+1}. {vr[m]['full_name']}",
- value=(
- f"{votes} votes. {percentage:.1f}% of total votes.\n"
- f"Vote for this monster by typing "
- f"'.monster vote {m}'\n"
- f"Get more information on this monster by typing "
- f"'.monster show {m}'"
- ),
- inline=False)
+ embed.add_field(
+ name=f"{rank+1}. {vr[m]['full_name']}",
+ value=(
+ f"{votes} votes. {percentage:.1f}% of total votes.\n"
+ f"Vote for this monster by typing "
+ f"'.monster vote {m}'\n"
+ f"Get more information on this monster by typing "
+ f"'.monster show {m}'"
+ ),
+ inline=False
+ )
embed.set_footer(text="You can also vote by their rank number. '.monster vote {number}' ")
@@ -201,4 +201,5 @@ class MonsterSurvey(Cog):
def setup(bot: Bot) -> None:
- """Monster survey Cog load."""
+ """Load the Monster Survey Cog."""
+ bot.add_cog(MonsterSurvey())
diff --git a/bot/exts/halloween/scarymovie.py b/bot/exts/halloween/scarymovie.py
index 0807eca6..f4cf41db 100644
--- a/bot/exts/halloween/scarymovie.py
+++ b/bot/exts/halloween/scarymovie.py
@@ -2,24 +2,25 @@ import logging
import random
from os import environ
-import aiohttp
from discord import Embed
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-TMDB_API_KEY = environ.get('TMDB_API_KEY')
-TMDB_TOKEN = environ.get('TMDB_TOKEN')
+TMDB_API_KEY = environ.get("TMDB_API_KEY")
+TMDB_TOKEN = environ.get("TMDB_TOKEN")
class ScaryMovie(commands.Cog):
"""Selects a random scary movie and embeds info into Discord chat."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
- @commands.command(name='scarymovie', alias=['smovie'])
+ @commands.command(name="scarymovie", alias=["smovie"])
async def random_movie(self, ctx: commands.Context) -> None:
"""Randomly select a scary movie and display information about it."""
async with ctx.typing():
@@ -28,36 +29,34 @@ class ScaryMovie(commands.Cog):
await ctx.send(embed=movie_details)
- @staticmethod
- async def select_movie() -> dict:
+ async def select_movie(self) -> dict:
"""Selects a random movie and returns a JSON of movie details from TMDb."""
- url = 'https://api.themoviedb.org/4/discover/movie'
+ url = "https://api.themoviedb.org/4/discover/movie"
params = {
- 'with_genres': '27',
- 'vote_count.gte': '5'
+ "with_genres": "27",
+ "vote_count.gte": "5"
}
headers = {
- 'Authorization': 'Bearer ' + TMDB_TOKEN,
- 'Content-Type': 'application/json;charset=utf-8'
+ "Authorization": "Bearer " + TMDB_TOKEN,
+ "Content-Type": "application/json;charset=utf-8"
}
# Get total page count of horror movies
- async with aiohttp.ClientSession() as session:
- response = await session.get(url=url, params=params, headers=headers)
- total_pages = await response.json()
- total_pages = total_pages.get('total_pages')
-
- # Get movie details from one random result on a random page
- params['page'] = random.randint(1, total_pages)
- response = await session.get(url=url, params=params, headers=headers)
- response = await response.json()
- selection_id = random.choice(response.get('results')).get('id')
-
- # Get full details and credits
- selection = await session.get(
- url='https://api.themoviedb.org/3/movie/' + str(selection_id),
- params={'api_key': TMDB_API_KEY, 'append_to_response': 'credits'}
- )
+ async with self.bot.http_session.get(url=url, params=params, headers=headers) as response:
+ data = await response.json()
+ total_pages = data.get("total_pages")
+
+ # Get movie details from one random result on a random page
+ params["page"] = random.randint(1, total_pages)
+ async with self.bot.http_session.get(url=url, params=params, headers=headers) as response:
+ data = await response.json()
+ selection_id = random.choice(data.get("results")).get("id")
+
+ # Get full details and credits
+ async with self.bot.http_session.get(
+ url=f"https://api.themoviedb.org/3/movie/{selection_id}",
+ params={"api_key": TMDB_API_KEY, "append_to_response": "credits"}
+ ) as selection:
return await selection.json()
@@ -67,40 +66,37 @@ class ScaryMovie(commands.Cog):
# Build the relevant URLs.
movie_id = movie.get("id")
poster_path = movie.get("poster_path")
- tmdb_url = f'https://www.themoviedb.org/movie/{movie_id}' if movie_id else None
- poster = f'https://image.tmdb.org/t/p/original{poster_path}' if poster_path else None
+ tmdb_url = f"https://www.themoviedb.org/movie/{movie_id}" if movie_id else None
+ poster = f"https://image.tmdb.org/t/p/original{poster_path}" if poster_path else None
# Get cast names
cast = []
- for actor in movie.get('credits', {}).get('cast', [])[:3]:
- cast.append(actor.get('name'))
+ for actor in movie.get("credits", {}).get("cast", [])[:3]:
+ cast.append(actor.get("name"))
# Get director name
- director = movie.get('credits', {}).get('crew', [])
+ director = movie.get("credits", {}).get("crew", [])
if director:
- director = director[0].get('name')
+ director = director[0].get("name")
# Determine the spookiness rating
- rating = ''
- rating_count = movie.get('vote_average', 0)
-
- if rating_count:
- rating_count /= 2
+ rating = ""
+ rating_count = movie.get("vote_average", 0) / 2
for _ in range(int(rating_count)):
- rating += ':skull:'
+ rating += ":skull:"
if (rating_count % 1) >= .5:
- rating += ':bat:'
+ rating += ":bat:"
# Try to get year of release and runtime
- year = movie.get('release_date', [])[:4]
- runtime = movie.get('runtime')
+ year = movie.get("release_date", [])[:4]
+ runtime = movie.get("runtime")
runtime = f"{runtime} minutes" if runtime else None
# Not all these attributes will always be present
movie_attributes = {
"Directed by": director,
- "Starring": ', '.join(cast),
+ "Starring": ", ".join(cast),
"Running time": runtime,
"Release year": year,
"Spookiness rating": rating,
@@ -108,9 +104,9 @@ class ScaryMovie(commands.Cog):
embed = Embed(
colour=0x01d277,
- title='**' + movie.get('title') + '**',
+ title=f"**{movie.get('title')}**",
url=tmdb_url,
- description=movie.get('overview')
+ description=movie.get("overview")
)
if poster:
@@ -127,6 +123,6 @@ class ScaryMovie(commands.Cog):
return embed
-def setup(bot: commands.Bot) -> None:
- """Scary movie Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Scary Movie Cog."""
bot.add_cog(ScaryMovie(bot))
diff --git a/bot/exts/halloween/spookygif.py b/bot/exts/halloween/spookygif.py
index f402437f..9511d407 100644
--- a/bot/exts/halloween/spookygif.py
+++ b/bot/exts/halloween/spookygif.py
@@ -1,38 +1,38 @@
import logging
-import aiohttp
import discord
from discord.ext import commands
-from bot.constants import Tokens
+from bot.bot import Bot
+from bot.constants import Colours, Tokens
log = logging.getLogger(__name__)
+API_URL = "http://api.giphy.com/v1/gifs/random"
+
class SpookyGif(commands.Cog):
"""A cog to fetch a random spooky gif from the web!"""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@commands.command(name="spookygif", aliases=("sgif", "scarygif"))
async def spookygif(self, ctx: commands.Context) -> None:
"""Fetches a random gif from the GIPHY API and responds with it."""
async with ctx.typing():
- async with aiohttp.ClientSession() as session:
- params = {'api_key': Tokens.giphy, 'tag': 'halloween', 'rating': 'g'}
- # Make a GET request to the Giphy API to get a random halloween gif.
- async with session.get('http://api.giphy.com/v1/gifs/random', params=params) as resp:
- data = await resp.json()
- url = data['data']['image_url']
+ params = {"api_key": Tokens.giphy, "tag": "halloween", "rating": "g"}
+ # Make a GET request to the Giphy API to get a random halloween gif.
+ async with self.bot.http_session.get(API_URL, params=params) as resp:
+ data = await resp.json()
+ url = data["data"]["image_url"]
- embed = discord.Embed(colour=0x9b59b6)
- embed.title = "A spooooky gif!"
- embed.set_image(url=url)
+ embed = discord.Embed(title="A spooooky gif!", colour=Colours.purple)
+ embed.set_image(url=url)
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
+def setup(bot: Bot) -> None:
"""Spooky GIF Cog load."""
bot.add_cog(SpookyGif(bot))
diff --git a/bot/exts/halloween/spookynamerate.py b/bot/exts/halloween/spookynamerate.py
index e2950343..3d6d95fa 100644
--- a/bot/exts/halloween/spookynamerate.py
+++ b/bot/exts/halloween/spookynamerate.py
@@ -6,14 +6,15 @@ from datetime import datetime, timedelta
from logging import getLogger
from os import getenv
from pathlib import Path
-from typing import Dict, Union
+from typing import Union
from async_rediscache import RedisCache
from discord import Embed, Reaction, TextChannel, User
from discord.colour import Colour
from discord.ext import tasks
-from discord.ext.commands import Bot, Cog, Context, group
+from discord.ext.commands import Cog, Context, group
+from bot.bot import Bot
from bot.constants import Channels, Client, Colours, Month
from bot.utils.decorators import InMonthCheckFailure
@@ -34,7 +35,7 @@ ADDED_MESSAGES = [
]
PING = "<@{id}>"
-EMOJI_MESSAGE = "\n".join([f"- {emoji} {val}" for emoji, val in EMOJIS_VAL.items()])
+EMOJI_MESSAGE = "\n".join(f"- {emoji} {val}" for emoji, val in EMOJIS_VAL.items())
HELP_MESSAGE_DICT = {
"title": "Spooky Name Rate",
"description": f"Help for the `{Client.prefix}spookynamerate` command",
@@ -64,6 +65,11 @@ HELP_MESSAGE_DICT = {
],
}
+# The names are from https://www.mockaroo.com/
+NAMES = json.loads(Path("bot/resources/halloween/spookynamerate_names.json").read_text("utf8"))
+FIRST_NAMES = NAMES["first_names"]
+LAST_NAMES = NAMES["last_names"]
+
class SpookyNameRate(Cog):
"""
@@ -80,21 +86,13 @@ class SpookyNameRate(Cog):
# The data cache stores small information such as the current name that is going on and whether it is the first time
# the bot is running
data = RedisCache()
- debug = getenv('SPOOKYNAMERATE_DEBUG', False) # Enable if you do not want to limit the commands to October or if
+ debug = getenv("SPOOKYNAMERATE_DEBUG", False) # Enable if you do not want to limit the commands to October or if
# you do not want to wait till 12 UTC. Note: if debug is enabled and you run `.cogs reload spookynamerate`, it
# will automatically start the scoring and announcing the result (without waiting for 12, so do not expect it to.).
# Also, it won't wait for the two hours (when the poll closes).
def __init__(self, bot: Bot) -> None:
self.bot = bot
-
- names_data = self.load_json(
- Path("bot", "resources", "halloween", "spookynamerate_names.json")
- )
- self.first_names = names_data["first_names"]
- self.last_names = names_data["last_names"]
- # the names are from https://www.mockaroo.com/
-
self.name = None
self.bot.loop.create_task(self.load_vars())
@@ -116,7 +114,7 @@ class SpookyNameRate(Cog):
"""Get help on the Spooky Name Rate game."""
await ctx.send(embed=Embed.from_dict(HELP_MESSAGE_DICT))
- @spooky_name_rate.command(name="list", aliases=["all", "entries"])
+ @spooky_name_rate.command(name="list", aliases=("all", "entries"))
async def list_entries(self, ctx: Context) -> None:
"""Send all the entries up till now in a single embed."""
await ctx.send(embed=await self.get_responses_list(final=False))
@@ -133,18 +131,16 @@ class SpookyNameRate(Cog):
"add an entry."
)
- @spooky_name_rate.command(name="add", aliases=["register"])
+ @spooky_name_rate.command(name="add", aliases=("register",))
async def add_name(self, ctx: Context, *, name: str) -> None:
"""Use this command to add/register your spookified name."""
if self.poll:
- logger.info(f"{ctx.message.author} tried to add a name, but the poll had already started.")
+ logger.info(f"{ctx.author} tried to add a name, but the poll had already started.")
await ctx.send("Sorry, the poll has started! You can try and participate in the next round though!")
return
- message = ctx.message
-
for data in (json.loads(user_data) for _, user_data in await self.messages.items()):
- if data["author"] == message.author.id:
+ if data["author"] == ctx.author.id:
await ctx.send(
"But you have already added an entry! Type "
f"`{self.bot.command_prefix}spookynamerate "
@@ -156,14 +152,14 @@ class SpookyNameRate(Cog):
await ctx.send("TOO LATE. Someone has already added this name.")
return
- msg = await (await self.get_channel()).send(f"{message.author.mention} added the name {name!r}!")
+ msg = await (await self.get_channel()).send(f"{ctx.author.mention} added the name {name!r}!")
await self.messages.set(
msg.id,
json.dumps(
{
"name": name,
- "author": message.author.id,
+ "author": ctx.author.id,
"score": 0,
}
),
@@ -172,7 +168,7 @@ class SpookyNameRate(Cog):
for emoji in EMOJIS_VAL:
await msg.add_reaction(emoji)
- logger.info(f"{message.author} added the name {name!r}")
+ logger.info(f"{ctx.author} added the name {name!r}")
@spooky_name_rate.command(name="delete")
async def delete_name(self, ctx: Context) -> None:
@@ -185,7 +181,7 @@ class SpookyNameRate(Cog):
if ctx.author.id == data["author"]:
await self.messages.delete(message_id)
- await ctx.send(f'Name deleted successfully ({data["name"]!r})!')
+ await ctx.send(f"Name deleted successfully ({data['name']!r})!")
return
await ctx.send(
@@ -303,7 +299,7 @@ class SpookyNameRate(Cog):
await self.messages.clear() # reset the messages
# send the next name
- self.name = f"{random.choice(self.first_names)} {random.choice(self.last_names)}"
+ self.name = f"{random.choice(FIRST_NAMES)} {random.choice(LAST_NAMES)}"
await self.data.set("name", self.name)
await channel.send(
@@ -370,12 +366,6 @@ class SpookyNameRate(Cog):
return channel
@staticmethod
- def load_json(file: Path) -> Dict[str, str]:
- """Loads a JSON file and returns its contents."""
- with file.open("r", encoding="utf-8") as f:
- return json.load(f)
-
- @staticmethod
def in_allowed_month() -> bool:
"""Returns whether running in the limited month."""
if SpookyNameRate.debug:
@@ -397,5 +387,5 @@ class SpookyNameRate(Cog):
def setup(bot: Bot) -> None:
- """Loads the SpookyNameRate Cog."""
+ """Load the SpookyNameRate Cog."""
bot.add_cog(SpookyNameRate(bot))
diff --git a/bot/exts/halloween/spookyrating.py b/bot/exts/halloween/spookyrating.py
index 6f069f8c..105d2164 100644
--- a/bot/exts/halloween/spookyrating.py
+++ b/bot/exts/halloween/spookyrating.py
@@ -3,24 +3,24 @@ import json
import logging
import random
from pathlib import Path
+from typing import Dict
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-with Path("bot/resources/halloween/spooky_rating.json").open(encoding="utf8") as file:
- SPOOKY_DATA = json.load(file)
- SPOOKY_DATA = sorted((int(key), value) for key, value in SPOOKY_DATA.items())
+data: Dict[str, Dict[str, str]] = json.loads(Path("bot/resources/halloween/spooky_rating.json").read_text("utf8"))
+SPOOKY_DATA = sorted((int(key), value) for key, value in data.items())
class SpookyRating(commands.Cog):
"""A cog for calculating one's spooky rating."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
+ def __init__(self):
self.local_random = random.Random()
@commands.command()
@@ -46,21 +46,21 @@ class SpookyRating(commands.Cog):
_, data = SPOOKY_DATA[index]
embed = discord.Embed(
- title=data['title'],
- description=f'{who} scored {spooky_percent}%!',
+ title=data["title"],
+ description=f"{who} scored {spooky_percent}%!",
color=Colours.orange
)
embed.add_field(
- name='A whisper from Satan',
- value=data['text']
+ name="A whisper from Satan",
+ value=data["text"]
)
embed.set_thumbnail(
- url=data['image']
+ url=data["image"]
)
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Spooky Rating Cog load."""
- bot.add_cog(SpookyRating(bot))
+def setup(bot: Bot) -> None:
+ """Load the Spooky Rating Cog."""
+ bot.add_cog(SpookyRating())
diff --git a/bot/exts/halloween/spookyreact.py b/bot/exts/halloween/spookyreact.py
index b335df75..25e783f4 100644
--- a/bot/exts/halloween/spookyreact.py
+++ b/bot/exts/halloween/spookyreact.py
@@ -2,21 +2,22 @@ import logging
import re
import discord
-from discord.ext.commands import Bot, Cog
+from discord.ext.commands import Cog
+from bot.bot import Bot
from bot.constants import Month
from bot.utils.decorators import in_month
log = logging.getLogger(__name__)
SPOOKY_TRIGGERS = {
- 'spooky': (r"\bspo{2,}ky\b", "\U0001F47B"),
- 'skeleton': (r"\bskeleton\b", "\U0001F480"),
- 'doot': (r"\bdo{2,}t\b", "\U0001F480"),
- 'pumpkin': (r"\bpumpkin\b", "\U0001F383"),
- 'halloween': (r"\bhalloween\b", "\U0001F383"),
- 'jack-o-lantern': (r"\bjack-o-lantern\b", "\U0001F383"),
- 'danger': (r"\bdanger\b", "\U00002620")
+ "spooky": (r"\bspo{2,}ky\b", "\U0001F47B"),
+ "skeleton": (r"\bskeleton\b", "\U0001F480"),
+ "doot": (r"\bdo{2,}t\b", "\U0001F480"),
+ "pumpkin": (r"\bpumpkin\b", "\U0001F383"),
+ "halloween": (r"\bhalloween\b", "\U0001F383"),
+ "jack-o-lantern": (r"\bjack-o-lantern\b", "\U0001F383"),
+ "danger": (r"\bdanger\b", "\U00002620")
}
@@ -28,20 +29,20 @@ class SpookyReact(Cog):
@in_month(Month.OCTOBER)
@Cog.listener()
- async def on_message(self, ctx: discord.Message) -> None:
+ async def on_message(self, message: discord.Message) -> None:
"""Triggered when the bot sees a message in October."""
- for trigger in SPOOKY_TRIGGERS.keys():
- trigger_test = re.search(SPOOKY_TRIGGERS[trigger][0], ctx.content.lower())
+ for name, trigger in SPOOKY_TRIGGERS.items():
+ trigger_test = re.search(trigger[0], message.content.lower())
if trigger_test:
# Check message for bot replies and/or command invocations
# Short circuit if they're found, logging is handled in _short_circuit_check
- if await self._short_circuit_check(ctx):
+ if await self._short_circuit_check(message):
return
else:
- await ctx.add_reaction(SPOOKY_TRIGGERS[trigger][1])
- logging.info(f"Added '{trigger}' reaction to message ID: {ctx.id}")
+ await message.add_reaction(trigger[1])
+ log.info(f"Added {name!r} reaction to message ID: {message.id}")
- async def _short_circuit_check(self, ctx: discord.Message) -> bool:
+ async def _short_circuit_check(self, message: discord.Message) -> bool:
"""
Short-circuit helper check.
@@ -50,20 +51,20 @@ class SpookyReact(Cog):
* prefix is not None
"""
# Check for self reaction
- if ctx.author == self.bot.user:
- logging.debug(f"Ignoring reactions on self message. Message ID: {ctx.id}")
+ if message.author == self.bot.user:
+ log.debug(f"Ignoring reactions on self message. Message ID: {message.id}")
return True
# Check for command invocation
# Because on_message doesn't give a full Context object, generate one first
- tmp_ctx = await self.bot.get_context(ctx)
- if tmp_ctx.prefix:
- logging.debug(f"Ignoring reactions on command invocation. Message ID: {ctx.id}")
+ ctx = await self.bot.get_context(message)
+ if ctx.prefix:
+ log.debug(f"Ignoring reactions on command invocation. Message ID: {message.id}")
return True
return False
def setup(bot: Bot) -> None:
- """Spooky reaction Cog load."""
+ """Load the Spooky Reaction Cog."""
bot.add_cog(SpookyReact(bot))
diff --git a/bot/exts/halloween/timeleft.py b/bot/exts/halloween/timeleft.py
index 47adb09b..e80025dc 100644
--- a/bot/exts/halloween/timeleft.py
+++ b/bot/exts/halloween/timeleft.py
@@ -4,15 +4,14 @@ from typing import Tuple
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
class TimeLeft(commands.Cog):
"""A Cog that tells you how long left until Hacktober is over!"""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
def in_hacktober(self) -> bool:
"""Return True if the current time is within Hacktoberfest."""
_, end, start = self.load_date()
@@ -64,6 +63,6 @@ class TimeLeft(commands.Cog):
)
-def setup(bot: commands.Bot) -> None:
- """Cog load."""
- bot.add_cog(TimeLeft(bot))
+def setup(bot: Bot) -> None:
+ """Load the Time Left Cog."""
+ bot.add_cog(TimeLeft())
diff --git a/bot/exts/internal_eval/_internal_eval.py b/bot/exts/internal_eval/_internal_eval.py
index 757a2a1e..56bf5add 100644
--- a/bot/exts/internal_eval/_internal_eval.py
+++ b/bot/exts/internal_eval/_internal_eval.py
@@ -114,7 +114,7 @@ class InternalEval(commands.Cog):
"""Evaluate the `code` in the current evaluation context."""
context_vars = {
"message": ctx.message,
- "author": ctx.message.author,
+ "author": ctx.author,
"channel": ctx.channel,
"guild": ctx.guild,
"ctx": ctx,
@@ -142,14 +142,14 @@ class InternalEval(commands.Cog):
log.trace("Sending the formatted output back to the context")
await self._send_output(ctx, eval_context.format_output())
- @commands.group(name='internal', aliases=('int',))
+ @commands.group(name="internal", aliases=("int",))
@with_role(Roles.admin)
async def internal_group(self, ctx: commands.Context) -> None:
"""Internal commands. Top secret!"""
if not ctx.invoked_subcommand:
await invoke_help_command(ctx)
- @internal_group.command(name='eval', aliases=('e',))
+ @internal_group.command(name="eval", aliases=("e",))
@with_role(Roles.admin)
async def eval(self, ctx: commands.Context, *, code: str) -> None:
"""Run eval in a REPL-like format."""
@@ -157,7 +157,7 @@ class InternalEval(commands.Cog):
blocks = [block for block in match if block.group("block")]
if len(blocks) > 1:
- code = '\n'.join(block.group("code") for block in blocks)
+ code = "\n".join(block.group("code") for block in blocks)
else:
match = match[0] if len(blocks) == 0 else blocks[0]
code, block, lang, delim = match.group("code", "block", "lang", "delim")
@@ -168,7 +168,7 @@ class InternalEval(commands.Cog):
code = textwrap.dedent(code)
await self._eval(ctx, code)
- @internal_group.command(name='reset', aliases=("clear", "exit", "r", "c"))
+ @internal_group.command(name="reset", aliases=("clear", "exit", "r", "c"))
@with_role(Roles.admin)
async def reset(self, ctx: commands.Context) -> None:
"""Reset the context and locals of the eval session."""
diff --git a/bot/exts/pride/drag_queen_name.py b/bot/exts/pride/drag_queen_name.py
index fca9750f..15ca6576 100644
--- a/bot/exts/pride/drag_queen_name.py
+++ b/bot/exts/pride/drag_queen_name.py
@@ -5,28 +5,22 @@ from pathlib import Path
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
+NAMES = json.loads(Path("bot/resources/pride/drag_queen_names.json").read_text("utf8"))
+
class DragNames(commands.Cog):
"""Gives a random drag queen name!"""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.names = self.load_names()
-
- @staticmethod
- def load_names() -> list:
- """Loads a list of drag queen names."""
- with open(Path("bot/resources/pride/drag_queen_names.json"), "r", encoding="utf8") as f:
- return json.load(f)
-
- @commands.command(name="dragname", aliases=["dragqueenname", "queenme"])
+ @commands.command(name="dragname", aliases=("dragqueenname", "queenme"))
async def dragname(self, ctx: commands.Context) -> None:
"""Sends a message with a drag queen name."""
- await ctx.send(random.choice(self.names))
+ await ctx.send(random.choice(NAMES))
-def setup(bot: commands.Bot) -> None:
- """Cog loader for drag queen name generator."""
- bot.add_cog(DragNames(bot))
+def setup(bot: Bot) -> None:
+ """Load the Drag Names Cog."""
+ bot.add_cog(DragNames())
diff --git a/bot/exts/pride/pride_anthem.py b/bot/exts/pride/pride_anthem.py
index 33cb2a9d..05286b3d 100644
--- a/bot/exts/pride/pride_anthem.py
+++ b/bot/exts/pride/pride_anthem.py
@@ -2,20 +2,21 @@ import json
import logging
import random
from pathlib import Path
+from typing import Optional
from discord.ext import commands
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
+VIDEOS = json.loads(Path("bot/resources/pride/anthems.json").read_text("utf8"))
+
class PrideAnthem(commands.Cog):
"""Embed a random youtube video for a gay anthem!"""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
- self.anthems = self.load_vids()
-
- def get_video(self, genre: str = None) -> dict:
+ def get_video(self, genre: Optional[str] = None) -> dict:
"""
Picks a random anthem from the list.
@@ -23,22 +24,15 @@ class PrideAnthem(commands.Cog):
If none can be found, it will log this as well as provide that information to the user.
"""
if not genre:
- return random.choice(self.anthems)
+ return random.choice(VIDEOS)
else:
- songs = [song for song in self.anthems if genre.casefold() in song["genre"]]
+ songs = [song for song in VIDEOS if genre.casefold() in song["genre"]]
try:
return random.choice(songs)
except IndexError:
log.info("No videos for that genre.")
- @staticmethod
- def load_vids() -> list:
- """Loads a list of videos from the resources folder as dictionaries."""
- with open(Path("bot/resources/pride/anthems.json"), "r", encoding="utf8") as f:
- anthems = json.load(f)
- return anthems
-
- @commands.command(name="prideanthem", aliases=["anthem", "pridesong"])
+ @commands.command(name="prideanthem", aliases=("anthem", "pridesong"))
async def prideanthem(self, ctx: commands.Context, genre: str = None) -> None:
"""
Sends a message with a video of a random pride anthem.
@@ -52,6 +46,6 @@ class PrideAnthem(commands.Cog):
await ctx.send("I couldn't find a video, sorry!")
-def setup(bot: commands.Bot) -> None:
- """Cog loader for pride anthem."""
- bot.add_cog(PrideAnthem(bot))
+def setup(bot: Bot) -> None:
+ """Load the Pride Anthem Cog."""
+ bot.add_cog(PrideAnthem())
diff --git a/bot/exts/pride/pride_facts.py b/bot/exts/pride/pride_facts.py
index 5bd5d0ce..63e33dda 100644
--- a/bot/exts/pride/pride_facts.py
+++ b/bot/exts/pride/pride_facts.py
@@ -15,7 +15,7 @@ from bot.utils.decorators import seasonal_task
log = logging.getLogger(__name__)
-Sendable = Union[commands.Context, discord.TextChannel]
+FACTS = json.loads(Path("bot/resources/pride/facts.json").read_text("utf8"))
class PrideFacts(commands.Cog):
@@ -23,16 +23,8 @@ class PrideFacts(commands.Cog):
def __init__(self, bot: Bot):
self.bot = bot
- self.facts = self.load_facts()
-
self.daily_fact_task = self.bot.loop.create_task(self.send_pride_fact_daily())
- @staticmethod
- def load_facts() -> dict:
- """Loads a dictionary of years mapping to lists of facts."""
- with open(Path("bot/resources/pride/facts.json"), "r", encoding="utf8") as f:
- return json.load(f)
-
@seasonal_task(Month.JUNE)
async def send_pride_fact_daily(self) -> None:
"""Background task to post the daily pride fact every day."""
@@ -44,15 +36,15 @@ class PrideFacts(commands.Cog):
async def send_random_fact(self, ctx: commands.Context) -> None:
"""Provides a fact from any previous day, or today."""
now = datetime.utcnow()
- previous_years_facts = (self.facts[x] for x in self.facts.keys() if int(x) < now.year)
- current_year_facts = self.facts.get(str(now.year), [])[:now.day]
+ previous_years_facts = (y for x, y in FACTS.items() if int(x) < now.year)
+ current_year_facts = FACTS.get(str(now.year), [])[:now.day]
previous_facts = current_year_facts + [x for y in previous_years_facts for x in y]
try:
await ctx.send(embed=self.make_embed(random.choice(previous_facts)))
except IndexError:
await ctx.send("No facts available")
- async def send_select_fact(self, target: Sendable, _date: Union[str, datetime]) -> None:
+ async def send_select_fact(self, target: discord.abc.Messageable, _date: Union[str, datetime]) -> None:
"""Provides the fact for the specified day, if the day is today, or is in the past."""
now = datetime.utcnow()
if isinstance(_date, str):
@@ -65,7 +57,7 @@ class PrideFacts(commands.Cog):
date = _date
if date.year < now.year or (date.year == now.year and date.day <= now.day):
try:
- await target.send(embed=self.make_embed(self.facts[str(date.year)][date.day - 1]))
+ await target.send(embed=self.make_embed(FACTS[str(date.year)][date.day - 1]))
except KeyError:
await target.send(f"The year {date.year} is not yet supported")
return
@@ -75,8 +67,8 @@ class PrideFacts(commands.Cog):
else:
await target.send("The fact for the selected day is not yet available.")
- @commands.command(name="pridefact", aliases=["pridefacts"])
- async def pridefact(self, ctx: commands.Context) -> None:
+ @commands.command(name="pridefact", aliases=("pridefacts",))
+ async def pridefact(self, ctx: commands.Context, option: str = None) -> None:
"""
Sends a message with a pride fact of the day.
@@ -85,15 +77,15 @@ class PrideFacts(commands.Cog):
If a date is given as an argument, and the date is in the past, the fact from that day
will be provided.
"""
- message_body = ctx.message.content[len(ctx.invoked_with) + 2:]
- if message_body == "":
+ if not option:
await self.send_select_fact(ctx, datetime.utcnow())
- elif message_body.lower().startswith("rand"):
+ elif option.lower().startswith("rand"):
await self.send_random_fact(ctx)
else:
- await self.send_select_fact(ctx, message_body)
+ await self.send_select_fact(ctx, option)
- def make_embed(self, fact: str) -> discord.Embed:
+ @staticmethod
+ def make_embed(fact: str) -> discord.Embed:
"""Makes a nice embed for the fact to be sent."""
return discord.Embed(
colour=Colours.pink,
@@ -103,5 +95,5 @@ class PrideFacts(commands.Cog):
def setup(bot: Bot) -> None:
- """Cog loader for pride facts."""
+ """Load the Pride Facts Cog."""
bot.add_cog(PrideFacts(bot))
diff --git a/bot/exts/pride/pride_leader.py b/bot/exts/pride/pride_leader.py
new file mode 100644
index 00000000..c3426ad1
--- /dev/null
+++ b/bot/exts/pride/pride_leader.py
@@ -0,0 +1,117 @@
+import json
+import logging
+import random
+from pathlib import Path
+from typing import Optional
+
+import discord
+from discord.ext import commands
+from fuzzywuzzy import fuzz
+
+from bot import bot
+from bot import constants
+
+log = logging.getLogger(__name__)
+
+PRIDE_RESOURCE = json.loads(Path("bot/resources/pride/prideleader.json").read_text("utf8"))
+MINIMUM_FUZZ_RATIO = 40
+
+
+class PrideLeader(commands.Cog):
+ """Gives information about Pride Leaders."""
+
+ def __init__(self, bot: bot.Bot):
+ self.bot = bot
+
+ def invalid_embed_generate(self, pride_leader: str) -> discord.Embed:
+ """
+ Generates Invalid Embed.
+
+ The invalid embed contains a list of closely matched names of the invalid pride
+ leader the user gave. If no closely matched names are found it would list all
+ the available pride leader names.
+
+ Wikipedia is a useful place to learn about pride leaders and we don't have all
+ the pride leaders, so the bot would add a field containing the wikipedia
+ command to execute.
+ """
+ embed = discord.Embed(
+ color=constants.Colours.soft_red
+ )
+ valid_names = []
+ pride_leader = pride_leader.title()
+ for name in PRIDE_RESOURCE:
+ if fuzz.ratio(pride_leader, name) >= MINIMUM_FUZZ_RATIO:
+ valid_names.append(name)
+
+ if not valid_names:
+ valid_names = ", ".join(PRIDE_RESOURCE)
+ error_msg = "Sorry your input didn't match any stored names, here is a list of available names:"
+ else:
+ valid_names = "\n".join(valid_names)
+ error_msg = "Did you mean?"
+
+ embed.description = f"{error_msg}\n```{valid_names}```"
+ embed.set_footer(text="To add more pride leaders, feel free to open a pull request!")
+
+ return embed
+
+ def embed_builder(self, pride_leader: dict) -> discord.Embed:
+ """Generate an Embed with information about a pride leader."""
+ name = [name for name, info in PRIDE_RESOURCE.items() if info == pride_leader][0]
+
+ embed = discord.Embed(
+ title=name,
+ description=pride_leader["About"],
+ color=constants.Colours.blue
+ )
+ embed.add_field(
+ name="Known for",
+ value=pride_leader["Known for"],
+ inline=False
+ )
+ embed.add_field(
+ name="D.O.B and Birth place",
+ value=pride_leader["Born"],
+ inline=False
+ )
+ embed.add_field(
+ name="Awards and honors",
+ value=pride_leader["Awards"],
+ inline=False
+ )
+ embed.add_field(
+ name="For More Information",
+ value=f"Do `{constants.Client.prefix}wiki {name}`"
+ f" in <#{constants.Channels.community_bot_commands}>",
+ inline=False
+ )
+ embed.set_thumbnail(url=pride_leader["url"])
+ return embed
+
+ @commands.command(aliases=("pl", "prideleader"))
+ async def pride_leader(self, ctx: commands.Context, *, pride_leader_name: Optional[str]) -> None:
+ """
+ Information about a Pride Leader.
+
+ Returns information about the specified pride leader
+ and if there is no pride leader given, return a random pride leader.
+ """
+ if not pride_leader_name:
+ leader = random.choice(list(PRIDE_RESOURCE.values()))
+ else:
+ leader = PRIDE_RESOURCE.get(pride_leader_name.title())
+ if not leader:
+ log.trace(f"Got a Invalid pride leader: {pride_leader_name}")
+
+ embed = self.invalid_embed_generate(pride_leader_name)
+ await ctx.send(embed=embed)
+ return
+
+ embed = self.embed_builder(leader)
+ await ctx.send(embed=embed)
+
+
+def setup(bot: bot.Bot) -> None:
+ """Load the Pride Leader Cog."""
+ bot.add_cog(PrideLeader(bot))
diff --git a/bot/exts/valentines/be_my_valentine.py b/bot/exts/valentines/be_my_valentine.py
index 09591cf8..8b522a72 100644
--- a/bot/exts/valentines/be_my_valentine.py
+++ b/bot/exts/valentines/be_my_valentine.py
@@ -1,13 +1,13 @@
import logging
import random
-from json import load
+from json import loads
from pathlib import Path
from typing import Tuple
import discord
from discord.ext import commands
-from discord.ext.commands.cooldowns import BucketType
+from bot.bot import Bot
from bot.constants import Channels, Colours, Lovefest, Month
from bot.utils.decorators import in_month
from bot.utils.extensions import invoke_help_command
@@ -20,7 +20,7 @@ HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_hea
class BeMyValentine(commands.Cog):
"""A cog that sends Valentines to other users!"""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
self.valentines = self.load_json()
@@ -28,9 +28,7 @@ class BeMyValentine(commands.Cog):
def load_json() -> dict:
"""Load Valentines messages from the static resources."""
p = Path("bot/resources/valentines/bemyvalentine_valentines.json")
- with p.open(encoding="utf8") as json_data:
- valentines = load(json_data)
- return valentines
+ return loads(p.read_text("utf8"))
@in_month(Month.FEBRUARY)
@commands.group(name="lovefest")
@@ -50,8 +48,8 @@ class BeMyValentine(commands.Cog):
async def add_role(self, ctx: commands.Context) -> None:
"""Adds the lovefest role."""
user = ctx.author
- role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id)
- if Lovefest.role_id not in [role.id for role in ctx.message.author.roles]:
+ role = ctx.guild.get_role(Lovefest.role_id)
+ if role not in ctx.author.roles:
await user.add_roles(role)
await ctx.send("The Lovefest role has been added !")
else:
@@ -61,15 +59,15 @@ class BeMyValentine(commands.Cog):
async def remove_role(self, ctx: commands.Context) -> None:
"""Removes the lovefest role."""
user = ctx.author
- role = discord.utils.get(ctx.guild.roles, id=Lovefest.role_id)
- if Lovefest.role_id not in [role.id for role in ctx.message.author.roles]:
+ role = ctx.guild.get_role(Lovefest.role_id)
+ if role not in ctx.author.roles:
await ctx.send("You dont have the lovefest role.")
else:
await user.remove_roles(role)
- await ctx.send("The lovefest role has been successfully removed !")
+ await ctx.send("The lovefest role has been successfully removed!")
- @commands.cooldown(1, 1800, BucketType.user)
- @commands.group(name='bemyvalentine', invoke_without_command=True)
+ @commands.cooldown(1, 1800, commands.BucketType.user)
+ @commands.group(name="bemyvalentine", invoke_without_command=True)
async def send_valentine(
self, ctx: commands.Context, user: discord.Member, *, valentine_type: str = None
) -> None:
@@ -101,14 +99,14 @@ class BeMyValentine(commands.Cog):
valentine, title = self.valentine_check(valentine_type)
embed = discord.Embed(
- title=f'{emoji_1} {title} {user.display_name} {emoji_2}',
- description=f'{valentine} \n **{emoji_2}From {ctx.author}{emoji_1}**',
+ title=f"{emoji_1} {title} {user.display_name} {emoji_2}",
+ description=f"{valentine} \n **{emoji_2}From {ctx.author}{emoji_1}**",
color=Colours.pink
)
await channel.send(user.mention, embed=embed)
- @commands.cooldown(1, 1800, BucketType.user)
- @send_valentine.command(name='secret')
+ @commands.cooldown(1, 1800, commands.BucketType.user)
+ @send_valentine.command(name="secret")
async def anonymous(
self, ctx: commands.Context, user: discord.Member, *, valentine_type: str = None
) -> None:
@@ -136,8 +134,8 @@ class BeMyValentine(commands.Cog):
valentine, title = self.valentine_check(valentine_type)
embed = discord.Embed(
- title=f'{emoji_1}{title} {user.display_name}{emoji_2}',
- description=f'{valentine} \n **{emoji_2}From anonymous{emoji_1}**',
+ title=f"{emoji_1}{title} {user.display_name}{emoji_2}",
+ description=f"{valentine} \n **{emoji_2}From anonymous{emoji_1}**",
color=Colours.pink
)
await ctx.message.delete()
@@ -151,21 +149,17 @@ class BeMyValentine(commands.Cog):
def valentine_check(self, valentine_type: str) -> Tuple[str, str]:
"""Return the appropriate Valentine type & title based on the invoking user's input."""
if valentine_type is None:
- valentine, title = self.random_valentine()
+ return self.random_valentine()
- elif valentine_type.lower() in ['p', 'poem']:
- valentine = self.valentine_poem()
- title = 'A poem dedicated to'
+ elif valentine_type.lower() in ["p", "poem"]:
+ return self.valentine_poem(), "A poem dedicated to"
- elif valentine_type.lower() in ['c', 'compliment']:
- valentine = self.valentine_compliment()
- title = 'A compliment for'
+ elif valentine_type.lower() in ["c", "compliment"]:
+ return self.valentine_compliment(), "A compliment for"
else:
# in this case, the user decides to type his own valentine.
- valentine = valentine_type
- title = 'A message for'
- return valentine, title
+ return valentine_type, "A message for"
@staticmethod
def random_emoji() -> Tuple[str, str]:
@@ -176,26 +170,24 @@ class BeMyValentine(commands.Cog):
def random_valentine(self) -> Tuple[str, str]:
"""Grabs a random poem or a compliment (any message)."""
- valentine_poem = random.choice(self.valentines['valentine_poems'])
- valentine_compliment = random.choice(self.valentines['valentine_compliments'])
+ valentine_poem = random.choice(self.valentines["valentine_poems"])
+ valentine_compliment = random.choice(self.valentines["valentine_compliments"])
random_valentine = random.choice([valentine_compliment, valentine_poem])
if random_valentine == valentine_poem:
- title = 'A poem dedicated to'
+ title = "A poem dedicated to"
else:
- title = 'A compliment for '
+ title = "A compliment for "
return random_valentine, title
def valentine_poem(self) -> str:
"""Grabs a random poem."""
- valentine_poem = random.choice(self.valentines['valentine_poems'])
- return valentine_poem
+ return random.choice(self.valentines["valentine_poems"])
def valentine_compliment(self) -> str:
"""Grabs a random compliment."""
- valentine_compliment = random.choice(self.valentines['valentine_compliments'])
- return valentine_compliment
+ return random.choice(self.valentines["valentine_compliments"])
-def setup(bot: commands.Bot) -> None:
- """Be my Valentine Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Be my Valentine Cog."""
bot.add_cog(BeMyValentine(bot))
diff --git a/bot/exts/valentines/lovecalculator.py b/bot/exts/valentines/lovecalculator.py
index 966acc82..b10b7bca 100644
--- a/bot/exts/valentines/lovecalculator.py
+++ b/bot/exts/valentines/lovecalculator.py
@@ -11,20 +11,18 @@ from discord import Member
from discord.ext import commands
from discord.ext.commands import BadArgument, Cog, clean_content
+from bot.bot import Bot
+
log = logging.getLogger(__name__)
-with Path("bot/resources/valentines/love_matches.json").open(encoding="utf8") as file:
- LOVE_DATA = json.load(file)
- LOVE_DATA = sorted((int(key), value) for key, value in LOVE_DATA.items())
+LOVE_DATA = json.loads(Path("bot/resources/valentines/love_matches.json").read_text("utf8"))
+LOVE_DATA = sorted((int(key), value) for key, value in LOVE_DATA.items())
class LoveCalculator(Cog):
"""A cog for calculating the love between two people."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=('love_calculator', 'love_calc'))
+ @commands.command(aliases=("love_calculator", "love_calc"))
@commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
async def love(self, ctx: commands.Context, who: Union[Member, str], whom: Union[Member, str] = None) -> None:
"""
@@ -62,7 +60,7 @@ class LoveCalculator(Cog):
# Make sure user didn't provide something silly such as 10 spaces
if not (who and whom):
- raise BadArgument('Arguments be non-empty strings.')
+ raise BadArgument("Arguments must be non-empty strings.")
# Hash inputs to guarantee consistent results (hashing algorithm choice arbitrary)
#
@@ -79,20 +77,20 @@ class LoveCalculator(Cog):
# We only need the dict, so we can ditch the first element
_, data = LOVE_DATA[index]
- status = random.choice(data['titles'])
+ status = random.choice(data["titles"])
embed = discord.Embed(
title=status,
- description=f'{who} \N{HEAVY BLACK HEART} {whom} scored {love_percent}%!\n\u200b',
+ description=f"{who} \N{HEAVY BLACK HEART} {whom} scored {love_percent}%!\n\u200b",
color=discord.Color.dark_magenta()
)
embed.add_field(
- name='A letter from Dr. Love:',
- value=data['text']
+ name="A letter from Dr. Love:",
+ value=data["text"]
)
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Love calculator Cog load."""
- bot.add_cog(LoveCalculator(bot))
+def setup(bot: Bot) -> None:
+ """Load the Love calculator Cog."""
+ bot.add_cog(LoveCalculator())
diff --git a/bot/exts/valentines/movie_generator.py b/bot/exts/valentines/movie_generator.py
index 4df9e0d5..0fc5edb4 100644
--- a/bot/exts/valentines/movie_generator.py
+++ b/bot/exts/valentines/movie_generator.py
@@ -6,6 +6,8 @@ from urllib import parse
import discord
from discord.ext import commands
+from bot.bot import Bot
+
TMDB_API_KEY = environ.get("TMDB_API_KEY")
log = logging.getLogger(__name__)
@@ -14,7 +16,7 @@ log = logging.getLogger(__name__)
class RomanceMovieFinder(commands.Cog):
"""A Cog that returns a random romance movie suggestion to a user."""
- def __init__(self, bot: commands.Bot):
+ def __init__(self, bot: Bot):
self.bot = bot
@commands.command(name="romancemovie")
@@ -52,13 +54,15 @@ class RomanceMovieFinder(commands.Cog):
embed.set_thumbnail(url="https://i.imgur.com/LtFtC8H.png")
await ctx.send(embed=embed)
except KeyError:
- warning_message = "A KeyError was raised while fetching information on the movie. The API service" \
- " could be unavailable or the API key could be set incorrectly."
+ warning_message = (
+ "A KeyError was raised while fetching information on the movie. The API service"
+ " could be unavailable or the API key could be set incorrectly."
+ )
embed = discord.Embed(title=warning_message)
log.warning(warning_message)
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Romance movie Cog load."""
+def setup(bot: Bot) -> None:
+ """Load the Romance movie Cog."""
bot.add_cog(RomanceMovieFinder(bot))
diff --git a/bot/exts/valentines/myvalenstate.py b/bot/exts/valentines/myvalenstate.py
index 01801847..52a61011 100644
--- a/bot/exts/valentines/myvalenstate.py
+++ b/bot/exts/valentines/myvalenstate.py
@@ -7,20 +7,17 @@ from random import choice
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-with open(Path("bot/resources/valentines/valenstates.json"), "r", encoding="utf8") as file:
- STATES = json.load(file)
+STATES = json.loads(Path("bot/resources/valentines/valenstates.json").read_text("utf8"))
class MyValenstate(commands.Cog):
"""A Cog to find your most likely Valentine's vacation destination."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
def levenshtein(self, source: str, goal: str) -> int:
"""Calculates the Levenshtein Distance between source and goal."""
if len(source) < len(goal):
@@ -46,12 +43,12 @@ class MyValenstate(commands.Cog):
"""Find the vacation spot(s) with the most matching characters to the invoking user."""
eq_chars = collections.defaultdict(int)
if name is None:
- author = ctx.message.author.name.lower().replace(' ', '')
+ author = ctx.author.name.lower().replace(" ", "")
else:
- author = name.lower().replace(' ', '')
+ author = name.lower().replace(" ", "")
for state in STATES.keys():
- lower_state = state.lower().replace(' ', '')
+ lower_state = state.lower().replace(" ", "")
eq_chars[state] = self.levenshtein(author, lower_state)
matches = [x for x, y in eq_chars.items() if y == min(eq_chars.values())]
@@ -60,27 +57,26 @@ class MyValenstate(commands.Cog):
embed_title = "But there are more!"
if len(matches) > 1:
- leftovers = f"{', '.join(matches[:len(matches)-2])}, and {matches[len(matches)-1]}"
+ leftovers = f"{', '.join(matches[:-2])}, and {matches[-1]}"
embed_text = f"You have {len(matches)} more matches, these being {leftovers}."
elif len(matches) == 1:
embed_title = "But there's another one!"
- leftovers = str(matches)
- embed_text = f"You have another match, this being {leftovers}."
+ embed_text = f"You have another match, this being {matches[0]}."
else:
embed_title = "You have a true match!"
embed_text = "This state is your true Valenstate! There are no states that would suit" \
" you better"
embed = discord.Embed(
- title=f'Your Valenstate is {valenstate} \u2764',
- description=f'{STATES[valenstate]["text"]}',
+ title=f"Your Valenstate is {valenstate} \u2764",
+ description=STATES[valenstate]["text"],
colour=Colours.pink
)
embed.add_field(name=embed_title, value=embed_text)
embed.set_image(url=STATES[valenstate]["flag"])
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Valenstate Cog load."""
- bot.add_cog(MyValenstate(bot))
+def setup(bot: Bot) -> None:
+ """Load the Valenstate Cog."""
+ bot.add_cog(MyValenstate())
diff --git a/bot/exts/valentines/pickuplines.py b/bot/exts/valentines/pickuplines.py
index 74c7e68b..00741a72 100644
--- a/bot/exts/valentines/pickuplines.py
+++ b/bot/exts/valentines/pickuplines.py
@@ -1,25 +1,22 @@
import logging
import random
-from json import load
+from json import loads
from pathlib import Path
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-with open(Path("bot/resources/valentines/pickup_lines.json"), "r", encoding="utf8") as f:
- pickup_lines = load(f)
+PICKUP_LINES = loads(Path("bot/resources/valentines/pickup_lines.json").read_text("utf8"))
class PickupLine(commands.Cog):
"""A cog that gives random cheesy pickup lines."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@commands.command()
async def pickupline(self, ctx: commands.Context) -> None:
"""
@@ -27,18 +24,18 @@ class PickupLine(commands.Cog):
Note that most of them are very cheesy.
"""
- random_line = random.choice(pickup_lines['lines'])
+ random_line = random.choice(PICKUP_LINES["lines"])
embed = discord.Embed(
- title=':cheese: Your pickup line :cheese:',
- description=random_line['line'],
+ title=":cheese: Your pickup line :cheese:",
+ description=random_line["line"],
color=Colours.pink
)
embed.set_thumbnail(
- url=random_line.get('image', pickup_lines['placeholder'])
+ url=random_line.get("image", PICKUP_LINES["placeholder"])
)
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Pickup lines Cog load."""
- bot.add_cog(PickupLine(bot))
+def setup(bot: Bot) -> None:
+ """Load the Pickup lines Cog."""
+ bot.add_cog(PickupLine())
diff --git a/bot/exts/valentines/savethedate.py b/bot/exts/valentines/savethedate.py
index ac38d279..ffe559d6 100644
--- a/bot/exts/valentines/savethedate.py
+++ b/bot/exts/valentines/savethedate.py
@@ -1,31 +1,28 @@
import logging
import random
-from json import load
+from json import loads
from pathlib import Path
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_heart:", ":two_hearts:"]
-with open(Path("bot/resources/valentines/date_ideas.json"), "r", encoding="utf8") as f:
- VALENTINES_DATES = load(f)
+VALENTINES_DATES = loads(Path("bot/resources/valentines/date_ideas.json").read_text("utf8"))
class SaveTheDate(commands.Cog):
"""A cog that gives random suggestion for a Valentine's date."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
@commands.command()
async def savethedate(self, ctx: commands.Context) -> None:
"""Gives you ideas for what to do on a date with your valentine."""
- random_date = random.choice(VALENTINES_DATES['ideas'])
+ random_date = random.choice(VALENTINES_DATES["ideas"])
emoji_1 = random.choice(HEART_EMOJIS)
emoji_2 = random.choice(HEART_EMOJIS)
embed = discord.Embed(
@@ -36,6 +33,6 @@ class SaveTheDate(commands.Cog):
await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Save the date Cog Load."""
- bot.add_cog(SaveTheDate(bot))
+def setup(bot: Bot) -> None:
+ """Load the Save the date Cog."""
+ bot.add_cog(SaveTheDate())
diff --git a/bot/exts/valentines/valentine_zodiac.py b/bot/exts/valentines/valentine_zodiac.py
index 2696999f..d862ee63 100644
--- a/bot/exts/valentines/valentine_zodiac.py
+++ b/bot/exts/valentines/valentine_zodiac.py
@@ -9,19 +9,19 @@ from typing import Tuple, Union
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-LETTER_EMOJI = ':love_letter:'
+LETTER_EMOJI = ":love_letter:"
HEART_EMOJIS = [":heart:", ":gift_heart:", ":revolving_hearts:", ":sparkling_heart:", ":two_hearts:"]
class ValentineZodiac(commands.Cog):
"""A Cog that returns a counter compatible zodiac sign to the given user's zodiac sign."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
+ def __init__(self):
self.zodiacs, self.zodiac_fact = self.load_comp_json()
@staticmethod
@@ -29,14 +29,14 @@ class ValentineZodiac(commands.Cog):
"""Load zodiac compatibility from static JSON resource."""
explanation_file = Path("bot/resources/valentines/zodiac_explanation.json")
compatibility_file = Path("bot/resources/valentines/zodiac_compatibility.json")
- with explanation_file.open(encoding="utf8") as json_data:
- zodiac_fact = json.load(json_data)
- for zodiac_data in zodiac_fact.values():
- zodiac_data['start_at'] = datetime.fromisoformat(zodiac_data['start_at'])
- zodiac_data['end_at'] = datetime.fromisoformat(zodiac_data['end_at'])
- with compatibility_file.open(encoding="utf8") as json_data:
- zodiacs = json.load(json_data)
+ zodiac_fact = json.loads(explanation_file.read_text("utf8"))
+
+ for zodiac_data in zodiac_fact.values():
+ zodiac_data["start_at"] = datetime.fromisoformat(zodiac_data["start_at"])
+ zodiac_data["end_at"] = datetime.fromisoformat(zodiac_data["end_at"])
+
+ zodiacs = json.loads(compatibility_file.read_text("utf8"))
return zodiacs, zodiac_fact
@@ -62,10 +62,10 @@ class ValentineZodiac(commands.Cog):
log.trace("Making zodiac embed.")
embed.title = f"__{zodiac}__"
embed.description = self.zodiac_fact[zodiac]["About"]
- embed.add_field(name='__Motto__', value=self.zodiac_fact[zodiac]["Motto"], inline=False)
- embed.add_field(name='__Strengths__', value=self.zodiac_fact[zodiac]["Strengths"], inline=False)
- embed.add_field(name='__Weaknesses__', value=self.zodiac_fact[zodiac]["Weaknesses"], inline=False)
- embed.add_field(name='__Full form__', value=self.zodiac_fact[zodiac]["full_form"], inline=False)
+ embed.add_field(name="__Motto__", value=self.zodiac_fact[zodiac]["Motto"], inline=False)
+ embed.add_field(name="__Strengths__", value=self.zodiac_fact[zodiac]["Strengths"], inline=False)
+ embed.add_field(name="__Weaknesses__", value=self.zodiac_fact[zodiac]["Weaknesses"], inline=False)
+ embed.add_field(name="__Full form__", value=self.zodiac_fact[zodiac]["full_form"], inline=False)
embed.set_thumbnail(url=self.zodiac_fact[zodiac]["url"])
else:
embed = self.generate_invalidname_embed(zodiac)
@@ -79,7 +79,7 @@ class ValentineZodiac(commands.Cog):
log.trace("Zodiac name sent.")
return zodiac_name
- @commands.group(name='zodiac', invoke_without_command=True)
+ @commands.group(name="zodiac", invoke_without_command=True)
async def zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None:
"""Provides information about zodiac sign by taking zodiac sign name as input."""
final_embed = self.zodiac_build_embed(zodiac_sign)
@@ -93,9 +93,9 @@ class ValentineZodiac(commands.Cog):
month = month.capitalize()
try:
month = list(calendar.month_abbr).index(month[:3])
- log.trace('Valid month name entered by user')
+ log.trace("Valid month name entered by user")
except ValueError:
- log.info('Invalid month name entered by user')
+ log.info("Invalid month name entered by user")
await ctx.send(f"Sorry, but `{month}` is not a valid month name.")
return
if (month == 1 and 1 <= date <= 19) or (month == 12 and 22 <= date <= 31):
@@ -109,14 +109,14 @@ class ValentineZodiac(commands.Cog):
final_embed = discord.Embed()
final_embed.color = Colours.soft_red
final_embed.description = f"Zodiac sign could not be found because.\n```{e}```"
- log.info(f'Error in "zodiac date" command:\n{e}.')
+ log.info(f"Error in 'zodiac date' command:\n{e}.")
else:
final_embed = self.zodiac_build_embed(zodiac_sign_based_on_date)
await ctx.send(embed=final_embed)
log.trace("Embed from date successfully sent.")
- @zodiac.command(name="partnerzodiac", aliases=['partner'])
+ @zodiac.command(name="partnerzodiac", aliases=("partner",))
async def partner_zodiac(self, ctx: commands.Context, zodiac_sign: str) -> None:
"""Provides a random counter compatible zodiac sign to the given user's zodiac sign."""
embed = discord.Embed()
@@ -128,12 +128,12 @@ class ValentineZodiac(commands.Cog):
emoji2 = random.choice(HEART_EMOJIS)
embed.title = "Zodiac Compatibility"
embed.description = (
- f'{zodiac_sign.capitalize()}{emoji1}{compatible_zodiac["Zodiac"]}\n'
- f'{emoji2}Compatibility meter : {compatible_zodiac["compatibility_score"]}{emoji2}'
+ f"{zodiac_sign.capitalize()}{emoji1}{compatible_zodiac['Zodiac']}\n"
+ f"{emoji2}Compatibility meter : {compatible_zodiac['compatibility_score']}{emoji2}"
)
embed.add_field(
- name=f'A letter from Dr.Zodiac {LETTER_EMOJI}',
- value=compatible_zodiac['description']
+ name=f"A letter from Dr.Zodiac {LETTER_EMOJI}",
+ value=compatible_zodiac["description"]
)
else:
embed = self.generate_invalidname_embed(zodiac_sign)
@@ -141,6 +141,6 @@ class ValentineZodiac(commands.Cog):
log.trace("Embed from date successfully sent.")
-def setup(bot: commands.Bot) -> None:
- """Valentine zodiac Cog load."""
- bot.add_cog(ValentineZodiac(bot))
+def setup(bot: Bot) -> None:
+ """Load the Valentine zodiac Cog."""
+ bot.add_cog(ValentineZodiac())
diff --git a/bot/exts/valentines/whoisvalentine.py b/bot/exts/valentines/whoisvalentine.py
index 0ff9186c..211b1f27 100644
--- a/bot/exts/valentines/whoisvalentine.py
+++ b/bot/exts/valentines/whoisvalentine.py
@@ -6,47 +6,44 @@ from random import choice
import discord
from discord.ext import commands
+from bot.bot import Bot
from bot.constants import Colours
log = logging.getLogger(__name__)
-with open(Path("bot/resources/valentines/valentine_facts.json"), "r", encoding="utf8") as file:
- FACTS = json.load(file)
+FACTS = json.loads(Path("bot/resources/valentines/valentine_facts.json").read_text("utf8"))
class ValentineFacts(commands.Cog):
"""A Cog for displaying facts about Saint Valentine."""
- def __init__(self, bot: commands.Bot):
- self.bot = bot
-
- @commands.command(aliases=('whoisvalentine', 'saint_valentine'))
+ @commands.command(aliases=("whoisvalentine", "saint_valentine"))
async def who_is_valentine(self, ctx: commands.Context) -> None:
"""Displays info about Saint Valentine."""
embed = discord.Embed(
title="Who is Saint Valentine?",
- description=FACTS['whois'],
+ description=FACTS["whois"],
color=Colours.pink
)
embed.set_thumbnail(
- url='https://upload.wikimedia.org/wikipedia/commons/thumb/f/f1/Saint_Valentine_-_'
- 'facial_reconstruction.jpg/1024px-Saint_Valentine_-_facial_reconstruction.jpg'
+ url="https://upload.wikimedia.org/wikipedia/commons/thumb/f/f1/Saint_Valentine_-_"
+ "facial_reconstruction.jpg/1024px-Saint_Valentine_-_facial_reconstruction.jpg"
)
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
@commands.command()
async def valentine_fact(self, ctx: commands.Context) -> None:
"""Shows a random fact about Valentine's Day."""
embed = discord.Embed(
- title=choice(FACTS['titles']),
- description=choice(FACTS['text']),
+ title=choice(FACTS["titles"]),
+ description=choice(FACTS["text"]),
color=Colours.pink
)
- await ctx.channel.send(embed=embed)
+ await ctx.send(embed=embed)
-def setup(bot: commands.Bot) -> None:
- """Who is Valentine Cog load."""
- bot.add_cog(ValentineFacts(bot))
+def setup(bot: Bot) -> None:
+ """Load the Who is Valentine Cog."""
+ bot.add_cog(ValentineFacts())
diff --git a/bot/resources/evergreen/trivia_quiz.json b/bot/resources/evergreen/trivia_quiz.json
index a4225eb1..fee1b6d7 100644
--- a/bot/resources/evergreen/trivia_quiz.json
+++ b/bot/resources/evergreen/trivia_quiz.json
@@ -44,6 +44,24 @@
],
"question": "What was the first game Yoshi appeared in?",
"answer": "Super Mario World"
+ },
+ {
+ "id": 6,
+ "hints": [
+ "They were used alternatively to playing cards.",
+ "They generally have handdrawn nature images on them."
+ ],
+ "question": "What did Nintendo make before video games and toys?",
+ "answer": "Hanafuda, Hanafuda cards"
+ },
+ {
+ "id": 7,
+ "hints": [
+ "Before being Nintendo's main competitor in home gaming, they were successful in arcades.",
+ "Their first console was called the Master System."
+ ],
+ "question": "Who was Nintendo's biggest competitor in 1990?",
+ "answer": "Sega"
}
],
"general": [
@@ -269,5 +287,322 @@
"answer": "1492",
"info": "The explorer Christopher Columbus made four trips across the Atlantic Ocean from Spain: in 1492, 1493, 1498 and 1502. He was determined to find a direct water route west from Europe to Asia, but he never did. Instead, he stumbled upon the Americas"
}
+ ],
+ "math": [
+ {
+ "id": 201,
+ "question": "What is the highest power of a biquadratic polynomial?",
+ "answer": "4, four"
+ },
+ {
+ "id": 202,
+ "question": "What is the formula for surface area of a sphere?",
+ "answer": "4pir^2, 4πr^2"
+ },
+ {
+ "id": 203,
+ "question": "Which theorem states that hypotenuse^2 = base^2 + height^2?",
+ "answer": "Pythagorean's, Pythagorean's theorem"
+ },
+ {
+ "id": 204,
+ "question": "Which trigonometric function is defined as hypotenuse/opposite?",
+ "answer": "cosecant, cosec, csc"
+ },
+ {
+ "id": 205,
+ "question": "Does the harmonic series converge or diverge?",
+ "answer": "diverge"
+ },
+ {
+ "id": 206,
+ "question": "How many quadrants are there in a cartesian plane?",
+ "answer": "4, four"
+ },
+ {
+ "id": 207,
+ "question": "What is the (0,0) coordinate in a cartesian plane termed as?",
+ "answer": "origin"
+ },
+ {
+ "id": 208,
+ "question": "What's the following formula that finds the area of a triangle called?",
+ "img_url": "https://wikimedia.org/api/rest_v1/media/math/render/png/d22b8566e8187542966e8d166e72e93746a1a6fc",
+ "answer": "Heron's formula, Heron"
+ },
+ {
+ "id": 209,
+ "dynamic_id": 201,
+ "question": "Solve the following system of linear equations (format your answer like this <x> & <y>):\n{}x + {}y = {},\n{}x + {}y = {}",
+ "answer": "{} & {}"
+ },
+ {
+ "id": 210,
+ "dynamic_id": 202,
+ "question": "What's {} + {} mod {} congruent to?",
+ "answer": "{}"
+ },
+ {
+ "id": 211,
+ "question": "What is the bottom number on a fraction called?",
+ "answer": "denominator"
+ },
+ {
+ "id": 212,
+ "dynamic_id": 203,
+ "question": "How many vertices are on a {}gonal prism?",
+ "answer": "{}"
+ },
+ {
+ "id": 213,
+ "question": "What is the term used to describe two triangles that have equal corresponding sides and angle measures?",
+ "answer": "congruent"
+ },
+ {
+ "id": 214,
+ "question": "⅓πr^2h is the volume of which 3 dimensional figure?",
+ "answer": "cone"
+ },
+ {
+ "id": 215,
+ "dynamic_id": 204,
+ "question": "Find the square root of -{}.",
+ "answer": "{}i"
+ },
+ {
+ "id": 216,
+ "question": "In set builder notation, {p/q | q ≠ 0, p & q ∈ Z} represents what?",
+ "answer": "Rational Numbers"
+ },
+ {
+ "id": 217,
+ "question": "What is the natural log of -1 (use i for imaginary number)?",
+ "answer": "pi*i, pii, πi"
+ },
+ {
+ "id": 218,
+ "question": "When is the *inaugural* World Maths Day (format your answer in MM/DD)?",
+ "answer": "03/13"
+ },
+ {
+ "id": 219,
+ "question": "As the Fibonacci sequence extends to infinity, what's the ratio of each number `n` and its preceding number `n-1` approaching?",
+ "answer": "Golden Ratio"
+ },
+ {
+ "id": 220,
+ "question": "0, 1, 1, 2, 3, 5, 8, 13, 21, 34 are numbers of which sequence?",
+ "answer": "Fibonacci"
+ },
+ {
+ "id": 221,
+ "question": "Prime numbers only have __ factors.",
+ "answer": "2, two"
+ },
+ {
+ "id": 222,
+ "question": "In probability, the ________ ______ of an experiment or random trial is the set of all possible outcomes of it.",
+ "answer": "sample space"
+ },
+ {
+ "id": 223,
+ "question": "In statistics, what does this formula represent?",
+ "img_url": "https://www.statisticshowto.com/wp-content/uploads/2013/11/sample-standard-deviation.jpg",
+ "answer": "sample standard deviation, standard deviation of a sample"
+ },
+ {
+ "id": 224,
+ "question": "\"Hexakosioihexekontahexaphobia\" is the fear of which number?",
+ "answer": "666"
+ },
+ {
+ "id": 225,
+ "question": "A matrix multiplied by its inverse matrix equals...",
+ "answer": "the identity matrix, identity matrix"
+ },
+ {
+ "id": 226,
+ "dynamic_id": 205,
+ "question": "BASE TWO QUESTION: Calculate {:b} {} {:b}",
+ "answer": "{:b}"
+ },
+ {
+ "id": 227,
+ "question": "What is the only number in the entire number system which can be spelled with the same number of letters as itself?",
+ "answer": "4, four"
+
+ },
+ {
+ "id": 228,
+ "question": "1/100th of a second is also termed as what?",
+ "answer": "a jiffy, jiffy, centisecond"
+ },
+ {
+ "id": 229,
+ "question": "What is this triangle called?",
+ "img_url": "https://cdn.askpython.com/wp-content/uploads/2020/07/Pascals-triangle.png",
+ "answer": "Pascal's triangle, Pascal"
+ },
+ {
+ "id": 230,
+ "question": "6a^2 is the surface area of which 3 dimensional figure?",
+ "answer": "cube"
+ }
+ ],
+ "science": [
+ {
+ "id": 301,
+ "question": "The three main components of a normal atom are: protons, neutrons, and...",
+ "answer": "electrons"
+ },
+ {
+ "id": 302,
+ "question": "As of 2021, how many elements are there in the Periodic Table?",
+ "answer": "118"
+ },
+ {
+ "id": 303,
+ "question": "What is the universal force discovered by Newton that causes objects with mass to attract each other called?",
+ "answer": "gravity"
+ },
+ {
+ "id": 304,
+ "question": "What do you call an organism composed of only one cell?",
+ "answer": "unicellular, single-celled"
+ },
+ {
+ "id": 305,
+ "question": "The Heisenberg's Uncertainty Principle states that the position and ________ of a quantum object can't be both exactly measured at the same time.",
+ "answer": "velocity, momentum"
+ },
+ {
+ "id": 306,
+ "question": "A ____________ reaction is the one wherein an atom or a set of atoms is/are replaced by another atom or a set of atoms",
+ "answer": "displacement, exchange"
+ },
+ {
+ "id": 307,
+ "question": "What is the process by which green plants and certain other organisms transform light energy into chemical energy?",
+ "answer": "photosynthesis"
+ },
+ {
+ "id": 308,
+ "dynamic_id": 301,
+ "question": "What is the {} planet of our Solar System?",
+ "answer": "{}"
+ },
+ {
+ "id": 309,
+ "dynamic_id": 302,
+ "question": "In the biological taxonomic hierarchy, what is placed directly above {}?",
+ "answer": "{}"
+ },
+ {
+ "id": 310,
+ "dynamic_id": 303,
+ "question": "How does one describe the unit {} in SI base units?\n**IMPORTANT:** enclose answer in backticks, use \\* for multiplication, ^ for exponentiation, and place your base units in this order: m - kg - s - A",
+ "img_url": "https://i.imgur.com/NRzU6tf.png",
+ "answer": "`{}`"
+ },
+ {
+ "id": 311,
+ "question": "How does one call the direct phase transition from gas to solid?",
+ "answer": "deposition"
+ },
+ {
+ "id": 312,
+ "question": "What is the intermolecular force caused by temporary and induced dipoles?",
+ "answer": "LDF, London dispersion, London dispersion force"
+ },
+ {
+ "id": 313,
+ "question": "What is the force that causes objects to float in fluids called?",
+ "answer": "buoyancy"
+ },
+ {
+ "id": 314,
+ "question": "About how many neurons are in the human brain? (A. 1 billion, B. 10 billion, C. 100 billion, D. 300 billion)",
+ "answer": "C, 100 billion, 100 bil"
+ },
+ {
+ "id": 315,
+ "question": "What is the name of our galaxy group in which the Milky Way resides?",
+ "answer": "Local Group"
+ },
+ {
+ "id": 316,
+ "question": "Which cell organelle is nicknamed \"the powerhouse of the cell\"?",
+ "answer": "mitochondria"
+ },
+ {
+ "id": 317,
+ "question": "Which vascular tissue transports water and minerals from the roots to the rest of a plant?",
+ "answer": "the xylem, xylem"
+ },
+ {
+ "id": 318,
+ "question": "Who discovered the theories of relativity?",
+ "answer": "Albert Einstein, Einstein"
+ },
+ {
+ "id": 319,
+ "question": "In particle physics, the hypothetical isolated elementary particle with only one magnetic pole is termed as...",
+ "answer": "magnetic monopole"
+ },
+ {
+ "id": 320,
+ "question": "How does one describe a chemical reaction wherein heat is released?",
+ "answer": "exothermic"
+ },
+ {
+ "id": 321,
+ "question": "What range of frequency are the average human ears capable of hearing (A. 10Hz-10kHz, B. 20Hz-20kHz, C. 20Hz-2000Hz, D. 10kHz-20kHz)?",
+ "answer": "B, 20Hz-20kHz"
+ },
+ {
+ "id": 322,
+ "question": "What is the process used to separate substances with different polarity in a mixture, using a stationary and mobile phase?",
+ "answer": "chromatography"
+ },
+ {
+ "id": 323,
+ "question": "Which law states that the current through a conductor between two points is directly proportional to the voltage across the two points?",
+ "answer": "Ohm's law"
+ },
+ {
+ "id": 324,
+ "question": "The type of rock that is formed by the accumulation or deposition of mineral or organic particles at the Earth's surface, followed by cementation, is called...",
+ "answer": "sedimentary, sedimentary rock"
+ },
+ {
+ "id": 325,
+ "question": "Is the Richter scale (common earthquake scale) linear or logarithmic?",
+ "answer": "logarithmic"
+ },
+ {
+ "id": 326,
+ "question": "What type of image is formed by a convex mirror?",
+ "answer": "virtual image, virtual"
+ },
+ {
+ "id": 327,
+ "question": "How does one call the branch of physics that deals with the study of mechanical waves in gases, liquids, and solids including topics such as vibration, sound, ultrasound and infrasound",
+ "answer": "acoustics"
+ },
+ {
+ "id": 328,
+ "question": "Which law states that the global entropy in a closed system can only increase?",
+ "answer": "second law, second law of thermodynamics"
+ },
+ {
+ "id": 329,
+ "question": "Which particle is emitted during the beta decay of a radioactive element?",
+ "answer": "an electron, the electron, electron"
+ },
+ {
+ "id": 330,
+ "question": "When DNA is unzipped, two strands are formed. What are they called (separate both answers by the word \"and\")?",
+ "answer": "leading and lagging, leading strand and lagging strand"
+ }
]
}
diff --git a/bot/resources/pride/prideleader.json b/bot/resources/pride/prideleader.json
new file mode 100644
index 00000000..30e84bdc
--- /dev/null
+++ b/bot/resources/pride/prideleader.json
@@ -0,0 +1,100 @@
+{
+ "Tim Cook": {
+ "Known for": "CEO of Apple",
+ "About": "**Timothy Donald Cook** popularly known as Tim Cook. Despite being a notably private person, Tim Cook became the first CEO of a 500 fortune company, coming out as gay in 2014. He revealed his sexual orientation through an open letter published in Bloomberg BusinessWeek. He strongly believes that business leaders need to do their part to make the world a better place. An excerpt from his open letter is as follows: “Part of social progress is understanding that a person is not defined only by one's sexuality, race, or gender.",
+ "url": "https://image.cnbcfm.com/api/v1/image/105608434-1543945658496rts28qzc.jpg?v=1554921416&w=1400&h=950",
+ "Born": "In November 1, 1960 at Mobile, Alabama, U.S.",
+ "Awards": "• Financial Times Person of the Year (2014)\n• Ripple of Change Award (2015)\n• Fortune Magazine's: World's Greatest Leader. (2015)\n• Alabama Academy of Honor: Inductee. (2015)\n• Human Rights Campaign's Visibility Award (2015)\n• Honorary Doctor of Science from University of Glasgow in Glasgow, Scotland (2017)\n• Courage Against Hate award from Anti-Defamation League (2018)"
+ },
+ "Alan Joyce": {
+ "Known for": "CEO of Qantas Airlines",
+ "About": "**Alan Joseph Joyce, AC** popularly known as Alan Joyce. Alan Joyce has been named as one of the world’s most influential business executives. He has always been very open about his orientation and has been in a committed gay relationship for over 20 years now. He supports same-sex marriage and believes that it is critical to make people recognize that they know LGBT people\n\nAlan likes to frequently talk about equality at the workplace and said, “It has become more important for leaders who are LGBT to be open about their sexuality. I am passionate about it. There should be more people like Apple’s Tim Cook and Paul Zahra, the former CEO of Australia’s David Jones [store chain].”",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Alan_Joyce_%28cropped%29.jpg/220px-Alan_Joyce_%28cropped%29.jpg",
+ "Born": "On 30 June, 1966 at Tallaght, Dublin, Republic of Ireland",
+ "Awards": "• The Australian named Joyce the most influential business leader in 2011\n• Joyce is an Ambassador of the Australian Indigenous Education Foundation\n• Joyce named a Companion of the Order of Australia, Australia's highest civil honour, in the 2017 Queen's birthday honours list"
+ },
+ "Peter Thiel": {
+ "Known for": "Co-Founder and Former CEO of paypal",
+ "About": "**Peter Andreas Thiel** popularly known as Peter Thiel. Peter Thiel served as the CEO of PayPal from its inception to its sale to eBay in October 2002. Thiel’s sexuality came out in 2007 when Gawker Media outed him in a blog post. He became the first openly gay speaker at Republican National Convention and delivered a speech on sexuality.\n\n“Of course every American has a unique identity,” he said. “I am proud to be gay. I am proud to be a Republican. But most of all I am proud to be an American.”",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3f/Peter_Thiel_2014_by_Heisenberg_Media.jpg/220px-Peter_Thiel_2014_by_Heisenberg_Media.jpg",
+ "Born": "On 11 October, 1967 at Frankfurt, West Germany",
+ "Awards": "• Thiel received a co-producer credit for Thank You for Smoking, a 2005 feature film based on Christopher Buckley's 1994 novel of the same name\n• In 2006, Thiel won the Herman Lay Award for Entrepreneurship\n• In 2007, he was honored as a Young Global leader by the World Economic Forum as one of the 250 most distinguished leaders age 40 and under\n• On 7 November 2009, Thiel was awarded an honorary degree from Universidad Francisco Marroquin\n• In 2012, Students For Liberty, an organization dedicated to spreading libertarian ideals on college campuses, awarded Thiel its “Alumnus of the Year” award\n• In February 2013, Thiel received a TechCrunch Crunchie Award for Venture Capitalist of the Year.\n• Insurance Executive of the Year: St Joseph’s University’s Academy of Risk Management and Insurance in Philadelphia"
+ },
+ "Martine Rothblatt": {
+ "Known for": "CEO of United Therapeutics",
+ "About": "**Martine Aliana Rothblatt** popularly known as Martine Rothblatt. Martine co-founded Sirius XM Satellite Radio in 1990 and 1996 founded United Therapeutics, making her the highest-earning CEO in the biopharmaceutical industry. She came out as a transgender in 1994 and has been vocal about the trans community ever since.\n\nIn 2014 in an interview, Martine said, “I took a journey from male to female, so if I hide that, I’m, like just replicating the closet of my past with another closet of the future. That made no sense and that is why I am open.” She has authored a book called “The apartheid of Sex”.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Martine-Rothblatt2-5773.jpg/220px-Martine-Rothblatt2-5773.jpg",
+ "Born": "On October 10, 1954 at Chicago, Illinois, U.S.",
+ "Awards": "• In January 2018 Rothblatt was presented the UCLA Medal, the university's highest award, in recognition of her creation of Sirius XM satellite radio, advancing organ transplant technology, and having “expanded the way we understand fundamental concepts ranging from communication to gender to the nature of consciousness and mortality.”"
+ },
+ "Peter Arvai": {
+ "Known for": "Co-Founder and CEO of Prezi",
+ "About": "Peter Arvai is the 11th most influential LGBT leader in the world. His goal has always been to create an open environment that fosters diversity. He has co-founded an NGO with Google and espell called ‘We Are Open’ to promote openness at the workplace. Peter regularly talks about his gay background in every on-boarding session at his company.\n\nWhen Prezi was featured on the cover of Forbes, Peter used the opportunity by coming out and sharing his story to start a conversation on this topic that most people seem to avoid. If you want to create a more inclusive workplace, you need to be willing to be vulnerable yourself, he says. “To spark honest discussions about inclusivity and openness, your personal experience of inclusion is a key resource and you need to create a safe environment so people find the courage to have uncomfortable conversations.”",
+ "url": "https://cached.imagescaler.hbpl.co.uk/resize/scaleWidth/880/cached.offlinehbpl.hbpl.co.uk/news/OTM/Peterarvai-20191218101617863.jpg",
+ "Born": "On October 26, 1979 at Karlskoga, Sweden",
+ "Awards": "• 2014: European Tech Startups Award for Best Startup Co-Founders.\n• 2014: European Web Entrepreneur of the Year.\n• 2015: Executive of the Year – Business Services: Bronze Stevie Winner.\n• 2016: Number 11 on the 2016 OUTstanding & Financial Times Leading LGBT Executives List of 100"
+ },
+ "Inga Beale": {
+ "Known for": "CEO of Lloyd's of London",
+ "About": "Inga became the first female CEO of Lloyd’s of London in 2013 and in 2017 was named a Dame Commander of the Order of the British Empire for her contribution towards the British economy.\n\nShe came out first as a bisexual, in an interview in 2008 and since then has made efforts to bring diversity and inclusion at the forefront in her company. “It’s not about me. It’s about what you do for other people. For me, it’s so important because you need these role models.”\n\nSpeaking between meetings at the World Economic Forum in Davos, she says her position at the top of the LGBT table is important for its impact on others: “It’s about giving people confidence,” she says.",
+ "url": "https://cdn-res.keymedia.com/cms/images/us/018/0248_637072371134927741.jpeg",
+ "Born": "On May 15, 1963 at Great Britain",
+ "Awards": "• Trailblazer of the Year: The Insurance Industry Charitable Foundation (IICF)(2019)\n• Free Enterprise Award: Insurance Federation of New York (IFNY)(2018)\n• Market People - Outstanding Contribution Award: The London Market Forums(2018)\n• Outstanding Achievement Award: Insurance Day | Informa(2018)\n• Barclay's Lifetime Achievement Award: Variety Children's Charity - Catherine Awards(2017)\n• Insurance Woman of the Year Award: Association of Professional Insurance Women (APIW)(2017)\n• Dame Commander of the Order of the British Empire - DBE: HM The Queen(2017)\n• Insurance Personality of the Year: British Insurance Awards\n• Insurance Executive of the Year: St Joseph’s University’s Academy of Risk Management and Insurance in Philadelphia(2015)"
+ },
+ "David Geffen": {
+ "Known for": "Founder of Dreamworks",
+ "About": "**David Lawrence Geffen** popularly known as David Geffen. Founder of film studio Dream Works as well as record labels Asylum Records, Geffen Records and DGC Records, David Geffen came out in 1992 at a fund raiser announcing, “As a Gay man, I have come a long way to be here tonight.” He was already among the strongest pillars of the gay rights movement by then.\n\n“If I am going to be a role model, I want to be one that I can be proud of,” Geffen said in an interview back in 1992.”\n\nGeffen contributed significantly towards society through the David Geffen Foundation that worked relentlessly towards healthcare, people affected by HIV/AIDS, civil liberties, issues of concern to the Jewish community, and arts. Interestingly, the song ‘Free man in Paris’ by Joni Mitchell is based on Geffen’s time in Paris during a trip they took together along with Canadian musician Robbie Robertson and his wife.",
+ "url": "https://i.insider.com/5b733a2be199f31d138b4bec?width=1100&format=jpeg&auto=webp",
+ "Born": "On February 21, 1943 at New York City, U.S.",
+ "Awards": "• Tony Award for Best Musical(1983)\n• Tony Award for Best Play(1988)\n• Daytime Emmy Award for Outstanding Children's Animated Program(1990)"
+ },
+ "Joel Simkhai": {
+ "Known for": "Founder and former CEO of Grindr and Blendr",
+ "About": "Joel Simkhai founded Grindr, a dating app for men in the LGBTQ+ community in 2009. He says he launched the app with a “selfish desire’ to meet more gay men. Today, Grindr has over 4 million users and has become the world's largest social networking platform for men from the LGBTQ+ community to interact.\n\nIn an interview Joel shared, “ As a kid I was teased, teased for being feminine, I guess for being gay. So a lot of my early life was just in denial about it with myself and others. But by 16 and as I started getting older, I realized that I like guys”. “My story is the story of every gay man. We are seen as a minority in some ways and the services out there weren’t that great. I am an introvert so I don’t really do well at being who I am right away but online seemed like my comfort zone”. It all begins with meeting someone, he adds.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/44/Joel_Simkhai_2012_%28cropped%29.jpg/220px-Joel_Simkhai_2012_%28cropped%29.jpg",
+ "Born": "On 1976 at Tel Aviv, Israel",
+ "Awards": "• Simkhai’s company has been mentioned in pop culture icons like The Office, Saturday Night Live, Judge Judy and Top Gear and won a number of industry awards including “Best Mobile Dating App” in 2011 and 2012 and “Best New Technology” in 2012 from the iDate Awards and “Best Location Application” at the TechCrunch Awards in 2011."
+ },
+ "Megan Smith": {
+ "Known for": "Former CTO of United States",
+ "About": "Megan Smith the former CTO of the United States has always been vocal about the need to push inclusion. Most central to her message, however, is the key insight that is most often lost: not only is inclusivity a part of technology’s future, it was also a seminal part of its past. Ada Lovelace, for example, an English woman born in 1812, was the first computer programmer; Katherine G. Johnson, an African-American woman featured in the Oscar-nominated film Hidden Figures, helped put NASA astronauts on the moon.\n\nIn 2003, in an interview Megan said, “When you are gay, you come out everyday because everyone assumes you are straight. But you have to be yourself.” Smith also hopes to open up the tech industry to more women and encourage girls to pursue a career in it.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/f/f0/Megan_Smith_official_portrait.jpg/220px-Megan_Smith_official_portrait.jpg",
+ "Born": "On October 21, 1964 at Buffalo, New York, and Fort Erie, Ontario.",
+ "Awards": "• World Economic Forum Technology Pioneer 2001, 2002\n• Listed by Out magazine in 2012 and 2013, as one of the 50 most powerful LGBT people in the United States\n• Reuters Digital Vision Program Fellow at Stanford, 2003-2004\n• Top 25 Women on the Web, 2000\n• Upside Magazine 100 Digital Elite, 1999 and 2000\n• Advertising Age i.20, 1999\n• GLAAD Interactive Media Award for Internet Leadership, 1999\n• Charging Buffalo Award, 2015\n• Business Insider 23 Most Powerful LGBTQ+ People in Tech, 2019"
+ },
+ "David Bohnett": {
+ "Known for": "Founder of Geocities",
+ "About": "**David C. Bohnett** popularly known as David Bohnett. A tech entrepreneur, with his LA-based venture firm Baroda Ventures, founded in 1998, David Bohnett is also the Founder of Geocities, which remains the first largest internet venture built on user-generated content, founded in 1998 and acquired by Yahoo in 1999. Geocities was cited #2 by TechRadar in its list of ‘20 websites that changed the world’ back in 2008.\n\nBohnett came out to his family post his graduation and worked extensively towards equal rights for gays and lesbians, and towards legalizing same-sex marriage. He founded the David Bohnett Foundation, dedicated to community-building and social activism addressing concerns across a broad spectrum of arts, educational, and civic programs. The first openly bisexual US congressperson Krysten Sinema and the first openly gay mayor of a major US city (Houston), Annise Parker, are both alumnis of the LGBT Leadership Fellows run by his foundation that trains LGBT leaders for local and state governments.",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/c/cb/David_Bohnett.jpg",
+ "Born": "On April 2, 1956 at Chicago, Illinois",
+ "Awards": "• Number 16 on Time's Top 50 Cyber Elite (1998)\n• Upside magazine's Elite 100 (1998)\n• Newsweek's “100 People to Watch in the Next Millennium”\n• Ernst & Young Entrepreneur of the Year Award for Southern California (1999)\n• Los Angeles Gay and Lesbian Center's Rand Schrader Award (1999)\n• Los Angeles Business Journal's Technology Leader of the Year (2000)\n• ACLU Citizen Advocate Award (2002)\n• amfAR Award of Courage (2006)\n• Los Angeles City of Angels Award (2008)\n• GLSEN's Lifetime Achievement Award (2009)\n• Honorary doctorate of Humane Letters from Whittier College (2012)\n• American Jewish Committee Los Angeles' Ira E. Yellin Community Leadership Award (2014)\n• Brady Bear Award from the Brady Center to Prevent Gun Violence (2016)\n• Los Angeles Business Journal's LA 500: The Most Influential People in Los Angeles (2017)"
+ },
+ "Jennifer Pritzker": {
+ "Known for": "Founder and CEO of Tawani Enterprises",
+ "About": "**Jennifer Natalya Pritzker** popularly known as Jennifer Pritzker. A retired Lieutenant Colonel of the US Army, and Founder and CEO of private wealth management firm Tawani Enterprises, Jennifer Natalya Pritzker, is an American investor, philanthropist, member of the Pritzker family, and the world’s first transgender billionaire. Having retired from the US Army in 2001, Jennifer was promoted to the honorary rank of Colonel in the Illinois Army National Guard.\n\nFormerly known as James Nicholas Pritzker, she legally changed her official name to Jennifer Natalya Pritzker in 2013, identifying herself as a woman for all business and personal undertakings, as per an announcement shared with employees of the Pritzker Military Library and Tawani Enterprises.\n\nPritzker in 1995 founded the Tawani Foundation aiming “to enhance the awareness and understanding of the importance of the Citizen Soldier; to preserve unique sites of significance to American and military history; to foster health and wellness projects for improved quality of life; and to honor the service of military personnel, past, present and future.”",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/6/62/Jennifer_Pritzker.jpg/220px-Jennifer_Pritzker.jpg",
+ "Born": "On August 13, 1950 at Chicago, Illinois.",
+ "Awards": "• First transgender billionaire\n• Founder of Tawani Foundation and Pritzker Military Library"
+ },
+ "Claudia Brind-Woody": {
+ "Known for": "VP and managing director of intellectual property IBM",
+ "About": "**Claudia Lavergne Brind-Woody** popularly known as Claudia Brind-Woody. Global Co-Chair for the LGBT Executive Task-force at IBM, Claudia Brind-Woody is a force of nature to reckon with. In 2019, she was named among the most powerful LGBTQ+ people in tech, in addition to being in Financial Times Top 50 Outstanding list in 2013, 2014 and 2015, The Guardian’s 100 most influential LGBT people of the year in 2012, and winning the Out & Equal Trailblazer award in 2011, among other accolades.\n\nShe came out as a lesbian in the early years of her career and strives to work towards equality at the workplace. In an interview back in 2016 she shared, “What the LGBT+ community wants is to just want it to be ordinary [to be LGBT+] so that you are just seen to be valued on merit and what you bring to the business without someone thinking twice about you being LGBT+....When our employees don't have to think twice about struggling for the same benefits, recognition, or are afraid of being safe, then productivity goes up.”",
+ "url": "https://image.insider.com/580e9350dd089551098b47ff?width=750&format=jpeg&auto=webp",
+ "Born": "On January 30, 1955 at Virginia, U.S.",
+ "Awards": "• Out & Equal Trailblazer Award (2011)\n• GO Magazine's 100 Women We Love (2011)\n• The Guardian's World Pride Power List Top 100 (2012)\n• The Financial Times' Top 50 Outstanding list (2013, 2014, 2015)\n• The Daily Telegraph's Top 50 list of LGBT executives (2015)\n• The Financial Times' Hall of Fame (2016)\n• Diva power list (2016)\n• Business Insider The 23 Most Influential LGBTQ+ People in Tech"
+ },
+ "Laxmi Narayan Tripathi": {
+ "Known for": "Humans rights activist and founder, Astitva trust",
+ "About": "The first transgender individual to represent APAC in the UN task meeting in 2008, representative of APAC yet again at the 20th International AIDS Conference in Melbourne and recipient of the ‘Indian of the Year’ award in 2017, Lakshmi Narayan Tripathi is a transgender activist, and part of the team that led the charge to getting transgender recognized as a third gender in India by the Supreme Court in 2014.\n\nLakshmi was appointed as the President of the NGO DAI Welfare Society in 2002, the first registered and working organization for eunuchs in South Asia. By 2007 she founded her own organization, Astitiva that works towards the welfare, support and development of sexual minorities.\n\nWith the background of an abusive childhood and being bullied for being feminine, she stated in an interview, “I chose not to remember the prejudice,” adding, “Rather I think (about) the good things that have happened to me, and be a flamboyant rainbow.”",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bd/Laxmi_Narayan_Tripathi_at_JLF_Melbourne_presented_by_Melbourne_Writers_Festival%2C_Federation_Square%2C_Melbourne_2017.jpg/220px-Laxmi_Narayan_Tripathi_at_JLF_Melbourne_presented_by_Melbourne_Writers_Festival%2C_Federation_Square%2C_Melbourne_2017.jpg",
+ "Born": "On 13th Dec 1978 at Thane",
+ "Awards": "• Awarded 'Indian of the Year 2017"
+ },
+ "Tim Gill": {
+ "Known for": "Founder of Quark",
+ "About": "Tim Gill founded Quark Inc in 1981 and sold his stakes in Quark in 1999 in order to focus more on his interests in LGBT+ activism and philanthropy. He founded the pro-LGBT Gill Foundation in 1994, and since its inception it has invested more than $357 Mn in programs and non-profits around the country, substantially contributing towards many victories for LGBT community.\n\nGill married Scott Miller in 2009 and continues to be the largest donor for LGBT initiatives in America.\n\n“The LGBTQ movement has no Martin Luther King. We never have. And we probably never will,” Gill said. “So it’s not going to be grandiose gestures and big speeches and things like that that secure us equal opportunity. It will be the hard work of thousands and thousands of people over many, many years.”",
+ "url": "https://gillfoundation.org/wp-content/uploads/2014/09/tim-gill-20151.jpg",
+ "Born": "On October 18, 1953 at Hobart, Indiana",
+ "Awards": "• Gill was awarded the NOGLSTP GLBT Engineer of the Year Award in 2007."
+ }
+}
diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py
index 35ef0a7b..bef12d25 100644
--- a/bot/utils/__init__.py
+++ b/bot/utils/__init__.py
@@ -3,7 +3,7 @@ import contextlib
import re
import string
from datetime import datetime
-from typing import Iterable, List
+from typing import Iterable, List, Optional
import discord
from discord.ext.commands import BadArgument, Context
@@ -31,8 +31,13 @@ def resolve_current_month() -> Month:
async def disambiguate(
- ctx: Context, entries: List[str], *, timeout: float = 30,
- entries_per_page: int = 20, empty: bool = False, embed: discord.Embed = None
+ ctx: Context,
+ entries: List[str],
+ *,
+ timeout: float = 30,
+ entries_per_page: int = 20,
+ empty: bool = False,
+ embed: Optional[discord.Embed] = None
) -> str:
"""
Has the user choose between multiple entries in case one could not be chosen automatically.
@@ -43,25 +48,29 @@ async def disambiguate(
or if the user makes an invalid choice.
"""
if len(entries) == 0:
- raise BadArgument('No matches found.')
+ raise BadArgument("No matches found.")
if len(entries) == 1:
return entries[0]
- choices = (f'{index}: {entry}' for index, entry in enumerate(entries, start=1))
+ choices = (f"{index}: {entry}" for index, entry in enumerate(entries, start=1))
def check(message: discord.Message) -> bool:
- return (message.content.isdigit()
- and message.author == ctx.author
- and message.channel == ctx.channel)
+ return (
+ message.content.isdecimal()
+ and message.author == ctx.author
+ and message.channel == ctx.channel
+ )
try:
if embed is None:
embed = discord.Embed()
- coro1 = ctx.bot.wait_for('message', check=check, timeout=timeout)
- coro2 = LinePaginator.paginate(choices, ctx, embed=embed, max_lines=entries_per_page,
- empty=empty, max_size=6000, timeout=9000)
+ coro1 = ctx.bot.wait_for("message", check=check, timeout=timeout)
+ coro2 = LinePaginator.paginate(
+ choices, ctx, embed=embed, max_lines=entries_per_page,
+ empty=empty, max_size=6000, timeout=9000
+ )
# wait_for timeout will go to except instead of the wait_for thing as I expected
futures = [asyncio.ensure_future(coro1), asyncio.ensure_future(coro2)]
@@ -74,7 +83,7 @@ async def disambiguate(
if result is None:
for coro in pending:
coro.cancel()
- raise BadArgument('Canceled.')
+ raise BadArgument("Canceled.")
# Pagination was not initiated, only one page
if result.author == ctx.bot.user:
@@ -85,19 +94,19 @@ async def disambiguate(
for coro in pending:
coro.cancel()
except asyncio.TimeoutError:
- raise BadArgument('Timed out.')
+ raise BadArgument("Timed out.")
- # Guaranteed to not error because of isdigit() in check
+ # Guaranteed to not error because of isdecimal() in check
index = int(result.content)
try:
return entries[index - 1]
except IndexError:
- raise BadArgument('Invalid choice.')
+ raise BadArgument("Invalid choice.")
def replace_many(
- sentence: str, replacements: dict, *, ignore_case: bool = False, match_case: bool = False
+ sentence: str, replacements: dict, *, ignore_case: bool = False, match_case: bool = False
) -> str:
"""
Replaces multiple substrings in a string given a mapping of strings.
@@ -139,7 +148,7 @@ def replace_many(
return replacement
# Clean punctuation from word so string methods work
- cleaned_word = word.translate(str.maketrans('', '', string.punctuation))
+ cleaned_word = word.translate(str.maketrans("", "", string.punctuation))
if cleaned_word.isupper():
return replacement.upper()
elif cleaned_word[0].isupper():
diff --git a/bot/utils/checks.py b/bot/utils/checks.py
index 9dd4dde0..c06b6870 100644
--- a/bot/utils/checks.py
+++ b/bot/utils/checks.py
@@ -92,8 +92,10 @@ def in_whitelist_check(
def with_role_check(ctx: Context, *role_ids: int) -> bool:
"""Returns True if the user has any one of the roles in role_ids."""
if not ctx.guild: # Return False in a DM
- log.trace(f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. "
- "This command is restricted by the with_role decorator. Rejecting request.")
+ log.trace(
+ f"{ctx.author} tried to use the '{ctx.command.name}'command from a DM. "
+ "This command is restricted by the with_role decorator. Rejecting request."
+ )
return False
for role in ctx.author.roles:
@@ -101,22 +103,28 @@ def with_role_check(ctx: Context, *role_ids: int) -> bool:
log.trace(f"{ctx.author} has the '{role.name}' role, and passes the check.")
return True
- log.trace(f"{ctx.author} does not have the required role to use "
- f"the '{ctx.command.name}' command, so the request is rejected.")
+ log.trace(
+ f"{ctx.author} does not have the required role to use "
+ f"the '{ctx.command.name}' command, so the request is rejected."
+ )
return False
def without_role_check(ctx: Context, *role_ids: int) -> bool:
"""Returns True if the user does not have any of the roles in role_ids."""
if not ctx.guild: # Return False in a DM
- log.trace(f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. "
- "This command is restricted by the without_role decorator. Rejecting request.")
+ log.trace(
+ f"{ctx.author} tried to use the '{ctx.command.name}' command from a DM. "
+ "This command is restricted by the without_role decorator. Rejecting request."
+ )
return False
author_roles = [role.id for role in ctx.author.roles]
check = all(role not in author_roles for role in role_ids)
- log.trace(f"{ctx.author} tried to call the '{ctx.command.name}' command. "
- f"The result of the without_role check was {check}.")
+ log.trace(
+ f"{ctx.author} tried to call the '{ctx.command.name}' command. "
+ f"The result of the without_role check was {check}."
+ )
return check
@@ -154,8 +162,10 @@ def cooldown_with_role_bypass(rate: int, per: float, type: BucketType = BucketTy
#
# If the `before_invoke` detail is ever a problem then I can quickly just swap over.
if not isinstance(command, Command):
- raise TypeError('Decorator `cooldown_with_role_bypass` must be applied after the command decorator. '
- 'This means it has to be above the command decorator in the code.')
+ raise TypeError(
+ "Decorator `cooldown_with_role_bypass` must be applied after the command decorator. "
+ "This means it has to be above the command decorator in the code."
+ )
command._before_invoke = predicate
diff --git a/bot/utils/converters.py b/bot/utils/converters.py
index 27804170..fe2c980c 100644
--- a/bot/utils/converters.py
+++ b/bot/utils/converters.py
@@ -1,12 +1,14 @@
+from datetime import datetime
+from typing import Tuple, Union
+
import discord
-from discord.ext.commands import BadArgument, Context
-from discord.ext.commands.converter import Converter, MessageConverter
+from discord.ext import commands
-class WrappedMessageConverter(MessageConverter):
+class WrappedMessageConverter(commands.MessageConverter):
"""A converter that handles embed-suppressed links like <http://example.com>."""
- async def convert(self, ctx: discord.ext.commands.Context, argument: str) -> discord.Message:
+ async def convert(self, ctx: commands.Context, argument: str) -> discord.Message:
"""Wrap the commands.MessageConverter to handle <> delimited message links."""
# It's possible to wrap a message in [<>] as well, and it's supported because its easy
if argument.startswith("[") and argument.endswith("]"):
@@ -17,11 +19,78 @@ class WrappedMessageConverter(MessageConverter):
return await super().convert(ctx, argument)
-class Subreddit(Converter):
+class CoordinateConverter(commands.Converter):
+ """Converter for Coordinates."""
+
+ @staticmethod
+ async def convert(ctx: commands.Context, coordinate: str) -> Tuple[int, int]:
+ """Take in a coordinate string and turn it into an (x, y) tuple."""
+ if len(coordinate) not in (2, 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.isdecimal():
+ 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
+
+
+SourceType = Union[commands.Command, commands.Cog]
+
+
+class SourceConverter(commands.Converter):
+ """Convert an argument into a command or cog."""
+
+ @staticmethod
+ async def convert(ctx: commands.Context, argument: str) -> SourceType:
+ """Convert argument into source object."""
+ cog = ctx.bot.get_cog(argument)
+ if cog:
+ return cog
+
+ cmd = ctx.bot.get_command(argument)
+ if cmd:
+ return cmd
+
+ raise commands.BadArgument(
+ f"Unable to convert `{argument}` to valid command or Cog."
+ )
+
+
+class DateConverter(commands.Converter):
+ """Parse SOL or earth date (in format YYYY-MM-DD) into `int` or `datetime`. When invalid input, raise error."""
+
+ @staticmethod
+ async def convert(ctx: commands.Context, argument: str) -> Union[int, datetime]:
+ """Parse date (SOL or earth) into `datetime` or `int`. When invalid value, raise error."""
+ if argument.isdecimal():
+ return int(argument)
+ try:
+ date = datetime.strptime(argument, "%Y-%m-%d")
+ except ValueError:
+ raise commands.BadArgument(
+ f"Can't convert `{argument}` to `datetime` in format `YYYY-MM-DD` or `int` in SOL."
+ )
+ return date
+
+
+class Subreddit(commands.Converter):
"""Forces a string to begin with "r/" and checks if it's a valid subreddit."""
@staticmethod
- async def convert(ctx: Context, sub: str) -> str:
+ async def convert(ctx: commands.Context, sub: str) -> str:
"""
Force sub to begin with "r/" and check if it's a valid subreddit.
@@ -39,7 +108,7 @@ class Subreddit(Converter):
json = await resp.json()
if not json["data"]["children"]:
- raise BadArgument(
+ raise commands.BadArgument(
f"The subreddit `{sub}` either doesn't exist, or it has no posts."
)
diff --git a/bot/utils/decorators.py b/bot/utils/decorators.py
index 60066dc4..c0783144 100644
--- a/bot/utils/decorators.py
+++ b/bot/utils/decorators.py
@@ -269,7 +269,7 @@ def whitelist_check(**default_kwargs: t.Container[int]) -> t.Callable[[Context],
channels.update(channel.id for channel in category.text_channels)
if channels:
- channels_str = ', '.join(f"<#{c_id}>" for c_id in channels)
+ channels_str = ", ".join(f"<#{c_id}>" for c_id in channels)
message = f"Sorry, but you may only use this command within {channels_str}."
else:
message = "Sorry, but you may not use this command."
diff --git a/bot/utils/extensions.py b/bot/utils/extensions.py
index 459588a1..cd491c4b 100644
--- a/bot/utils/extensions.py
+++ b/bot/utils/extensions.py
@@ -35,8 +35,8 @@ def walk_extensions() -> Iterator[str]:
async def invoke_help_command(ctx: Context) -> None:
"""Invoke the help command or default help command if help extensions is not loaded."""
- if 'bot.exts.evergreen.help' in ctx.bot.extensions:
- help_command = ctx.bot.get_command('help')
+ if "bot.exts.evergreen.help" in ctx.bot.extensions:
+ help_command = ctx.bot.get_command("help")
await ctx.invoke(help_command, ctx.command.qualified_name)
return
await ctx.send_help(ctx.command)
diff --git a/bot/utils/halloween/spookifications.py b/bot/utils/halloween/spookifications.py
index 11f69850..f69dd6fd 100644
--- a/bot/utils/halloween/spookifications.py
+++ b/bot/utils/halloween/spookifications.py
@@ -13,16 +13,16 @@ def inversion(im: Image) -> Image:
Returns an inverted image when supplied with an Image object.
"""
- im = im.convert('RGB')
+ im = im.convert("RGB")
inv = ImageOps.invert(im)
return inv
def pentagram(im: Image) -> Image:
"""Adds pentagram to the image."""
- im = im.convert('RGB')
+ im = im.convert("RGB")
wt, ht = im.size
- penta = Image.open('bot/resources/halloween/bloody-pentagram.png')
+ penta = Image.open("bot/resources/halloween/bloody-pentagram.png")
penta = penta.resize((wt, ht))
im.paste(penta, (0, 0), penta)
return im
@@ -35,9 +35,9 @@ def bat(im: Image) -> Image:
The bat silhoutte is of a size at least one-fifths that of the original image and may be rotated
up to 90 degrees anti-clockwise.
"""
- im = im.convert('RGB')
+ im = im.convert("RGB")
wt, ht = im.size
- bat = Image.open('bot/resources/halloween/bat-clipart.png')
+ bat = Image.open("bot/resources/halloween/bat-clipart.png")
bat_size = randint(wt//10, wt//7)
rot = randint(0, 90)
bat = bat.resize((bat_size, bat_size))
diff --git a/bot/utils/pagination.py b/bot/utils/pagination.py
index 917275c0..d9c0862a 100644
--- a/bot/utils/pagination.py
+++ b/bot/utils/pagination.py
@@ -27,7 +27,14 @@ class EmptyPaginatorEmbed(Exception):
class LinePaginator(Paginator):
"""A class that aids in paginating code blocks for Discord messages."""
- def __init__(self, prefix: str = '```', suffix: str = '```', max_size: int = 2000, max_lines: int = None):
+ def __init__(
+ self,
+ prefix: str = '```',
+ suffix: str = '```',
+ max_size: int = 2000,
+ max_lines: Optional[int] = None,
+ linesep: str = "\n"
+ ):
"""
Overrides the Paginator.__init__ from inside discord.ext.commands.
@@ -36,16 +43,20 @@ class LinePaginator(Paginator):
`max_size` and `max_lines` denote the maximum amount of codepoints and lines
allowed per page.
"""
- self.prefix = prefix
- self.suffix = suffix
- self.max_size = max_size - len(suffix)
+ super().__init__(
+ prefix,
+ suffix,
+ max_size - len(suffix),
+ linesep
+ )
+
self.max_lines = max_lines
self._current_page = [prefix]
self._linecount = 0
self._count = len(prefix) + 1 # prefix + newline
self._pages = []
- def add_line(self, line: str = '', *, empty: bool = False) -> None:
+ def add_line(self, line: str = "", *, empty: bool = False) -> None:
"""
Adds a line to the current page.
@@ -57,7 +68,7 @@ class LinePaginator(Paginator):
If `empty` is True, an empty line will be placed after the a given `line`.
"""
if len(line) > self.max_size - len(self.prefix) - 2:
- raise RuntimeError('Line exceeds maximum page size %s' % (self.max_size - len(self.prefix) - 2))
+ raise RuntimeError("Line exceeds maximum page size %s" % (self.max_size - len(self.prefix) - 2))
if self.max_lines is not None:
if self._linecount >= self.max_lines:
@@ -72,7 +83,7 @@ class LinePaginator(Paginator):
self._current_page.append(line)
if empty:
- self._current_page.append('')
+ self._current_page.append("")
self._count += 1
@classmethod
@@ -80,7 +91,7 @@ class LinePaginator(Paginator):
prefix: str = "", suffix: str = "", max_lines: Optional[int] = None,
max_size: int = 500, empty: bool = True, restrict_to_user: User = None,
timeout: int = 300, footer_text: str = None, url: str = None,
- exception_on_empty_embed: bool = False):
+ exception_on_empty_embed: bool = False) -> None:
"""
Use a paginator and set of reactions to provide pagination over a set of lines.
@@ -158,7 +169,8 @@ class LinePaginator(Paginator):
log.trace(f"Setting embed url to '{url}'")
log.debug("There's less than two pages, so we won't paginate - sending single page on its own")
- return await ctx.send(embed=embed)
+ await ctx.send(embed=embed)
+ return
else:
if footer_text:
embed.set_footer(text=f"{footer_text} (Page {current_page + 1}/{len(paginator.pages)})")
@@ -283,7 +295,7 @@ class ImagePaginator(Paginator):
self.images = []
self._pages = []
- def add_line(self, line: str = '', *, empty: bool = False) -> None:
+ def add_line(self, line: str = "", *, empty: bool = False) -> None:
"""
Adds a line to each page, usually just 1 line in this context.
@@ -303,7 +315,7 @@ class ImagePaginator(Paginator):
@classmethod
async def paginate(cls, pages: List[Tuple[str, str]], ctx: Context, embed: Embed,
prefix: str = "", suffix: str = "", timeout: int = 300,
- exception_on_empty_embed: bool = False):
+ exception_on_empty_embed: bool = False) -> None:
"""
Use a paginator and set of reactions to provide pagination over a set of title/image pairs.
@@ -353,7 +365,8 @@ class ImagePaginator(Paginator):
embed.set_image(url=image)
if len(paginator.pages) <= 1:
- return await ctx.send(embed=embed)
+ await ctx.send(embed=embed)
+ return
embed.set_footer(text=f"Page {current_page + 1}/{len(paginator.pages)}")
message = await ctx.send(embed=embed)