-
Notifications
You must be signed in to change notification settings - Fork 1
/
errorreporter.py
211 lines (184 loc) · 7.94 KB
/
errorreporter.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
"""An error reporter."""
import os
import traceback
import discord
import pymongo.errors
from discord.ext import commands
import inconnu
from logger import Logger
class ErrorReporter:
"""A utility that posts error messages in a specified channel."""
def __init__(self):
self.bot = None
self.channel = None
@property
def reinvite_message(self) -> discord.Embed:
"""Instructions for re-inviting the bot and submitting a bug report."""
invite = self.bot.invite_url
embed = discord.Embed(
title="Please re-invite Inconnu",
description=(
f"**Inconnu** has encountered an error. Please [re-invite]({invite}) "
"the bot and try again."
),
color=discord.Color.red(),
)
embed.add_field(
name="If that doesn't work ...",
value=(
f"Please join [the support server]({inconnu.constants.SUPPORT_URL}) "
"and file a bug report. Thank you!"
),
)
return embed
async def prepare_channel(self, bot):
"""Attempt to get the error channel from the bot."""
self.bot = bot
try:
if (channel := os.getenv("REPORT_CHANNEL")) is not None:
Logger.info("REPORTER: Report channel ID: %s", channel)
if (channel := await bot.fetch_channel(channel)) is not None:
Logger.info("REPORTER: Recording errors in #%s", channel.name)
self.channel = channel
else:
Logger.warning("REPORTER: Unhandled exceptions channel invalid")
else:
Logger.warning("REPORTER: Unhandled exceptions report channel not set")
except ValueError:
Logger.warning("REPORTER: Unhandled exceptions channel is not an int")
async def report_error(self, ctx: discord.ApplicationContext | discord.Interaction, error):
"""Report an error, switching between known and unknown."""
error = getattr(error, "original", error)
if isinstance(ctx, discord.ApplicationContext):
respond = ctx.respond
else:
if ctx.response.is_done():
respond = ctx.followup.send
else:
respond = ctx.response.send_message
# Known exceptions
if isinstance(error, commands.NoPrivateMessage):
await respond("Sorry, this command can only be run in a server!", ephemeral=True)
return
if isinstance(error, commands.MissingPermissions):
await respond("Sorry, you don't have permission to do this!", ephemeral=True)
return
if isinstance(error, discord.errors.NotFound):
# This just means a button tried to disable when its message no longer exists.
# We don't care, and there's nothing we can do about it anyway.
return
if isinstance(error, inconnu.errors.NotReady):
await inconnu.utils.error(ctx, str(error), title="One moment, please")
return
if isinstance(error, inconnu.errors.NotPremium):
troubleshoot_url = (
"https://docs.inconnu.app/advanced/troubleshooting#you-arent-able-to-upload-images"
)
cmd_mention = ctx.bot.cmd_mention(ctx.command.qualified_name)
await inconnu.utils.error(
ctx,
(
f"Only patrons can use {cmd_mention}. "
"Click the Patreon button to get started!"
),
(
"Already a patron?",
f"[Check the troubleshooting page.]({troubleshoot_url})",
),
title="Premium feature",
help="https://docs.inconnu.app/guides/premium",
)
return
if isinstance(error, commands.NotOwner):
await respond(
f"Sorry, only {ctx.bot.user.mention}'s owner may issue this command!",
ephemeral=True,
)
return
if isinstance(error, inconnu.errors.LockdownError):
timestamp = discord.utils.format_dt(ctx.bot.lockdown, "R")
err = f"{ctx.bot.user.mention} is undergoing maintenance {timestamp}."
embed = inconnu.utils.ErrorEmbed(
ctx.user,
err,
title="Command temporarily unavailable",
footer="Sorry for any inconvenience.",
)
await respond(embed=embed, ephemeral=True)
return
if isinstance(error, inconnu.errors.HandledError):
Logger.debug("REPORTER: Ignoring a HandledError")
return
if isinstance(error, discord.errors.DiscordServerError):
# There's nothing we can do about these
Logger.error("REPORTER: Discord server error detected")
return
if isinstance(error, AttributeError) and "PartialMessageable" in str(error):
await respond(embed=self.reinvite_message)
return
# Unknown errors and database errors are logged to a channel
if isinstance(error, pymongo.errors.PyMongoError):
await inconnu.log.report_database_error(self.bot, ctx)
# Unknown exceptions
embed = await self.error_embed(ctx, error)
try:
await self._report_unknown_error(respond, embed)
except discord.HTTPException:
embed.description = "The error was too long to fit! Check the logs."
await self._report_unknown_error(respond, embed)
raise error from error
# Print the error to the log
if isinstance(ctx, discord.ApplicationContext):
scope = ctx.command.qualified_name.upper()
else:
scope = "INTERACTION"
formatted = "".join(traceback.format_exception(error))
Logger.error("%s: %s", scope, formatted)
async def _report_unknown_error(self, respond, embed):
"""Report an unknown exception."""
user_msg = f"{self.bot.user.mention} has encountered an error. Support has been notified!"
try:
await respond(user_msg, ephemeral=True)
except (discord.NotFound, discord.HTTPException) as err:
Logger.error("REPORTER: Couldn't inform user of an error: %s", str(err))
finally:
if self.channel is not None:
await self.channel.send(embed=embed)
@staticmethod
async def error_embed(ctx, error) -> discord.Embed:
"""Create an error embed."""
if "50027" in str(error):
description = "**Unrecoverable Discord error:** Invalid webhook token"
await ctx.channel.send(
(
"**Alert:** A Discord issue is currently impacting {self.bot.user.mention}'s "
"responsiveness. Check status here: https://discordstatus.com"
),
delete_after=15,
)
else:
description = "\n".join(traceback.format_exception(error))
# If we can, we use the command name to try to pinpoint where the error
# took place. The stack trace usually makes this clear, but not always!
if isinstance(ctx, discord.ApplicationContext):
command_name = ctx.command.qualified_name.upper()
else:
command_name = "INTERACTION"
error_name = type(error).__name__
embed = discord.Embed(
title=f"{command_name}: {error_name}",
description=description,
color=0xFF0000,
timestamp=discord.utils.utcnow(),
)
if ctx.guild is not None:
guild_name = ctx.guild.name
guild_icon = ctx.guild.icon or ""
else:
guild_name = "DM"
guild_icon = ""
embed.set_author(
name=f"{ctx.user.name} ({ctx.user.id} on {guild_name}", icon_url=guild_icon
)
return embed
reporter = ErrorReporter()