Skip to content

Commit f84ddd6

Browse files
authored
Merge branch 'main' into aoc-slash-commands
2 parents 39ee4e2 + 38a59e2 commit f84ddd6

File tree

8 files changed

+808
-314
lines changed

8 files changed

+808
-314
lines changed

bot/exts/advent_of_code/_cog.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from bot.exts.advent_of_code import _helpers
2323
from bot.exts.advent_of_code.views.dayandstarview import AoCDropdownView
2424
from bot.utils import members
25-
from bot.utils.decorators import InChannelCheckFailure, in_month, whitelist_override, with_role
25+
from bot.utils.decorators import in_month, in_whitelist, with_role
2626

2727
log = logging.getLogger(__name__)
2828

@@ -34,6 +34,8 @@
3434
# They aren't spammy and foster discussion
3535
AOC_WHITELIST = AOC_WHITELIST_RESTRICTED + (Channels.advent_of_code,)
3636

37+
AOC_REDIRECT = (Channels.advent_of_code_commands, Channels.sir_lancebot_playground, Channels.bot_commands)
38+
3739

3840
class AdventOfCode(commands.Cog):
3941
"""Advent of Code festivities! Ho Ho Ho!"""
@@ -133,7 +135,7 @@ async def adventofcode_group(self, ctx: commands.Context) -> None:
133135
if not ctx.invoked_subcommand:
134136
await self.bot.invoke_help_command(ctx)
135137

136-
@with_role(Roles.admins)
138+
@with_role(Roles.admins, fail_silently=True)
137139
@adventofcode_group.command(
138140
name="block",
139141
brief="Block a user from getting the completionist role.",
@@ -148,7 +150,7 @@ async def block_from_role(self, ctx: commands.Context, member: discord.Member) -
148150
await ctx.send(f":+1: Blocked {member.mention} from getting the AoC completionist role.")
149151

150152
@adventofcode_group.command(name="countdown", aliases=("count", "c"), brief="Return time left until next day")
151-
@whitelist_override(channels=AOC_WHITELIST)
153+
@in_whitelist(channels=AOC_WHITELIST, redirect=AOC_REDIRECT)
152154
async def aoc_countdown(self, ctx: commands.Context) -> None:
153155
"""Return time left until next day."""
154156
if _helpers.is_in_advent():
@@ -167,13 +169,13 @@ async def aoc_countdown(self, ctx: commands.Context) -> None:
167169
)
168170

169171
@adventofcode_group.command(name="about", aliases=("ab", "info"), brief="Learn about Advent of Code")
170-
@whitelist_override(channels=AOC_WHITELIST)
172+
@in_whitelist(channels=AOC_WHITELIST, redirect=AOC_REDIRECT)
171173
async def about_aoc(self, ctx: commands.Context) -> None:
172174
"""Respond with an explanation of all things Advent of Code."""
173175
await ctx.send(embed=self.cached_about_aoc)
174176

175177
@aoc_slash_group.command(name="join", description="Get the join code for our community Advent of Code leaderboard")
176-
@whitelist_override(channels=AOC_WHITELIST)
178+
@in_whitelist(channels=AOC_WHITELIST, redirect=AOC_REDIRECT)
177179
@app_commands.guild_only()
178180
async def join_leaderboard(self, interaction: discord.Interaction) -> None:
179181
"""Send the user an ephemeral message with the information for joining the Python Discord leaderboard."""
@@ -229,7 +231,7 @@ async def join_leaderboard(self, interaction: discord.Interaction) -> None:
229231
aliases=("connect",),
230232
brief="Tie your Discord account with your Advent of Code name."
231233
)
232-
@whitelist_override(channels=AOC_WHITELIST)
234+
@in_whitelist(channels=AOC_WHITELIST, redirect=AOC_REDIRECT)
233235
async def aoc_link_account(self, ctx: commands.Context, *, aoc_name: str | None = None) -> None:
234236
"""
235237
Link your Discord Account to your Advent of Code name.
@@ -282,7 +284,7 @@ async def aoc_link_account(self, ctx: commands.Context, *, aoc_name: str | None
282284
aliases=("disconnect",),
283285
brief="Untie your Discord account from your Advent of Code name."
284286
)
285-
@whitelist_override(channels=AOC_WHITELIST)
287+
@in_whitelist(channels=AOC_WHITELIST, redirect=AOC_REDIRECT)
286288
async def aoc_unlink_account(self, ctx: commands.Context) -> None:
287289
"""
288290
Unlink your Discord ID with your Advent of Code leaderboard name.
@@ -303,7 +305,7 @@ async def aoc_unlink_account(self, ctx: commands.Context) -> None:
303305
aliases=("daynstar", "daystar"),
304306
brief="Get a view that lets you filter the leaderboard by day and star",
305307
)
306-
@whitelist_override(channels=AOC_WHITELIST_RESTRICTED)
308+
@in_whitelist(channels=AOC_WHITELIST_RESTRICTED, redirect=AOC_REDIRECT)
307309
async def aoc_day_and_star_leaderboard(
308310
self,
309311
ctx: commands.Context,
@@ -341,7 +343,7 @@ async def aoc_day_and_star_leaderboard(
341343
aliases=("board", "lb"),
342344
brief="Get a snapshot of the PyDis private AoC leaderboard",
343345
)
344-
@whitelist_override(channels=AOC_WHITELIST_RESTRICTED)
346+
@in_whitelist(channels=AOC_WHITELIST_RESTRICTED, redirect=AOC_REDIRECT)
345347
async def aoc_leaderboard(self, ctx: commands.Context, *, aoc_name: str | None = None) -> None:
346348
"""
347349
Get the current top scorers of the Python Discord Leaderboard.
@@ -375,7 +377,7 @@ async def aoc_leaderboard(self, ctx: commands.Context, *, aoc_name: str | None =
375377
table = (
376378
"```\n"
377379
f"{leaderboard['placement_leaderboard'] if aoc_name else leaderboard['top_leaderboard']}"
378-
"\n```"
380+
"\n```"
379381
)
380382
info_embed = _helpers.get_summary_embed(leaderboard)
381383

@@ -388,7 +390,7 @@ async def aoc_leaderboard(self, ctx: commands.Context, *, aoc_name: str | None =
388390
aliases=("globalboard", "gb"),
389391
brief="Get a link to the global leaderboard",
390392
)
391-
@whitelist_override(channels=AOC_WHITELIST_RESTRICTED)
393+
@in_whitelist(channels=AOC_WHITELIST_RESTRICTED, redirect=AOC_REDIRECT)
392394
async def aoc_global_leaderboard(self, ctx: commands.Context) -> None:
393395
"""Get a link to the global Advent of Code leaderboard."""
394396
url = self.global_leaderboard_url
@@ -404,7 +406,7 @@ async def aoc_global_leaderboard(self, ctx: commands.Context) -> None:
404406
aliases=("dailystats", "ds"),
405407
brief="Get daily statistics for the Python Discord leaderboard"
406408
)
407-
@whitelist_override(channels=AOC_WHITELIST_RESTRICTED)
409+
@in_whitelist(channels=AOC_WHITELIST_RESTRICTED, redirect=AOC_REDIRECT)
408410
async def private_leaderboard_daily_stats(self, ctx: commands.Context) -> None:
409411
"""Send an embed with daily completion statistics for the Python Discord leaderboard."""
410412
try:
@@ -429,7 +431,7 @@ async def private_leaderboard_daily_stats(self, ctx: commands.Context) -> None:
429431
info_embed = _helpers.get_summary_embed(leaderboard)
430432
await ctx.send(f"```\n{table}\n```", embed=info_embed)
431433

432-
@with_role(Roles.admins)
434+
@with_role(Roles.admins, fail_silently=True)
433435
@adventofcode_group.command(
434436
name="refresh",
435437
aliases=("fetch",),
@@ -473,9 +475,3 @@ def _build_about_embed(self) -> discord.Embed:
473475

474476
about_embed.set_footer(text="Last Updated")
475477
return about_embed
476-
477-
async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None:
478-
"""Custom error handler if an advent of code command was posted in the wrong channel."""
479-
if isinstance(error, InChannelCheckFailure):
480-
await ctx.send(f":x: Please use <#{Channels.advent_of_code_commands}> for aoc commands instead.")
481-
error.handled = True

bot/exts/core/__init__.py

Whitespace-only changes.

bot/exts/core/error_handler.py

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from discord import Colour, Embed
2+
from discord.ext.commands import (
3+
Cog,
4+
CommandError,
5+
Context,
6+
errors,
7+
)
8+
9+
from bot.bot import SirRobin
10+
from bot.log import get_logger
11+
from bot.utils.exceptions import (
12+
InMonthCheckFailure,
13+
InWhitelistCheckFailure,
14+
SilentCheckFailure,
15+
)
16+
17+
log = get_logger(__name__)
18+
19+
20+
class ErrorHandler(Cog):
21+
"""Handles errors emitted from commands."""
22+
23+
def __init__(self, bot: SirRobin):
24+
self.bot = bot
25+
26+
@staticmethod
27+
def _get_error_embed(title: str, body: str) -> Embed:
28+
"""Return a embed with our error colour assigned."""
29+
return Embed(
30+
title=title,
31+
colour=Colour.brand_red(),
32+
description=body
33+
)
34+
35+
@Cog.listener()
36+
async def on_command_error(self, ctx: Context, error: CommandError) -> None:
37+
"""
38+
Generic command error handling from other cogs.
39+
40+
Using the error type, handle the error appropriately.
41+
if there is no handling for the error type raised,
42+
a message will be sent to the user & it will be logged.
43+
44+
In the future, I would expect this to be used as a place
45+
to push errors to a sentry instance.
46+
"""
47+
log.trace(f"Handling a raised error {error} from {ctx.command}")
48+
49+
if isinstance(error, errors.UserInputError):
50+
await self.handle_user_input_error(ctx, error)
51+
return
52+
53+
if isinstance(error, errors.CheckFailure):
54+
await self.handle_check_failure(ctx, error)
55+
return
56+
57+
if isinstance(error, errors.CommandNotFound):
58+
embed = self._get_error_embed("Command not found", str(error))
59+
else:
60+
# If we haven't handled it by this point, it is considered an unexpected/handled error.
61+
log.exception(f"Error executing command invoked by {ctx.message.author}: {ctx.message.content}")
62+
embed = self._get_error_embed(
63+
"Unexpected error",
64+
"Sorry, an unexpected error occurred. Please let us know!\n\n"
65+
f"```{error.__class__.__name__}: {error}```"
66+
)
67+
await ctx.send(embed=embed)
68+
69+
async def handle_user_input_error(self, ctx: Context, e: errors.UserInputError) -> None:
70+
"""
71+
Send an error message in `ctx` for UserInputError, sometimes invoking the help command too.
72+
73+
* MissingRequiredArgument: send an error message with arg name and the help command
74+
* TooManyArguments: send an error message and the help command
75+
* BadArgument: send an error message and the help command
76+
* BadUnionArgument: send an error message including the error produced by the last converter
77+
* ArgumentParsingError: send an error message
78+
* Other: send an error message and the help command
79+
"""
80+
if isinstance(e, errors.MissingRequiredArgument):
81+
embed = self._get_error_embed("Missing required argument", e.param.name)
82+
elif isinstance(e, errors.TooManyArguments):
83+
embed = self._get_error_embed("Too many arguments", str(e))
84+
elif isinstance(e, errors.BadArgument):
85+
embed = self._get_error_embed("Bad argument", str(e))
86+
elif isinstance(e, errors.BadUnionArgument):
87+
embed = self._get_error_embed("Bad argument", f"{e}\n{e.errors[-1]}")
88+
elif isinstance(e, errors.ArgumentParsingError):
89+
embed = self._get_error_embed("Argument parsing error", str(e))
90+
else:
91+
embed = self._get_error_embed(
92+
"Input error",
93+
"Something about your input seems off. Check the arguments and try again."
94+
)
95+
96+
await ctx.send(embed=embed)
97+
98+
async def handle_check_failure(self, ctx: Context, e: errors.CheckFailure) -> None:
99+
"""
100+
Send an error message in `ctx` for certain types of CheckFailure.
101+
102+
The following types are handled:
103+
104+
* BotMissingPermissions
105+
* BotMissingRole
106+
* BotMissingAnyRole
107+
* MissingAnyRole
108+
* InMonthCheckFailure
109+
* SilentCheckFailure
110+
* InWhitelistCheckFailure
111+
* NoPrivateMessage
112+
"""
113+
bot_missing_errors = (
114+
errors.BotMissingPermissions,
115+
errors.BotMissingRole,
116+
errors.BotMissingAnyRole
117+
)
118+
119+
if isinstance(e, SilentCheckFailure):
120+
# Silently fail, SirRobin should not respond
121+
log.info(
122+
f"{ctx.author} ({ctx.author.id}) tried to run {ctx.command} "
123+
f"but hit a silent check failure {e.__class__.__name__}",
124+
)
125+
return
126+
127+
if isinstance(e, bot_missing_errors):
128+
embed = self._get_error_embed("Permission error", "I don't have the permission I need to do that!")
129+
elif isinstance(e, errors.MissingAnyRole):
130+
embed = self._get_error_embed("Permission error", "You are not allowed to use this command!")
131+
elif isinstance(e, InMonthCheckFailure):
132+
embed = self._get_error_embed("Command not available", str(e))
133+
elif isinstance(e, InWhitelistCheckFailure):
134+
embed = self._get_error_embed("Wrong Channel", str(e))
135+
elif isinstance(e, errors.NoPrivateMessage):
136+
embed = self._get_error_embed("Wrong channel", "This command can not be ran in DMs!")
137+
else:
138+
embed = self._get_error_embed(
139+
"Unexpected check failure",
140+
"Sorry, an unexpected check error occurred. Please let us know!\n\n"
141+
f"```{e.__class__.__name__}: {e}```"
142+
)
143+
await ctx.send(embed=embed)
144+
145+
146+
async def setup(bot: SirRobin) -> None:
147+
"""Load the ErrorHandler cog."""
148+
await bot.add_cog(ErrorHandler(bot))

0 commit comments

Comments
 (0)