aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/automod/AutomodShared.ts4
-rw-r--r--lib/common/Sentry.ts2
-rw-r--r--lib/common/tags.ts2
-rw-r--r--lib/extensions/discord-akairo/BushClient.ts43
-rw-r--r--lib/global.ts (renamed from lib/extensions/global.ts)4
-rw-r--r--lib/index.ts5
-rw-r--r--lib/models/index.ts13
-rw-r--r--lib/models/instance/ActivePunishment.ts3
-rw-r--r--lib/models/instance/Guild.ts6
-rw-r--r--lib/models/instance/Highlight.ts3
-rw-r--r--lib/models/instance/Level.ts3
-rw-r--r--lib/models/instance/ModLog.ts3
-rw-r--r--lib/models/instance/Reminder.ts3
-rw-r--r--lib/models/instance/StickyRole.ts3
-rw-r--r--lib/models/shared/Global.ts3
-rw-r--r--lib/models/shared/Shared.ts3
-rw-r--r--lib/models/shared/Stat.ts3
-rw-r--r--lib/tsconfig.json6
-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
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)]);
+}