diff options
Diffstat (limited to 'bot/exts')
-rw-r--r-- | bot/exts/core/extensions.py | 2 | ||||
-rw-r--r-- | bot/exts/core/internal_eval/_helpers.py | 2 | ||||
-rw-r--r-- | bot/exts/events/hacktoberfest/hacktoberstats.py | 4 | ||||
-rw-r--r-- | bot/exts/events/trivianight/_game.py | 4 | ||||
-rw-r--r-- | bot/exts/events/trivianight/_scoreboard.py | 6 | ||||
-rw-r--r-- | bot/exts/events/trivianight/trivianight.py | 2 | ||||
-rw-r--r-- | bot/exts/fun/snakes/_snakes_cog.py | 6 | ||||
-rw-r--r-- | bot/exts/fun/trivia_quiz.py | 254 | ||||
-rw-r--r-- | bot/exts/holidays/halloween/monstersurvey.py | 6 | ||||
-rw-r--r-- | bot/exts/holidays/pride/pride_anthem.py | 2 | ||||
-rw-r--r-- | bot/exts/holidays/pride/pride_facts.py | 67 | ||||
-rw-r--r-- | bot/exts/holidays/pride/pride_leader.py | 2 | ||||
-rw-r--r-- | bot/exts/holidays/valentines/be_my_valentine.py | 4 | ||||
-rw-r--r-- | bot/exts/holidays/valentines/myvalenstate.py | 2 | ||||
-rw-r--r-- | bot/exts/utilities/bookmark.py | 144 | ||||
-rw-r--r-- | bot/exts/utilities/challenges.py | 2 | ||||
-rw-r--r-- | bot/exts/utilities/colour.py | 2 | ||||
-rw-r--r-- | bot/exts/utilities/emoji.py | 2 | ||||
-rw-r--r-- | bot/exts/utilities/reddit.py | 33 | ||||
-rw-r--r-- | bot/exts/utilities/wolfram.py | 4 |
20 files changed, 303 insertions, 247 deletions
diff --git a/bot/exts/core/extensions.py b/bot/exts/core/extensions.py index 87ef2ed1..f96b0292 100644 --- a/bot/exts/core/extensions.py +++ b/bot/exts/core/extensions.py @@ -153,7 +153,7 @@ class Extensions(commands.Cog): embed = Embed(colour=Colour.og_blurple()) embed.set_author( name="Extensions List", - url=Client.github_bot_repo, + url=Client.github_repo, icon_url=str(self.bot.user.display_avatar.url) ) diff --git a/bot/exts/core/internal_eval/_helpers.py b/bot/exts/core/internal_eval/_helpers.py index 34ef7fef..30c4b11c 100644 --- a/bot/exts/core/internal_eval/_helpers.py +++ b/bot/exts/core/internal_eval/_helpers.py @@ -227,7 +227,7 @@ class CaptureLastExpression(ast.NodeTransformer): log.trace("Found a trailing last expression in the evaluation code") log.trace("Creating assignment statement with trailing expression as the right-hand side") - right_hand_side = list(ast.iter_child_nodes(node))[0] + right_hand_side = next(iter(ast.iter_child_nodes(node))) assignment = ast.Assign( targets=[ast.Name(id="_value_last_expression", ctx=ast.Store())], diff --git a/bot/exts/events/hacktoberfest/hacktoberstats.py b/bot/exts/events/hacktoberfest/hacktoberstats.py index c7fd3601..04dc4aae 100644 --- a/bot/exts/events/hacktoberfest/hacktoberstats.py +++ b/bot/exts/events/hacktoberfest/hacktoberstats.py @@ -43,7 +43,7 @@ class HacktoberStats(commands.Cog): @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER) @commands.group(name="hacktoberstats", aliases=("hackstats",), invoke_without_command=True) - async def hacktoberstats_group(self, ctx: commands.Context, github_username: str = None) -> None: + async def hacktoberstats_group(self, ctx: commands.Context, github_username: str | None = None) -> None: """ Display an embed for a user's Hacktoberfest contributions. @@ -70,7 +70,7 @@ class HacktoberStats(commands.Cog): @in_month(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER) @hacktoberstats_group.command(name="link") - async def link_user(self, ctx: commands.Context, github_username: str = None) -> None: + async def link_user(self, ctx: commands.Context, github_username: str | None = None) -> None: """ Link the invoking user's Github github_username to their Discord ID. diff --git a/bot/exts/events/trivianight/_game.py b/bot/exts/events/trivianight/_game.py index 15126f60..0568ce81 100644 --- a/bot/exts/events/trivianight/_game.py +++ b/bot/exts/events/trivianight/_game.py @@ -134,7 +134,7 @@ class TriviaNightGame: def __iter__(self) -> Iterable[Question]: return iter(self._questions) - def next_question(self, number: str = None) -> Question: + def next_question(self, number: str | None = None) -> Question: """ Consume one random question from the trivia night game. @@ -145,7 +145,7 @@ class TriviaNightGame: if number is not None: try: - question = [q for q in self._all_questions if q.number == int(number)][0] + question = next(q for q in self._all_questions if q.number == int(number)) except IndexError: raise ValueError(f"Question number {number} does not exist.") elif len(self._questions) == 0: diff --git a/bot/exts/events/trivianight/_scoreboard.py b/bot/exts/events/trivianight/_scoreboard.py index bd61be3d..c2b39d67 100644 --- a/bot/exts/events/trivianight/_scoreboard.py +++ b/bot/exts/events/trivianight/_scoreboard.py @@ -155,18 +155,18 @@ class Scoreboard: self._points = {} self._speed = {} - def assign_points(self, user_id: int, *, points: int = None, speed: float = None) -> None: + def assign_points(self, user_id: int, *, points: int | None = None, speed: float | None = None) -> None: """ Assign points or deduct points to/from a certain user. This method should be called once the question has finished and all answers have been registered. """ - if points is not None and user_id not in self._points.keys(): + if points is not None and user_id not in self._points: self._points[user_id] = points elif points is not None: self._points[user_id] += points - if speed is not None and user_id not in self._speed.keys(): + if speed is not None and user_id not in self._speed: self._speed[user_id] = [1, speed] elif speed is not None: self._speed[user_id] = [ diff --git a/bot/exts/events/trivianight/trivianight.py b/bot/exts/events/trivianight/trivianight.py index e0db45d8..e47d1d83 100644 --- a/bot/exts/events/trivianight/trivianight.py +++ b/bot/exts/events/trivianight/trivianight.py @@ -107,7 +107,7 @@ class TriviaNightCog(commands.Cog): @trivianight.command(aliases=("next",)) @commands.has_any_role(*TRIVIA_NIGHT_ROLES) - async def question(self, ctx: commands.Context, question_number: str = None) -> None: + async def question(self, ctx: commands.Context, question_number: str | None = None) -> None: """ Gets a random question from the unanswered question list and lets the user(s) choose the answer. diff --git a/bot/exts/fun/snakes/_snakes_cog.py b/bot/exts/fun/snakes/_snakes_cog.py index f1a1cab9..82496629 100644 --- a/bot/exts/fun/snakes/_snakes_cog.py +++ b/bot/exts/fun/snakes/_snakes_cog.py @@ -848,7 +848,7 @@ class Snakes(Cog): await self._validate_answer(ctx, quiz, answer, options) @snakes_group.command(name="name", aliases=("name_gen",)) - async def name_command(self, ctx: Context, *, name: str = None) -> None: + async def name_command(self, ctx: Context, *, name: str | None = None) -> None: """ Snakifies a username. @@ -1041,7 +1041,7 @@ class Snakes(Cog): await ctx.send(embed=embed) @snakes_group.command(name="snakify") - async def snakify_command(self, ctx: Context, *, message: str = None) -> None: + async def snakify_command(self, ctx: Context, *, message: str | None = None) -> None: """ How would I talk if I were a snake? @@ -1076,7 +1076,7 @@ class Snakes(Cog): await ctx.send(embed=embed) @snakes_group.command(name="video", aliases=("get_video",)) - async def video_command(self, ctx: Context, *, search: str = None) -> None: + async def video_command(self, ctx: Context, *, search: str | None = None) -> None: """ Gets a YouTube video about snakes. diff --git a/bot/exts/fun/trivia_quiz.py b/bot/exts/fun/trivia_quiz.py index 28cd4657..b96007b5 100644 --- a/bot/exts/fun/trivia_quiz.py +++ b/bot/exts/fun/trivia_quiz.py @@ -40,6 +40,41 @@ WIKI_FEED_API_URL = "https://en.wikipedia.org/api/rest_v1/feed/featured/{date}" TRIVIA_QUIZ_ICON = ( "https://raw.githubusercontent.com/python-discord/branding/main/icons/trivia_quiz/trivia-quiz-dist.png" ) +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) @@ -51,163 +86,117 @@ class QuizEntry: var_tol: int -class DynamicQuestionGen: - """Class that contains functions to generate math/science questions for TriviaQuiz Cog.""" - - 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"), - } - - @classmethod - def linear_system(cls, 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, - ) +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) - return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) + coeffs = random.sample(range(1, 6), 4) - @classmethod - def mod_arith(cls, 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( + coeffs[0], + coeffs[1], + coeffs[0] * x + coeffs[1] * y, + coeffs[2], + coeffs[3], + coeffs[2] * x + coeffs[3] * y, + ) - question = q_format.format(a, b, m) - answer = a_format.format(ans) + return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) - return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) +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 - @classmethod - def ngonal_prism(cls, q_format: str, a_format: str) -> QuizEntry: - """Generate a question regarding vertices on n-gonal prisms.""" - n = random.randint(0, len(cls.N_PREFIXES) - 1) + question = q_format.format(a, b, m) + answer = a_format.format(ans) - question = q_format.format(cls.N_PREFIXES[n]) - answer = a_format.format((n + cls.N_PREFIX_STARTS_AT) * 2) + return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) - return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) +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) - @classmethod - def imag_sqrt(cls, q_format: str, a_format: str) -> QuizEntry: - """Generate a negative square root question.""" - ans_coeff = random.randint(3, 10) + question = q_format.format(N_PREFIXES[n]) + answer = a_format.format((n + N_PREFIX_STARTS_AT) * 2) - question = q_format.format(ans_coeff ** 2) - answer = a_format.format(ans_coeff) + return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) - return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) +def imag_sqrt(q_format: str, a_format: str) -> QuizEntry: + """Generate a negative square root question.""" + ans_coeff = random.randint(3, 10) - @classmethod - def binary_calc(cls, 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), - ) + question = q_format.format(ans_coeff ** 2) + answer = a_format.format(ans_coeff) + + return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) + +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 + # 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)) + question = q_format.format(a, oper[0], b) + answer = a_format.format(oper[1](a, b)) - return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) + return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) - @classmethod - def solar_system(cls, q_format: str, a_format: str) -> QuizEntry: - """Generate a question on the planets of the Solar System.""" - planet = random.choice(cls.PLANETS) +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]) + question = q_format.format(planet[0]) + answer = a_format.format(planet[1]) - return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) + return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) - @classmethod - def taxonomic_rank(cls, q_format: str, a_format: str) -> QuizEntry: - """Generate a question on taxonomic classification.""" - level = random.randint(0, len(cls.TAXONOMIC_HIERARCHY) - 2) +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(cls.TAXONOMIC_HIERARCHY[level]) - answer = a_format.format(cls.TAXONOMIC_HIERARCHY[level + 1]) + question = q_format.format(TAXONOMIC_HIERARCHY[level]) + answer = a_format.format(TAXONOMIC_HIERARCHY[level + 1]) - return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) + return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) - @classmethod - def base_units_convert(cls, q_format: str, a_format: str) -> QuizEntry: - """Generate a SI base units conversion question.""" - unit = random.choice(list(cls.UNITS_TO_BASE_UNITS)) - question = q_format.format( - unit + " " + cls.UNITS_TO_BASE_UNITS[unit][0] - ) - answer = a_format.format( - cls.UNITS_TO_BASE_UNITS[unit][1] - ) +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], DYNAMICALLY_GEN_VARIATION_TOLERANCE) + return QuizEntry(question, [answer], DYNAMICALLY_GEN_VARIATION_TOLERANCE) DYNAMIC_QUESTIONS_FORMAT_FUNCS = { - 201: DynamicQuestionGen.linear_system, - 202: DynamicQuestionGen.mod_arith, - 203: DynamicQuestionGen.ngonal_prism, - 204: DynamicQuestionGen.imag_sqrt, - 205: DynamicQuestionGen.binary_calc, - 301: DynamicQuestionGen.solar_system, - 302: DynamicQuestionGen.taxonomic_rank, - 303: DynamicQuestionGen.base_units_convert, + 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, } @@ -361,7 +350,7 @@ class TriviaQuiz(commands.Cog): ) return - elif questions < 1: + if questions < 1: await ctx.send( embed=self.make_error_embed( "You must choose to complete at least one question. " @@ -370,8 +359,7 @@ class TriviaQuiz(commands.Cog): ) return - else: - self.question_limit = questions + self.question_limit = questions # Start game if not running. if not self.game_status[ctx.channel.id]: diff --git a/bot/exts/holidays/halloween/monstersurvey.py b/bot/exts/holidays/halloween/monstersurvey.py index d129f3cc..fdc15e13 100644 --- a/bot/exts/holidays/halloween/monstersurvey.py +++ b/bot/exts/holidays/halloween/monstersurvey.py @@ -90,7 +90,7 @@ class MonsterSurvey(Cog): @monster_group.command( name="vote" ) - async def monster_vote(self, ctx: Context, name: str = None) -> None: + async def monster_vote(self, ctx: Context, name: str | None = None) -> None: """ Cast a vote for a particular monster. @@ -109,7 +109,7 @@ class MonsterSurvey(Cog): name = name.lower() vote_embed = Embed( - name="Monster Voting", + title="Monster Voting", color=0xFF6800 ) @@ -141,7 +141,7 @@ class MonsterSurvey(Cog): @monster_group.command( name="show" ) - async def monster_show(self, ctx: Context, name: str = None) -> None: + async def monster_show(self, ctx: Context, name: str | None = None) -> None: """Shows the named monster. If one is not named, it sends the default voting embed instead.""" if name is None: await ctx.invoke(self.monster_leaderboard) diff --git a/bot/exts/holidays/pride/pride_anthem.py b/bot/exts/holidays/pride/pride_anthem.py index c719e388..1f214fc0 100644 --- a/bot/exts/holidays/pride/pride_anthem.py +++ b/bot/exts/holidays/pride/pride_anthem.py @@ -32,7 +32,7 @@ class PrideAnthem(commands.Cog): log.info("No videos for that genre.") @commands.command(name="prideanthem", aliases=("anthem", "pridesong")) - async def prideanthem(self, ctx: commands.Context, genre: str = None) -> None: + async def prideanthem(self, ctx: commands.Context, genre: str | None = None) -> None: """ Sends a message with a video of a random pride anthem. diff --git a/bot/exts/holidays/pride/pride_facts.py b/bot/exts/holidays/pride/pride_facts.py index f3ce50a9..5be1b085 100644 --- a/bot/exts/holidays/pride/pride_facts.py +++ b/bot/exts/holidays/pride/pride_facts.py @@ -4,7 +4,6 @@ import random from datetime import UTC, datetime from pathlib import Path -import dateutil.parser import discord from discord.ext import commands @@ -28,65 +27,45 @@ class PrideFacts(commands.Cog): async def send_pride_fact_daily(self) -> None: """Background task to post the daily pride fact every day.""" channel = self.bot.get_channel(Channels.sir_lancebot_playground) - await self.send_select_fact(channel, datetime.now(tz=UTC)) + await self.send_select_fact(channel, datetime.now(tz=UTC).day) - async def send_random_fact(self, ctx: commands.Context) -> None: - """Provides a fact from any previous day, or today.""" - now = datetime.now(tz=UTC) - previous_years_facts = (y for x, y in FACTS.items() if int(x) < now.year) - current_year_facts = FACTS.get(str(now.year), [])[:now.day] - previous_facts = current_year_facts + [x for y in previous_years_facts for x in y] + async def send_select_fact(self, target: discord.abc.Messageable, day_num: int) -> None: + """Provides the fact for the specified day.""" try: - await ctx.send(embed=self.make_embed(random.choice(previous_facts))) + await target.send(embed=self.get_fact_embed(day_num)) except IndexError: - await ctx.send("No facts available") - - async def send_select_fact(self, target: discord.abc.Messageable, _date: str | datetime) -> None: - """Provides the fact for the specified day, if the day is today, or is in the past.""" - now = datetime.now(tz=UTC) - if isinstance(_date, str): - try: - date = dateutil.parser.parse(_date, dayfirst=False, yearfirst=False, fuzzy=True) - except (ValueError, OverflowError) as err: - await target.send(f"Error parsing date: {err}") - return - else: - 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(FACTS[str(date.year)][date.day - 1])) - except KeyError: - await target.send(f"The year {date.year} is not yet supported") - return - except IndexError: - await target.send(f"Day {date.day} of {date.year} is not yet support") - return - else: - await target.send("The fact for the selected day is not yet available.") + await target.send(f"Day {day_num} is not supported") + return @commands.command(name="pridefact", aliases=("pridefacts",)) - async def pridefact(self, ctx: commands.Context, option: str = None) -> None: + async def pridefact(self, ctx: commands.Context, option: int | str | None = None) -> None: """ Sends a message with a pride fact of the day. - If "random" is given as an argument, a random previous fact will be provided. - - If a date is given as an argument, and the date is in the past, the fact from that day - will be provided. + "option" is an optional setting, which has two has two accepted values: + - "random": a random previous fact will be provided. + - If a option is a number (1-30), the fact for that given day of June is returned. """ if not option: - await self.send_select_fact(ctx, datetime.now(tz=UTC)) + await self.send_select_fact(ctx, datetime.now(tz=UTC).day) + elif isinstance(option, int): + await self.send_select_fact(ctx, option) elif option.lower().startswith("rand"): - await self.send_random_fact(ctx) + await ctx.send(embed=self.get_fact_embed()) else: - await self.send_select_fact(ctx, option) + await ctx.send(f"Could not parse option {option}") @staticmethod - def make_embed(fact: str) -> discord.Embed: - """Makes a nice embed for the fact to be sent.""" + def get_fact_embed(day_num: int | None=None) -> discord.Embed: + """ + Makes a embed for the fact on the given day_num to be sent. + + if day_num is not set, a random fact is selected. + """ + fact = FACTS[day_num-1] if day_num else random.choice(FACTS) return discord.Embed( colour=Colours.pink, - title="Pride Fact!", + title=f"Day {day_num}'s pride fact!" if day_num else "Random pride fact!", description=fact ) diff --git a/bot/exts/holidays/pride/pride_leader.py b/bot/exts/holidays/pride/pride_leader.py index b4a98892..9f01ebf9 100644 --- a/bot/exts/holidays/pride/pride_leader.py +++ b/bot/exts/holidays/pride/pride_leader.py @@ -57,7 +57,7 @@ class PrideLeader(commands.Cog): 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] + name = next(name for name, info in PRIDE_RESOURCE.items() if info == pride_leader) embed = discord.Embed( title=name, diff --git a/bot/exts/holidays/valentines/be_my_valentine.py b/bot/exts/holidays/valentines/be_my_valentine.py index c2dd8bb6..714d3884 100644 --- a/bot/exts/holidays/valentines/be_my_valentine.py +++ b/bot/exts/holidays/valentines/be_my_valentine.py @@ -44,7 +44,7 @@ class BeMyValentine(commands.Cog): @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 + self, ctx: commands.Context, user: discord.Member, *, valentine_type: str | None = None ) -> None: """ Send a valentine to a specified user with the lovefest role. @@ -83,7 +83,7 @@ class BeMyValentine(commands.Cog): @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 + self, ctx: commands.Context, user: discord.Member, *, valentine_type: str | None = None ) -> None: """ Send an anonymous Valentine via DM to to a specified user with the lovefest role. diff --git a/bot/exts/holidays/valentines/myvalenstate.py b/bot/exts/holidays/valentines/myvalenstate.py index fcef24bc..977b9665 100644 --- a/bot/exts/holidays/valentines/myvalenstate.py +++ b/bot/exts/holidays/valentines/myvalenstate.py @@ -39,7 +39,7 @@ class MyValenstate(commands.Cog): return pre_row[-1] @commands.command() - async def myvalenstate(self, ctx: commands.Context, *, name: str = None) -> None: + async def myvalenstate(self, ctx: commands.Context, *, name: str | None = None) -> None: """Find the vacation spot(s) with the most matching characters to the invoking user.""" eq_chars = collections.defaultdict(int) if name is None: diff --git a/bot/exts/utilities/bookmark.py b/bot/exts/utilities/bookmark.py index 150dfc48..22801d14 100644 --- a/bot/exts/utilities/bookmark.py +++ b/bot/exts/utilities/bookmark.py @@ -6,10 +6,76 @@ from discord.ext import commands from bot.bot import Bot from bot.constants import Colours, ERROR_REPLIES, Icons, Roles +from bot.utils.converters import WrappedMessageConverter from bot.utils.decorators import whitelist_override log = logging.getLogger(__name__) +MESSAGE_NOT_FOUND_ERROR = ( + "You must either provide a reference to a valid message, or reply to one." + "\n\nThe lookup strategy for a message is as follows (in order):" + "\n1. Lookup by '{channel ID}-{message ID}' (retrieved by shift-clicking on 'Copy ID')" + "\n2. Lookup by message ID (the message **must** be in the current channel)" + "\n3. Lookup by message URL" +) + + +async def dm_bookmark( + target_user: discord.Member | discord.User, + target_message: discord.Message, + title: str, +) -> None: + """ + Sends the `target_message` as a bookmark to the `target_user` DMs, with `title` as the embed title. + + Raises `discord.Forbidden` if the user's DMs are closed. + """ + embed = Bookmark.build_bookmark_dm(target_message, title) + message_url_view = discord.ui.View().add_item( + discord.ui.Button(label="View Message", url=target_message.jump_url) + ) + await target_user.send(embed=embed, view=message_url_view) + log.info(f"{target_user} bookmarked {target_message.jump_url} with title {title!r}") + + +class SendBookmark(discord.ui.View): + """The button that sends a bookmark to other users.""" + + def __init__( + self, + author: discord.Member, + channel: discord.TextChannel, + target_message: discord.Message, + title: str, + ): + super().__init__() + + self.clicked = [] + self.channel = channel + self.target_message = target_message + self.title = title + + @discord.ui.button(label="Receive Bookmark", style=discord.ButtonStyle.green) + async def button_callback(self, interaction: discord.Interaction, button: discord.ui.Button) -> None: + """The button callback.""" + if interaction.user.id in self.clicked: + await interaction.response.send_message( + "You have already received a bookmark to that message.", + ephemeral=True, + ) + return + + try: + await dm_bookmark(interaction.user, self.target_message, self.title) + except discord.Forbidden: + await interaction.response.send_message( + embed=Bookmark.build_error_embed("Enable your DMs to receive the bookmark."), + ephemeral=True, + ) + else: + self.clicked.append(interaction.user.id) + await interaction.response.send_message("You have received a bookmark to that message.", ephemeral=True) + class BookmarkForm(discord.ui.Modal): """The form where a user can fill in a custom title for their bookmark & submit it.""" @@ -31,7 +97,7 @@ class BookmarkForm(discord.ui.Modal): """Sends the bookmark embed to the user with the newly chosen title.""" title = self.bookmark_title.value or self.bookmark_title.default try: - await self.dm_bookmark(interaction, self.message, title) + await dm_bookmark(interaction.user, self.message, title) except discord.Forbidden: await interaction.response.send_message( embed=Bookmark.build_error_embed("Enable your DMs to receive the bookmark."), @@ -44,24 +110,6 @@ class BookmarkForm(discord.ui.Modal): ephemeral=True, ) - async def dm_bookmark( - self, - interaction: discord.Interaction, - target_message: discord.Message, - title: str, - ) -> None: - """ - Sends the target_message as a bookmark to the interaction user's DMs. - - Raises ``discord.Forbidden`` if the user's DMs are closed. - """ - embed = Bookmark.build_bookmark_dm(target_message, title) - message_url_view = discord.ui.View().add_item( - discord.ui.Button(label="View Message", url=target_message.jump_url) - ) - await interaction.user.send(embed=embed, view=message_url_view) - log.info(f"{interaction.user} bookmarked {target_message.jump_url} with title {title!r}") - class Bookmark(commands.Cog): """Creates personal bookmarks by relaying a message link to the user's DMs.""" @@ -104,6 +152,17 @@ class Bookmark(commands.Cog): colour=Colours.soft_red, ) + @staticmethod + def build_bookmark_embed(target_message: discord.Message) -> discord.Embed: + """Build the channel embed to the bookmark requester.""" + return discord.Embed( + description=( + f"Click the button to be sent your very own bookmark to " + f"[this message]({target_message.jump_url})." + ), + colour=Colours.soft_green, + ) + async def _bookmark_context_menu_callback(self, interaction: discord.Interaction, message: discord.Message) -> None: """The callback that will be invoked upon using the bookmark's context menu command.""" permissions = interaction.channel.permissions_for(interaction.user) @@ -122,15 +181,46 @@ class Bookmark(commands.Cog): @commands.guild_only() @whitelist_override(roles=(Roles.everyone,)) @commands.cooldown(1, 30, commands.BucketType.channel) - async def bookmark(self, ctx: commands.Context) -> None: - """Teach the invoker how to use the new context-menu based command for a smooth migration.""" - await ctx.send( - embed=self.build_error_embed( - "The bookmark text command has been replaced with a context menu command!\n\n" - "To bookmark a message simply right-click (press and hold on mobile) " - "on a message, open the 'Apps' menu, and click 'Bookmark'." + async def bookmark( + self, + ctx: commands.Context, + target_message: WrappedMessageConverter | None, + *, + title: str = "Bookmark", + ) -> None: + """ + Send the author a link to the specified message via DMs. + + Members can either give a message as an argument, or reply to a message. + + Bookmarks can subsequently be deleted by using the `bookmark delete` command in DMs. + """ + target_message: discord.Message | None = target_message or getattr(ctx.message.reference, "resolved", None) + if target_message is None: + raise commands.UserInputError(MESSAGE_NOT_FOUND_ERROR) + + permissions = target_message.channel.permissions_for(ctx.author) + if not permissions.read_messages: + log.info(f"{ctx.author} tried to bookmark a message in #{target_message.channel} but has no permissions.") + embed = self.build_error_embed("You don't have permission to view this channel.") + await ctx.send(embed=embed) + return + + view = SendBookmark(ctx.author, ctx.channel, target_message, title) + try: + await dm_bookmark(ctx.author, target_message, title) + except discord.Forbidden: + error_embed = self.build_error_embed( + f"{ctx.author.mention}, please enable your DMs to receive the bookmark." ) - ) + await ctx.send(embed=error_embed) + else: + view.clicked.append(ctx.author.id) + log.info(f"{ctx.author.mention} bookmarked {target_message.jump_url} with title '{title}'") + + embed = self.build_bookmark_embed(target_message) + + await ctx.send(embed=embed, view=view, delete_after=180) @bookmark.command(name="delete", aliases=("del", "rm"), root_aliases=("unbm", "unbookmark", "dmdelete", "dmdel")) @whitelist_override(bypass_defaults=True, allow_dm=True) diff --git a/bot/exts/utilities/challenges.py b/bot/exts/utilities/challenges.py index 2f9ac73e..6d1813bb 100644 --- a/bot/exts/utilities/challenges.py +++ b/bot/exts/utilities/challenges.py @@ -258,7 +258,7 @@ class Challenges(commands.Cog): @commands.command(aliases=["kata"]) @commands.cooldown(1, 5, commands.BucketType.user) - async def challenge(self, ctx: commands.Context, language: str = "python", *, query: str = None) -> None: + async def challenge(self, ctx: commands.Context, language: str = "python", *, query: str | None = None) -> None: """ The challenge command pulls a random kata (challenge) from codewars.com. diff --git a/bot/exts/utilities/colour.py b/bot/exts/utilities/colour.py index 95f9ac22..b0ff8747 100644 --- a/bot/exts/utilities/colour.py +++ b/bot/exts/utilities/colour.py @@ -241,7 +241,7 @@ class Colour(commands.Cog): choices=self.colour_mapping.values(), score_cutoff=80 ) - colour_name = [name for name, hex_code in self.colour_mapping.items() if hex_code == match][0] + colour_name = next(name for name, hex_code in self.colour_mapping.items() if hex_code == match) except TypeError: colour_name = None return colour_name diff --git a/bot/exts/utilities/emoji.py b/bot/exts/utilities/emoji.py index ce352fe2..bbaf6d25 100644 --- a/bot/exts/utilities/emoji.py +++ b/bot/exts/utilities/emoji.py @@ -78,7 +78,7 @@ class Emojis(commands.Cog): await self.bot.invoke_help_command(ctx) @emoji_group.command(name="count", aliases=("c",)) - async def count_command(self, ctx: commands.Context, *, category_query: str = None) -> None: + async def count_command(self, ctx: commands.Context, *, category_query: str | None = None) -> None: """Returns embed with emoji category and info given by the user.""" emoji_dict = defaultdict(list) diff --git a/bot/exts/utilities/reddit.py b/bot/exts/utilities/reddit.py index cfc70d85..5dd4a377 100644 --- a/bot/exts/utilities/reddit.py +++ b/bot/exts/utilities/reddit.py @@ -20,16 +20,15 @@ from bot.utils.pagination import ImagePaginator, LinePaginator log = logging.getLogger(__name__) AccessToken = namedtuple("AccessToken", ["token", "expires_at"]) +HEADERS = {"User-Agent": "python3:python-discord/bot:1.0.0 (by /u/PythonDiscord)"} +URL = "https://www.reddit.com" +OAUTH_URL = "https://oauth.reddit.com" +MAX_RETRIES = 3 class Reddit(Cog): """Track subreddit posts and show detailed statistics about them.""" - HEADERS = {"User-Agent": "python3:python-discord/bot:1.0.0 (by /u/PythonDiscord)"} - URL = "https://www.reddit.com" - OAUTH_URL = "https://oauth.reddit.com" - MAX_RETRIES = 3 - def __init__(self, bot: Bot): self.bot = bot @@ -37,7 +36,7 @@ class Reddit(Cog): self.access_token = None self.client_auth = BasicAuth(RedditConfig.client_id.get_secret_value(), RedditConfig.secret.get_secret_value()) - # self.auto_poster_loop.start() + self.auto_poster_loop.start() async def cog_unload(self) -> None: """Stop the loop task and revoke the access token when the cog is unloaded.""" @@ -68,7 +67,7 @@ class Reddit(Cog): # Normal brackets interfere with Markdown. title = escape_markdown(title).replace("[", "⦋").replace("]", "⦌") - link = self.URL + data["permalink"] + link = URL + data["permalink"] first_page += f"**[{title.replace('*', '')}]({link})**\n" @@ -121,10 +120,10 @@ class Reddit(Cog): A token is valid for 1 hour. There will be MAX_RETRIES to get a token, after which the cog will be unloaded and a ClientError raised if retrieval was still unsuccessful. """ - for i in range(1, self.MAX_RETRIES + 1): + for i in range(1, MAX_RETRIES + 1): response = await self.bot.http_session.post( - url=f"{self.URL}/api/v1/access_token", - headers=self.HEADERS, + url=f"{URL}/api/v1/access_token", + headers=HEADERS, auth=self.client_auth, data={ "grant_type": "client_credentials", @@ -144,7 +143,7 @@ class Reddit(Cog): return log.debug( f"Failed to get an access token: status {response.status} & content type {response.content_type}; " - f"retrying ({i}/{self.MAX_RETRIES})" + f"retrying ({i}/{MAX_RETRIES})" ) await asyncio.sleep(3) @@ -159,8 +158,8 @@ class Reddit(Cog): For security reasons, it's good practice to revoke the token when it's no longer being used. """ response = await self.bot.http_session.post( - url=f"{self.URL}/api/v1/revoke_token", - headers=self.HEADERS, + url=f"{URL}/api/v1/revoke_token", + headers=HEADERS, auth=self.client_auth, data={ "token": self.access_token.token, @@ -173,7 +172,7 @@ class Reddit(Cog): else: log.warning(f"Unable to revoke access token: status {response.status}.") - async def fetch_posts(self, route: str, *, amount: int = 25, params: dict = None) -> list[dict]: + async def fetch_posts(self, route: str, *, amount: int = 25, params: dict | None = None) -> list[dict]: """A helper method to fetch a certain amount of Reddit posts at a given route.""" # Reddit's JSON responses only provide 25 posts at most. if not 25 >= amount > 0: @@ -183,11 +182,11 @@ class Reddit(Cog): if not self.access_token or self.access_token.expires_at < datetime.now(tz=UTC): await self.get_access_token() - url = f"{self.OAUTH_URL}/{route}" - for _ in range(self.MAX_RETRIES): + url = f"{OAUTH_URL}/{route}" + for _ in range(MAX_RETRIES): response = await self.bot.http_session.get( url=url, - headers={**self.HEADERS, "Authorization": f"bearer {self.access_token.token}"}, + headers=HEADERS | {"Authorization": f"bearer {self.access_token.token}"}, params=params ) if response.status == 200 and response.content_type == "application/json": diff --git a/bot/exts/utilities/wolfram.py b/bot/exts/utilities/wolfram.py index d5669c6b..e77573a7 100644 --- a/bot/exts/utilities/wolfram.py +++ b/bot/exts/utilities/wolfram.py @@ -33,8 +33,8 @@ async def send_embed( ctx: Context, message_txt: str, colour: int = Colours.soft_red, - footer: str = None, - img_url: str = None, + footer: str | None = None, + img_url: str | None = None, f: discord.File = None ) -> None: """Generate & send a response embed with Wolfram as the author.""" |