diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/automod/AutomodShared.ts | 4 | ||||
-rw-r--r-- | lib/common/Sentry.ts | 2 | ||||
-rw-r--r-- | lib/common/tags.ts | 2 | ||||
-rw-r--r-- | lib/extensions/discord-akairo/BushClient.ts | 43 | ||||
-rw-r--r-- | lib/global.ts (renamed from lib/extensions/global.ts) | 4 | ||||
-rw-r--r-- | lib/index.ts | 5 | ||||
-rw-r--r-- | lib/models/index.ts | 13 | ||||
-rw-r--r-- | lib/models/instance/ActivePunishment.ts | 3 | ||||
-rw-r--r-- | lib/models/instance/Guild.ts | 6 | ||||
-rw-r--r-- | lib/models/instance/Highlight.ts | 3 | ||||
-rw-r--r-- | lib/models/instance/Level.ts | 3 | ||||
-rw-r--r-- | lib/models/instance/ModLog.ts | 3 | ||||
-rw-r--r-- | lib/models/instance/Reminder.ts | 3 | ||||
-rw-r--r-- | lib/models/instance/StickyRole.ts | 3 | ||||
-rw-r--r-- | lib/models/shared/Global.ts | 3 | ||||
-rw-r--r-- | lib/models/shared/Shared.ts | 3 | ||||
-rw-r--r-- | lib/models/shared/Stat.ts | 3 | ||||
-rw-r--r-- | lib/tsconfig.json | 6 | ||||
-rw-r--r-- | lib/utils/BushClientUtils.ts | 45 | ||||
-rw-r--r-- | lib/utils/BushConstants.ts | 2 | ||||
-rw-r--r-- | lib/utils/BushUtils.ts | 6 | ||||
-rw-r--r-- | lib/utils/ErrorHandler.ts | 236 | ||||
-rw-r--r-- | lib/utils/FormatResponse.ts | 32 | ||||
-rw-r--r-- | lib/utils/UpdateCache.ts | 36 |
24 files changed, 390 insertions, 79 deletions
diff --git a/lib/automod/AutomodShared.ts b/lib/automod/AutomodShared.ts index 5d031d0..08cde25 100644 --- a/lib/automod/AutomodShared.ts +++ b/lib/automod/AutomodShared.ts @@ -8,11 +8,11 @@ import { PermissionFlagsBits, Snowflake } from 'discord.js'; -import UnmuteCommand from '../../src/commands/moderation/unmute.js'; import * as Moderation from '../common/Moderation.js'; import { unmuteResponse } from '../extensions/discord.js/ExtendedGuildMember.js'; import { colors, emojis } from '../utils/BushConstants.js'; import * as Format from '../utils/Format.js'; +import { formatUnmuteResponse } from '../utils/FormatResponse.js'; /** * Handles shared auto moderation functionality. @@ -207,7 +207,7 @@ export async function handleAutomodInteraction(interaction: ButtonInteraction) { if (check !== true) return interaction.reply({ content: check, ephemeral: true }); const check2 = await Moderation.checkMutePermissions(interaction.guild); - if (check2 !== true) return interaction.reply({ content: UnmuteCommand.formatCode('/', victim!, check2), ephemeral: true }); + if (check2 !== true) return interaction.reply({ content: formatUnmuteResponse('/', victim!, check2), ephemeral: true }); const result = await victim.bushUnmute({ reason, diff --git a/lib/common/Sentry.ts b/lib/common/Sentry.ts index 446ec27..1b0e19a 100644 --- a/lib/common/Sentry.ts +++ b/lib/common/Sentry.ts @@ -1,7 +1,7 @@ +import type { Config } from '#config'; import { RewriteFrames } from '@sentry/integrations'; import * as SentryNode from '@sentry/node'; import { Integrations } from '@sentry/node'; -import type { Config } from '../../config/Config.js'; export class Sentry { public constructor(rootdir: string, config: Config) { diff --git a/lib/common/tags.ts b/lib/common/tags.ts index 098cf29..4af8783 100644 --- a/lib/common/tags.ts +++ b/lib/common/tags.ts @@ -1,5 +1,5 @@ /* these functions are adapted from the common-tags npm package which is licensed under the MIT license */ -/* the js docs are adapted from the @types/common-tags npm package which is licensed under the MIT license */ +/* the JSDOCs are adapted from the @types/common-tags npm package which is licensed under the MIT license */ /** * Strips the **initial** indentation from the beginning of each line in a multiline string. diff --git a/lib/extensions/discord-akairo/BushClient.ts b/lib/extensions/discord-akairo/BushClient.ts index 1a6bb8c..92968d6 100644 --- a/lib/extensions/discord-akairo/BushClient.ts +++ b/lib/extensions/discord-akairo/BushClient.ts @@ -10,7 +10,8 @@ import { roleWithDuration, snowflake } from '#args'; -import { BushClientEvents, emojis, formatError, inspect } from '#lib'; +import type { Config } from '#config'; +import { BushClientEvents, emojis, formatError, inspect, updateEveryCache } from '#lib'; import { patch, type PatchedElements } from '@notenoughupdates/events-intercept'; import * as Sentry from '@sentry/node'; import { @@ -44,26 +45,25 @@ import type EventEmitter from 'events'; import { google } from 'googleapis'; import path from 'path'; import readline from 'readline'; -import type { Options as SequelizeOptions, Sequelize as SequelizeType } from 'sequelize'; +import { Options as SequelizeOptions, Sequelize, Sequelize as SequelizeType } from 'sequelize'; import { fileURLToPath } from 'url'; -import type { Config } from '../../../config/Config.js'; -import UpdateCacheTask from '../../../src/tasks/cache/updateCache.js'; -import UpdateStatsTask from '../../../src/tasks/feature/updateStats.js'; import { tinyColor } from '../../arguments/tinyColor.js'; import { BushCache } from '../../common/BushCache.js'; import { HighlightManager } from '../../common/HighlightManager.js'; -import { ActivePunishment } from '../../models/instance/ActivePunishment.js'; -import { Guild as GuildDB } from '../../models/instance/Guild.js'; -import { Highlight } from '../../models/instance/Highlight.js'; -import { Level } from '../../models/instance/Level.js'; -import { ModLog } from '../../models/instance/ModLog.js'; -import { Reminder } from '../../models/instance/Reminder.js'; -import { StickyRole } from '../../models/instance/StickyRole.js'; -import { Global } from '../../models/shared/Global.js'; -import { GuildCount } from '../../models/shared/GuildCount.js'; -import { MemberCount } from '../../models/shared/MemberCount.js'; -import { Shared } from '../../models/shared/Shared.js'; -import { Stat } from '../../models/shared/Stat.js'; +import { + ActivePunishment, + Global, + Guild as GuildModel, + GuildCount, + Highlight, + Level, + MemberCount, + ModLog, + Reminder, + Shared, + Stat, + StickyRole +} from '../../models/index.js'; import { AllowedMentions } from '../../utils/AllowedMentions.js'; import { BushClientUtils } from '../../utils/BushClientUtils.js'; import { BushLogger } from '../../utils/BushLogger.js'; @@ -75,7 +75,6 @@ import { BushCommandHandler } from './BushCommandHandler.js'; import { BushInhibitorHandler } from './BushInhibitorHandler.js'; import { BushListenerHandler } from './BushListenerHandler.js'; import { BushTaskHandler } from './BushTaskHandler.js'; -const { Sequelize } = (await import('sequelize')).default; declare module 'discord.js' { export interface Client extends EventEmitter { @@ -467,7 +466,7 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re public async dbPreInit() { try { await this.instanceDB.authenticate(); - GuildDB.initModel(this.instanceDB, this); + GuildModel.initModel(this.instanceDB, this); ModLog.initModel(this.instanceDB); ActivePunishment.initModel(this.instanceDB); Level.initModel(this.instanceDB); @@ -525,9 +524,11 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re try { await this.highlightManager.syncCache(); - await UpdateCacheTask.init(this); + await updateEveryCache(this); void this.console.success('startup', `Successfully created <<cache>>.`, false); - const stats = await UpdateStatsTask.init(this); + + const stats = + (await Stat.findByPk(this.config.environment)) ?? (await Stat.create({ environment: this.config.environment })); this.stats.commandsUsed = stats.commandsUsed; this.stats.slashCommandsUsed = stats.slashCommandsUsed; await this.login(this.token!); diff --git a/lib/extensions/global.ts b/lib/global.ts index a9020d7..0a0bcca 100644 --- a/lib/extensions/global.ts +++ b/lib/global.ts @@ -1,6 +1,6 @@ -/* eslint-disable no-var */ +/* eslint-disable */ + declare global { - // eslint-disable-next-line @typescript-eslint/no-unused-vars interface ReadonlyArray<T> { includes<S, R extends `${Extract<S, string>}`>( this: ReadonlyArray<R>, diff --git a/lib/index.ts b/lib/index.ts index 5a8ecde..ca23177 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,3 +1,5 @@ +import './global.js'; + export * from './automod/AutomodShared.js'; export * from './automod/MemberAutomod.js'; export * from './automod/MessageAutomod.js'; @@ -53,4 +55,7 @@ export * as Arg from './utils/Arg.js'; export * from './utils/BushConstants.js'; export * from './utils/BushLogger.js'; export * from './utils/BushUtils.js'; +export * from './utils/ErrorHandler.js'; export * as Format from './utils/Format.js'; +export * from './utils/FormatResponse.js'; +export * from './utils/UpdateCache.js'; diff --git a/lib/models/index.ts b/lib/models/index.ts new file mode 100644 index 0000000..ae82fb7 --- /dev/null +++ b/lib/models/index.ts @@ -0,0 +1,13 @@ +export * from './BaseModel.js'; +export * from './instance/ActivePunishment.js'; +export * from './instance/Guild.js'; +export * from './instance/Highlight.js'; +export * from './instance/Level.js'; +export * from './instance/ModLog.js'; +export * from './instance/Reminder.js'; +export * from './instance/StickyRole.js'; +export * from './shared/Global.js'; +export * from './shared/GuildCount.js'; +export * from './shared/MemberCount.js'; +export * from './shared/Shared.js'; +export * from './shared/Stat.js'; diff --git a/lib/models/instance/ActivePunishment.ts b/lib/models/instance/ActivePunishment.ts index 38012ca..9bd9d01 100644 --- a/lib/models/instance/ActivePunishment.ts +++ b/lib/models/instance/ActivePunishment.ts @@ -1,8 +1,7 @@ import { type Snowflake } from 'discord.js'; import { nanoid } from 'nanoid'; -import { type Sequelize } from 'sequelize'; +import { DataTypes, type Sequelize } from 'sequelize'; import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; export enum ActivePunishmentType { BAN = 'BAN', diff --git a/lib/models/instance/Guild.ts b/lib/models/instance/Guild.ts index f258d48..1d645e9 100644 --- a/lib/models/instance/Guild.ts +++ b/lib/models/instance/Guild.ts @@ -1,9 +1,9 @@ +import config from '#config'; import { ChannelType, Constants, type Snowflake } from 'discord.js'; -import { type Sequelize } from 'sequelize'; +import { DataTypes, type Sequelize } from 'sequelize'; import { BadWordDetails } from '../../automod/AutomodShared.js'; import { type BushClient } from '../../extensions/discord-akairo/BushClient.js'; import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; export interface GuildModel { id: Snowflake; @@ -199,8 +199,6 @@ const asGuildSetting = <T>(et: { [K in keyof T]: PartialBy<GuildSetting, 'config return et as { [K in keyof T]: GuildSetting }; }; -const { default: config } = await import('../../../config/options.js'); - export const guildSettingsObj = asGuildSetting({ prefix: { name: 'Prefix', diff --git a/lib/models/instance/Highlight.ts b/lib/models/instance/Highlight.ts index 5889fad..38c7990 100644 --- a/lib/models/instance/Highlight.ts +++ b/lib/models/instance/Highlight.ts @@ -1,8 +1,7 @@ import { type Snowflake } from 'discord.js'; import { nanoid } from 'nanoid'; -import { type Sequelize } from 'sequelize'; +import { DataTypes, type Sequelize } from 'sequelize'; import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; export interface HighlightModel { pk: string; diff --git a/lib/models/instance/Level.ts b/lib/models/instance/Level.ts index d8d16f0..e22d63b 100644 --- a/lib/models/instance/Level.ts +++ b/lib/models/instance/Level.ts @@ -1,7 +1,6 @@ import { type Snowflake } from 'discord.js'; -import { type Sequelize } from 'sequelize'; +import { DataTypes, type Sequelize } from 'sequelize'; import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; export interface LevelModel { user: Snowflake; diff --git a/lib/models/instance/ModLog.ts b/lib/models/instance/ModLog.ts index c25f043..324ad83 100644 --- a/lib/models/instance/ModLog.ts +++ b/lib/models/instance/ModLog.ts @@ -1,8 +1,7 @@ import { type Snowflake } from 'discord.js'; import { nanoid } from 'nanoid'; -import { type Sequelize } from 'sequelize'; +import { DataTypes, type Sequelize } from 'sequelize'; import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; export enum ModLogType { PERM_BAN = 'PERM_BAN', diff --git a/lib/models/instance/Reminder.ts b/lib/models/instance/Reminder.ts index 964ea63..8d46edb 100644 --- a/lib/models/instance/Reminder.ts +++ b/lib/models/instance/Reminder.ts @@ -1,8 +1,7 @@ import { Snowflake } from 'discord.js'; import { nanoid } from 'nanoid'; -import { type Sequelize } from 'sequelize'; +import { DataTypes, type Sequelize } from 'sequelize'; import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; export interface ReminderModel { id: string; diff --git a/lib/models/instance/StickyRole.ts b/lib/models/instance/StickyRole.ts index 00e98ce..90ded0e 100644 --- a/lib/models/instance/StickyRole.ts +++ b/lib/models/instance/StickyRole.ts @@ -1,7 +1,6 @@ import { type Snowflake } from 'discord.js'; -import { type Sequelize } from 'sequelize'; +import { DataTypes, type Sequelize } from 'sequelize'; import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; export interface StickyRoleModel { user: Snowflake; diff --git a/lib/models/shared/Global.ts b/lib/models/shared/Global.ts index b1aa0cc..eb6c5dd 100644 --- a/lib/models/shared/Global.ts +++ b/lib/models/shared/Global.ts @@ -1,7 +1,6 @@ import { type Snowflake } from 'discord.js'; -import { type Sequelize } from 'sequelize'; +import { DataTypes, type Sequelize } from 'sequelize'; import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; export interface GlobalModel { environment: 'production' | 'development' | 'beta'; diff --git a/lib/models/shared/Shared.ts b/lib/models/shared/Shared.ts index dec77d1..bf8d461 100644 --- a/lib/models/shared/Shared.ts +++ b/lib/models/shared/Shared.ts @@ -1,8 +1,7 @@ import { Snowflake } from 'discord.js'; -import type { Sequelize } from 'sequelize'; +import { DataTypes, Sequelize } from 'sequelize'; import { BadWords } from '../../automod/AutomodShared.js'; import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; export interface SharedModel { primaryKey: 0; diff --git a/lib/models/shared/Stat.ts b/lib/models/shared/Stat.ts index 8e2e0b3..bce1620 100644 --- a/lib/models/shared/Stat.ts +++ b/lib/models/shared/Stat.ts @@ -1,6 +1,5 @@ -import { type Sequelize } from 'sequelize'; +import { DataTypes, type Sequelize } from 'sequelize'; import { BaseModel } from '../BaseModel.js'; -const { DataTypes } = (await import('sequelize')).default; type Environment = 'production' | 'development' | 'beta'; diff --git a/lib/tsconfig.json b/lib/tsconfig.json index e6d554e..0b2117d 100644 --- a/lib/tsconfig.json +++ b/lib/tsconfig.json @@ -1,9 +1,7 @@ { "extends": "../tsconfig.base.json", "compilerOptions": { - "outDir": "../../dist/lib", - "composite": true + "outDir": "../dist/lib" }, - "include": ["lib/**/*.ts"], - "references": [{ "path": "../config" }] + "references": [{ "path": "../config" }, { "path": "../tsconfig.pkg.json" }] } 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)]); +} |