aboutsummaryrefslogtreecommitdiff
path: root/lib/utils
diff options
context:
space:
mode:
Diffstat (limited to 'lib/utils')
-rw-r--r--lib/utils/BushClientUtils.ts45
-rw-r--r--lib/utils/BushConstants.ts2
-rw-r--r--lib/utils/BushUtils.ts6
-rw-r--r--lib/utils/ErrorHandler.ts236
-rw-r--r--lib/utils/FormatResponse.ts32
-rw-r--r--lib/utils/UpdateCache.ts36
6 files changed, 331 insertions, 26 deletions
diff --git a/lib/utils/BushClientUtils.ts b/lib/utils/BushClientUtils.ts
index 68a1dc3..2cf546e 100644
--- a/lib/utils/BushClientUtils.ts
+++ b/lib/utils/BushClientUtils.ts
@@ -15,10 +15,8 @@ import {
type Snowflake,
type UserResolvable
} from 'discord.js';
-import got from 'got';
import _ from 'lodash';
import { ConfigChannelKey } from '../../config/Config.js';
-import CommandErrorListener from '../../src/listeners/commands/commandError.js';
import { GlobalCache, SharedCache } from '../common/BushCache.js';
import { CommandMessage } from '../extensions/discord-akairo/BushCommand.js';
import { SlashMessage } from '../extensions/discord-akairo/SlashMessage.js';
@@ -28,6 +26,7 @@ import { BushInspectOptions } from '../types/BushInspectOptions.js';
import { CodeBlockLang } from '../types/CodeBlockLang.js';
import { emojis, Pronoun, PronounCode, pronounMapping, regex } from './BushConstants.js';
import { addOrRemoveFromArray, formatError, inspect } from './BushUtils.js';
+import { generateErrorEmbed } from './ErrorHandler.js';
/**
* Utilities that require access to the client.
@@ -74,7 +73,7 @@ export class BushClientUtils {
}
for (const url of this.#hasteURLs) {
try {
- const res: HastebinRes = await got.post(`${url}/documents`, { body: content }).json();
+ const res: HastebinRes = await (await fetch(`${url}/documents`, { method: 'POST', body: content })).json();
return { url: `${url}/${res.key}`, error: isSubstr ? 'substr' : undefined };
} catch {
void this.client.console.error('haste', `Unable to upload haste to ${url}`);
@@ -334,7 +333,7 @@ export class BushClientUtils {
public async handleError(context: string, error: Error) {
await this.client.console.error(_.camelCase(context), `An error occurred:\n${formatError(error, false)}`, false);
await this.client.console.channelError({
- embeds: await CommandErrorListener.generateErrorEmbed(this.client, { type: 'unhandledRejection', error: error, context })
+ embeds: await generateErrorEmbed(this.client, { type: 'unhandledRejection', error: error, context })
});
}
@@ -367,9 +366,8 @@ export class BushClientUtils {
public async getPronounsOf(user: User | Snowflake): Promise<Pronoun | undefined> {
const _user = await this.resolveNonCachedUser(user);
if (!_user) throw new Error(`Cannot find user ${user}`);
- const apiRes = (await got
- .get(`https://pronoundb.org/api/v1/lookup?platform=discord&id=${_user.id}`)
- .json()
+ const apiRes = (await fetch(`https://pronoundb.org/api/v1/lookup?platform=discord&id=${_user.id}`)
+ .then((p) => (p.ok ? p.json() : undefined))
.catch(() => undefined)) as { pronouns: PronounCode } | undefined;
if (!apiRes) return undefined;
@@ -386,22 +384,23 @@ export class BushClientUtils {
public async uploadImageToImgur(image: string) {
const clientId = this.client.config.credentials.imgurClientId;
- const resp = (await got
- .post('https://api.imgur.com/3/upload', {
- headers: {
- Authorization: `Client-ID ${clientId}`,
- Accept: 'application/json'
- },
- form: {
- image: image,
- type: 'base64'
- },
- followRedirect: true
- })
- .json()
- .catch(() => null)) as { data: { link: string } | undefined };
-
- return resp.data?.link ?? null;
+ const formData = new FormData();
+ formData.append('type', 'base64');
+ formData.append('image', image);
+
+ const resp = (await fetch('https://api.imgur.com/3/upload', {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ Authorization: `Client-ID ${clientId}`
+ },
+ body: formData,
+ redirect: 'follow'
+ })
+ .then((p) => (p.ok ? p.json() : null))
+ .catch(() => null)) as { data: { link: string } } | null;
+
+ return resp?.data?.link ?? null;
}
/**
diff --git a/lib/utils/BushConstants.ts b/lib/utils/BushConstants.ts
index d3089ec..c65b5e8 100644
--- a/lib/utils/BushConstants.ts
+++ b/lib/utils/BushConstants.ts
@@ -1,4 +1,4 @@
-import deepLock from 'deep-lock';
+import { default as deepLock } from 'deep-lock';
import {
ArgumentMatches as AkairoArgumentMatches,
ArgumentTypes as AkairoArgumentTypes,
diff --git a/lib/utils/BushUtils.ts b/lib/utils/BushUtils.ts
index 34ea461..1922204 100644
--- a/lib/utils/BushUtils.ts
+++ b/lib/utils/BushUtils.ts
@@ -27,7 +27,6 @@ import {
type InteractionReplyOptions,
type PermissionsString
} from 'discord.js';
-import got from 'got';
import { DeepWritable } from 'ts-essentials';
import { inspect as inspectUtil, promisify } from 'util';
import * as Format from './Format.js';
@@ -86,8 +85,11 @@ export function chunk<T>(arr: T[], perChunk: number): T[][] {
* @returns The the uuid of the user.
*/
export async function mcUUID(username: string, dashed = false): Promise<string> {
- const apiRes = (await got.get(`https://api.ashcon.app/mojang/v2/user/${username}`).json()) as UuidRes;
+ const apiRes = (await fetch(`https://api.ashcon.app/mojang/v2/user/${username}`).then((p) =>
+ p.ok ? p.json() : undefined
+ )) as UuidRes;
+ // this will throw an error if response is not ok
return dashed ? apiRes.uuid : apiRes.uuid.replace(/-/g, '');
}
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;
+ }
+}
diff --git a/lib/utils/FormatResponse.ts b/lib/utils/FormatResponse.ts
new file mode 100644
index 0000000..f094601
--- /dev/null
+++ b/lib/utils/FormatResponse.ts
@@ -0,0 +1,32 @@
+import type { GuildMember } from 'discord.js';
+import { unmuteResponse, UnmuteResponse } from '../extensions/discord.js/ExtendedGuildMember.js';
+import { emojis } from './BushConstants.js';
+import { format } from './BushUtils.js';
+import { input } from './Format.js';
+
+export function formatUnmuteResponse(prefix: string, member: GuildMember, code: UnmuteResponse): string {
+ const error = emojis.error;
+ const victim = input(member.user.tag);
+ switch (code) {
+ case unmuteResponse.MISSING_PERMISSIONS:
+ return `${error} Could not unmute ${victim} because I am missing the **Manage Roles** permission.`;
+ case unmuteResponse.NO_MUTE_ROLE:
+ return `${error} Could not unmute ${victim}, you must set a mute role with \`${prefix}config muteRole\`.`;
+ case unmuteResponse.MUTE_ROLE_INVALID:
+ return `${error} Could not unmute ${victim} because the current mute role no longer exists. Please set a new mute role with \`${prefix}config muteRole\`.`;
+ case unmuteResponse.MUTE_ROLE_NOT_MANAGEABLE:
+ return `${error} Could not unmute ${victim} because I cannot assign the current mute role, either change the role's position or set a new mute role with \`${prefix}config muteRole\`.`;
+ case unmuteResponse.ACTION_ERROR:
+ return `${error} Could not unmute ${victim}, there was an error removing their mute role.`;
+ case unmuteResponse.MODLOG_ERROR:
+ return `${error} While muting ${victim}, there was an error creating a modlog entry, please report this to my developers.`;
+ case unmuteResponse.PUNISHMENT_ENTRY_REMOVE_ERROR:
+ return `${error} While muting ${victim}, there was an error removing their mute entry, please report this to my developers.`;
+ case unmuteResponse.DM_ERROR:
+ return `${emojis.warn} unmuted ${victim} however I could not send them a dm.`;
+ case unmuteResponse.SUCCESS:
+ return `${emojis.success} Successfully unmuted ${victim}.`;
+ default:
+ return `${emojis.error} An error occurred: ${format.input(code)}}`;
+ }
+}
diff --git a/lib/utils/UpdateCache.ts b/lib/utils/UpdateCache.ts
new file mode 100644
index 0000000..2f96d9d
--- /dev/null
+++ b/lib/utils/UpdateCache.ts
@@ -0,0 +1,36 @@
+import config from '#config';
+import { Client } from 'discord.js';
+import { Global, Guild, Shared } from '../models/index.js';
+
+export async function updateGlobalCache(client: Client) {
+ const environment = config.environment;
+ const row: { [x: string]: any } = ((await Global.findByPk(environment)) ?? (await Global.create({ environment }))).toJSON();
+
+ for (const option in row) {
+ if (Object.keys(client.cache.global).includes(option)) {
+ client.cache.global[option as keyof typeof client.cache.global] = row[option];
+ }
+ }
+}
+
+export async function updateSharedCache(client: Client) {
+ const row: { [x: string]: any } = ((await Shared.findByPk(0)) ?? (await Shared.create())).toJSON();
+
+ for (const option in row) {
+ if (Object.keys(client.cache.shared).includes(option)) {
+ client.cache.shared[option as keyof typeof client.cache.shared] = row[option];
+ if (option === 'superUsers') client.superUserID = row[option];
+ }
+ }
+}
+
+export async function updateGuildCache(client: Client) {
+ const rows = await Guild.findAll();
+ for (const row of rows) {
+ client.cache.guilds.set(row.id, row.toJSON() as Guild);
+ }
+}
+
+export async function updateEveryCache(client: Client) {
+ await Promise.all([updateGlobalCache(client), updateSharedCache(client), updateGuildCache(client)]);
+}