aboutsummaryrefslogtreecommitdiff
path: root/lib/utils/ErrorHandler.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/utils/ErrorHandler.ts')
-rw-r--r--lib/utils/ErrorHandler.ts236
1 files changed, 236 insertions, 0 deletions
diff --git a/lib/utils/ErrorHandler.ts b/lib/utils/ErrorHandler.ts
new file mode 100644
index 0000000..923da75
--- /dev/null
+++ b/lib/utils/ErrorHandler.ts
@@ -0,0 +1,236 @@
+import { AkairoMessage, Command } from 'discord-akairo';
+import { ChannelType, Client, EmbedBuilder, escapeInlineCode, GuildTextBasedChannel, Message } from 'discord.js';
+import { BushCommandHandlerEvents } from '../extensions/discord-akairo/BushCommandHandler.js';
+import { SlashMessage } from '../extensions/discord-akairo/SlashMessage.js';
+import { colors } from './BushConstants.js';
+import { capitalize, formatError } from './BushUtils.js';
+import { bold, input } from './Format.js';
+
+export async function handleCommandError(
+ client: Client,
+ ...[error, message, _command]: BushCommandHandlerEvents['error'] | BushCommandHandlerEvents['slashError']
+) {
+ try {
+ const isSlash = message.util?.isSlash;
+ const errorNum = Math.floor(Math.random() * 6969696969) + 69; // hehe funny number
+ const channel =
+ message.channel?.type === ChannelType.DM ? message.channel.recipient?.tag : (<GuildTextBasedChannel>message.channel)?.name;
+ const command = _command ?? message.util?.parsed?.command;
+
+ client.sentry.captureException(error, {
+ level: 'error',
+ user: { id: message.author.id, username: message.author.tag },
+ extra: {
+ 'command.name': command?.id,
+ 'message.id': message.id,
+ 'message.type': message.util ? (message.util.isSlash ? 'slash' : 'normal') : 'unknown',
+ 'message.parsed.content': message.util?.parsed?.content,
+ 'channel.id':
+ (message.channel?.type === ChannelType.DM ? message.channel.recipient?.id : message.channel?.id) ?? '¯\\_(ツ)_/¯',
+ 'channel.name': channel,
+ 'guild.id': message.guild?.id ?? '¯\\_(ツ)_/¯',
+ 'guild.name': message.guild?.name ?? '¯\\_(ツ)_/¯',
+ 'environment': client.config.environment
+ }
+ });
+
+ void client.console.error(
+ `${isSlash ? 'slashC' : 'c'}ommandError`,
+ `an error occurred with the <<${command}>> ${isSlash ? 'slash ' : ''}command in <<${channel}>> triggered by <<${
+ message?.author?.tag
+ }>>:\n${formatError(error, true)})}`,
+ false
+ );
+
+ const _haste = getErrorHaste(client, error);
+ const _stack = getErrorStack(client, error);
+ const [haste, stack] = await Promise.all([_haste, _stack]);
+ const options = { message, error, isSlash, errorNum, command, channel, haste, stack };
+
+ const errorEmbed = _generateErrorEmbed({
+ ...options,
+ type: 'command-log'
+ });
+
+ void client.logger.channelError({ embeds: errorEmbed });
+
+ if (message) {
+ if (!client.config.owners.includes(message.author.id)) {
+ const errorUserEmbed = _generateErrorEmbed({
+ ...options,
+ type: 'command-user'
+ });
+ void message.util?.send({ embeds: errorUserEmbed }).catch(() => null);
+ } else {
+ const errorDevEmbed = _generateErrorEmbed({
+ ...options,
+ type: 'command-dev'
+ });
+
+ void message.util?.send({ embeds: errorDevEmbed }).catch(() => null);
+ }
+ }
+ } catch (e) {
+ throw new IFuckedUpError('An error occurred while handling a command error.', error, e);
+ }
+}
+
+export async function generateErrorEmbed(
+ client: Client,
+ options:
+ | {
+ message: Message | AkairoMessage;
+ error: Error | any;
+ isSlash?: boolean;
+ type: 'command-log' | 'command-dev' | 'command-user';
+ errorNum: number;
+ command?: Command;
+ channel?: string;
+ }
+ | { error: Error | any; type: 'uncaughtException' | 'unhandledRejection'; context?: string }
+): Promise<EmbedBuilder[]> {
+ const _haste = getErrorHaste(client, options.error);
+ const _stack = getErrorStack(client, options.error);
+ const [haste, stack] = await Promise.all([_haste, _stack]);
+
+ return _generateErrorEmbed({ ...options, haste, stack });
+}
+
+function _generateErrorEmbed(
+ options:
+ | {
+ message: Message | SlashMessage;
+ error: Error | any;
+ isSlash?: boolean;
+ type: 'command-log' | 'command-dev' | 'command-user';
+ errorNum: number;
+ command?: Command;
+ channel?: string;
+ haste: string[];
+ stack: string;
+ }
+ | {
+ error: Error | any;
+ type: 'uncaughtException' | 'unhandledRejection';
+ context?: string;
+ haste: string[];
+ stack: string;
+ }
+): EmbedBuilder[] {
+ const embeds = [new EmbedBuilder().setColor(colors.error)];
+ if (options.type === 'command-user') {
+ embeds[0]
+ .setTitle('An Error Occurred')
+ .setDescription(
+ `Oh no! ${
+ options.command ? `While running the ${options.isSlash ? 'slash ' : ''}command ${input(options.command.id)}, a` : 'A'
+ }n error occurred. Please give the developers code ${input(`${options.errorNum}`)}.`
+ )
+ .setTimestamp();
+ return embeds;
+ }
+ const description: string[] = [];
+
+ if (options.type === 'command-log') {
+ description.push(
+ `**User:** ${options.message.author} (${options.message.author.tag})`,
+ `**Command:** ${options.command ?? 'N/A'}`,
+ `**Channel:** <#${options.message.channel?.id}> (${options.channel})`,
+ `**Message:** [link](${options.message.url})`
+ );
+ if (options.message?.util?.parsed?.content) description.push(`**Command Content:** ${options.message.util.parsed.content}`);
+ }
+
+ description.push(...options.haste);
+
+ embeds.push(new EmbedBuilder().setColor(colors.error).setTimestamp().setDescription(options.stack.substring(0, 4000)));
+ if (description.length) embeds[0].setDescription(description.join('\n').substring(0, 4000));
+
+ if (options.type === 'command-dev' || options.type === 'command-log')
+ embeds[0].setTitle(`${options.isSlash ? 'Slash ' : ''}CommandError #${input(`${options.errorNum}`)}`);
+ else if (options.type === 'uncaughtException')
+ embeds[0].setTitle(`${options.context ? `[${bold(options.context)}] An Error Occurred` : 'Uncaught Exception'}`);
+ else if (options.type === 'unhandledRejection')
+ embeds[0].setTitle(`${options.context ? `[${bold(options.context)}] An Error Occurred` : 'Unhandled Promise Rejection'}`);
+ return embeds;
+}
+
+export async function getErrorHaste(client: Client, error: Error | any): Promise<string[]> {
+ const inspectOptions = {
+ showHidden: false,
+ depth: 9,
+ colors: false,
+ customInspect: true,
+ showProxy: false,
+ maxArrayLength: Infinity,
+ maxStringLength: Infinity,
+ breakLength: 80,
+ compact: 3,
+ sorted: false,
+ getters: true
+ };
+
+ const ret: string[] = [];
+ const promises: Promise<{
+ url?: string | undefined;
+ error?: 'content too long' | 'substr' | 'unable to post' | undefined;
+ }>[] = [];
+ const pair: {
+ [key: string]: {
+ url?: string | undefined;
+ error?: 'content too long' | 'substr' | 'unable to post' | undefined;
+ };
+ } = {};
+
+ for (const element in error) {
+ if (['stack', 'name', 'message'].includes(element)) continue;
+ else if (typeof (error as any)[element] === 'object') {
+ promises.push(client.utils.inspectCleanRedactHaste((error as any)[element], inspectOptions));
+ }
+ }
+
+ const links = await Promise.all(promises);
+
+ let index = 0;
+ for (const element in error) {
+ if (['stack', 'name', 'message'].includes(element)) continue;
+ else if (typeof (error as any)[element] === 'object') {
+ pair[element] = links[index];
+ index++;
+ }
+ }
+
+ for (const element in error) {
+ if (['stack', 'name', 'message'].includes(element)) continue;
+ else {
+ ret.push(
+ `**Error ${capitalize(element)}:** ${
+ typeof error[element] === 'object'
+ ? `${
+ pair[element].url
+ ? `[haste](${pair[element].url})${pair[element].error ? ` - ${pair[element].error}` : ''}`
+ : pair[element].error
+ }`
+ : `\`${escapeInlineCode(client.utils.inspectAndRedact((error as any)[element], inspectOptions))}\``
+ }`
+ );
+ }
+ }
+ return ret;
+}
+
+export async function getErrorStack(client: Client, error: Error | any): Promise<string> {
+ return await client.utils.inspectCleanRedactCodeblock(error, 'js', { colors: false }, 4000);
+}
+
+export class IFuckedUpError extends Error {
+ public declare original: Error | any;
+ public declare newError: Error | any;
+
+ public constructor(message: string, original?: Error | any, newError?: Error | any) {
+ super(message);
+ this.name = 'IFuckedUpError';
+ this.original = original;
+ this.newError = newError;
+ }
+}