diff options
Diffstat (limited to 'lib/utils/ErrorHandler.ts')
-rw-r--r-- | lib/utils/ErrorHandler.ts | 236 |
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; + } +} |