aboutsummaryrefslogtreecommitdiff
path: root/src/lib
diff options
context:
space:
mode:
authorIRONM00N <64110067+IRONM00N@users.noreply.github.com>2022-06-16 14:32:18 -0400
committerIRONM00N <64110067+IRONM00N@users.noreply.github.com>2022-06-16 14:32:18 -0400
commit0e87bbd3940d89defcb04926587b35c8f4d1947f (patch)
treee50860d4dc25a11d4c3977b583284c4bcad1b077 /src/lib
parent661e4c9935aeb8760dafc7ced4bbec6cc356a033 (diff)
downloadtanzanite-0e87bbd3940d89defcb04926587b35c8f4d1947f.tar.gz
tanzanite-0e87bbd3940d89defcb04926587b35c8f4d1947f.tar.bz2
tanzanite-0e87bbd3940d89defcb04926587b35c8f4d1947f.zip
remove util classes, move config out of src
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/common/AutoMod.ts51
-rw-r--r--src/lib/common/ButtonPaginator.ts47
-rw-r--r--src/lib/common/ConfirmationPrompt.ts27
-rw-r--r--src/lib/common/DeleteButton.ts27
-rw-r--r--src/lib/common/HighlightManager.ts16
-rw-r--r--src/lib/common/Sentry.ts2
-rw-r--r--src/lib/common/util/Arg.ts269
-rw-r--r--src/lib/common/util/Format.ts187
-rw-r--r--src/lib/common/util/Moderation.ts517
-rw-r--r--src/lib/extensions/discord-akairo/BushClient.ts52
-rw-r--r--src/lib/extensions/discord-akairo/BushClientUtil.ts1187
-rw-r--r--src/lib/extensions/discord-akairo/BushInhibitor.ts8
-rw-r--r--src/lib/extensions/discord-akairo/BushListener.ts15
-rw-r--r--src/lib/extensions/discord-akairo/BushTask.ts2
-rw-r--r--src/lib/extensions/discord.js/ExtendedGuild.ts33
-rw-r--r--src/lib/extensions/discord.js/ExtendedGuildMember.ts26
-rw-r--r--src/lib/extensions/global.ts7
-rw-r--r--src/lib/index.ts19
-rw-r--r--src/lib/utils/BushConstants.ts813
-rw-r--r--src/lib/utils/BushLogger.ts256
-rw-r--r--src/lib/utils/BushUtils.ts1058
-rw-r--r--src/lib/utils/Config.ts93
22 files changed, 2219 insertions, 2493 deletions
diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts
index 982e0e8..7f19e63 100644
--- a/src/lib/common/AutoMod.ts
+++ b/src/lib/common/AutoMod.ts
@@ -1,4 +1,4 @@
-import { banResponse, Moderation } from '#lib';
+import { banResponse, codeblock, colors, emojis, format, formatError, getShared, Moderation, resolveNonCachedUser } from '#lib';
import assert from 'assert';
import chalk from 'chalk';
import {
@@ -18,11 +18,6 @@ import {
*/
export class AutoMod {
/**
- * The message to check for blacklisted phrases on
- */
- private message: Message;
-
- /**
* Whether or not a punishment has already been given to the user
*/
private punished = false;
@@ -30,8 +25,12 @@ export class AutoMod {
/**
* @param message The message to check and potentially perform automod actions to
*/
- public constructor(message: Message) {
- this.message = message;
+ public constructor(
+ /**
+ * The message to check for blacklisted phrases on
+ */
+ private message: Message
+ ) {
if (message.author.id === client.user?.id) return;
void this.handle();
}
@@ -57,9 +56,9 @@ export class AutoMod {
traditional: {
if (this.isImmune) break traditional;
- const badLinksArray = util.getShared('badLinks');
- const badLinksSecretArray = util.getShared('badLinksSecret');
- const badWordsRaw = util.getShared('badWords');
+ const badLinksArray = getShared('badLinks');
+ const badLinksSecretArray = getShared('badLinksSecret');
+ const badWordsRaw = getShared('badWords');
const customAutomodPhrases = (await this.message.guild.getSetting('autoModPhases')) ?? [];
const uniqueLinks = [...new Set([...badLinksArray, ...badLinksSecretArray])];
@@ -90,8 +89,8 @@ export class AutoMod {
embeds: [
{
title: 'AutoMod Error',
- description: `Unable to find severity information for ${util.format.inlineCode(highestOffence.match)}`,
- color: util.colors.error
+ description: `Unable to find severity information for ${format.inlineCode(highestOffence.match)}`,
+ color: colors.error
}
]
});
@@ -168,7 +167,7 @@ export class AutoMod {
.setDescription(
`**User:** ${this.message.author} (${this.message.author.tag})\n**Sent From:** <#${this.message.channel.id}> [Jump to context](${this.message.url})`
)
- .addFields([{ name: 'Message Content', value: `${await util.codeblock(this.message.content, 1024)}` }])
+ .addFields([{ name: 'Message Content', value: `${await codeblock(this.message.content, 1024)}` }])
.setColor(color)
.setTimestamp()
],
@@ -252,13 +251,13 @@ export class AutoMod {
let color;
switch (highestOffence.severity) {
case Severity.DELETE: {
- color = util.colors.lightGray;
+ color = colors.lightGray;
void this.message.delete().catch((e) => deleteError.bind(this, e));
this.punished = true;
break;
}
case Severity.WARN: {
- color = util.colors.yellow;
+ color = colors.yellow;
void this.message.delete().catch((e) => deleteError.bind(this, e));
void this.message.member?.bushWarn({
moderator: this.message.guild!.members.me!,
@@ -268,7 +267,7 @@ export class AutoMod {
break;
}
case Severity.TEMP_MUTE: {
- color = util.colors.orange;
+ color = colors.orange;
void this.message.delete().catch((e) => deleteError.bind(this, e));
void this.message.member?.bushMute({
moderator: this.message.guild!.members.me!,
@@ -279,7 +278,7 @@ export class AutoMod {
break;
}
case Severity.PERM_MUTE: {
- color = util.colors.red;
+ color = colors.red;
void this.message.delete().catch((e) => deleteError.bind(this, e));
void this.message.member?.bushMute({
moderator: this.message.guild!.members.me!,
@@ -302,8 +301,8 @@ export class AutoMod {
{
title: 'AutoMod Error',
description: `Unable to delete triggered message.`,
- fields: [{ name: 'Error', value: await util.codeblock(`${util.formatError(e)}`, 1024, 'js', true) }],
- color: util.colors.error
+ fields: [{ name: 'Error', value: await codeblock(`${formatError(e)}`, 1024, 'js', true) }],
+ color: colors.error
}
]
});
@@ -333,7 +332,7 @@ export class AutoMod {
this.message.channel.id
}> [Jump to context](${this.message.url})\n**Blacklisted Words:** ${offences.map((o) => `\`${o.match}\``).join(', ')}`
)
- .addFields([{ name: 'Message Content', value: `${await util.codeblock(this.message.content, 1024)}` }])
+ .addFields([{ name: 'Message Content', value: `${await codeblock(this.message.content, 1024)}` }])
.setColor(color)
.setTimestamp()
.setAuthor({ name: this.message.author.tag, url: this.message.author.displayAvatarURL() })
@@ -360,7 +359,7 @@ export class AutoMod {
public static async handleInteraction(interaction: ButtonInteraction) {
if (!interaction.memberPermissions?.has(PermissionFlagsBits.BanMembers))
return interaction.reply({
- content: `${util.emojis.error} You are missing the **Ban Members** permission.`,
+ content: `${emojis.error} You are missing the **Ban Members** permission.`,
ephemeral: true
});
const [action, userId, reason] = interaction.customId.replace('automod;', '').split(';');
@@ -387,20 +386,20 @@ export class AutoMod {
evidence: (interaction.message as Message).url ?? undefined
});
- const victimUserFormatted = (await util.resolveNonCachedUser(userId))?.tag ?? userId;
+ const victimUserFormatted = (await resolveNonCachedUser(userId))?.tag ?? userId;
if (result === banResponse.SUCCESS)
return interaction.reply({
- content: `${util.emojis.success} Successfully banned **${victimUserFormatted}**.`,
+ content: `${emojis.success} Successfully banned **${victimUserFormatted}**.`,
ephemeral: true
});
else if (result === banResponse.DM_ERROR)
return interaction.reply({
- content: `${util.emojis.warn} Banned ${victimUserFormatted} however I could not send them a dm.`,
+ content: `${emojis.warn} Banned ${victimUserFormatted} however I could not send them a dm.`,
ephemeral: true
});
else
return interaction.reply({
- content: `${util.emojis.error} Could not ban **${victimUserFormatted}**: \`${result}\` .`,
+ content: `${emojis.error} Could not ban **${victimUserFormatted}**: \`${result}\` .`,
ephemeral: true
});
}
diff --git a/src/lib/common/ButtonPaginator.ts b/src/lib/common/ButtonPaginator.ts
index 64870cf..9560247 100644
--- a/src/lib/common/ButtonPaginator.ts
+++ b/src/lib/common/ButtonPaginator.ts
@@ -15,26 +15,6 @@ import {
*/
export class ButtonPaginator {
/**
- * The message that triggered the command
- */
- protected message: CommandMessage | SlashMessage;
-
- /**
- * The embeds to paginate
- */
- protected embeds: EmbedBuilder[] | APIEmbed[];
-
- /**
- * The optional text to send with the paginator
- */
- protected text: string | null;
-
- /**
- * Whether the paginator message gets deleted when the exit button is pressed
- */
- protected deleteOnExit: boolean;
-
- /**
* The current page of the paginator
*/
protected curPage: number;
@@ -52,16 +32,27 @@ export class ButtonPaginator {
* @param startOn The page to start from (**not** the index)
*/
protected constructor(
- message: CommandMessage | SlashMessage,
- embeds: EmbedBuilder[] | APIEmbed[],
- text: string | null,
- deleteOnExit: boolean,
+ /**
+ * The message that triggered the command
+ */
+ protected message: CommandMessage | SlashMessage,
+
+ /**
+ * The embeds to paginate
+ */
+ protected embeds: EmbedBuilder[] | APIEmbed[],
+
+ /**
+ * The optional text to send with the paginator
+ */
+ protected text: string | null,
+
+ /**
+ * Whether the paginator message gets deleted when the exit button is pressed
+ */
+ protected deleteOnExit: boolean,
startOn: number
) {
- this.message = message;
- this.embeds = embeds;
- this.text = text ? text : null;
- this.deleteOnExit = deleteOnExit;
this.curPage = startOn - 1;
// add footers
diff --git a/src/lib/common/ConfirmationPrompt.ts b/src/lib/common/ConfirmationPrompt.ts
index c95dbbc..4593d24 100644
--- a/src/lib/common/ConfirmationPrompt.ts
+++ b/src/lib/common/ConfirmationPrompt.ts
@@ -6,23 +6,20 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, type MessageComponentInte
*/
export class ConfirmationPrompt {
/**
- * Options for sending the message
- */
- protected messageOptions: MessageOptions;
-
- /**
- * The message that triggered the command
- */
- protected message: CommandMessage | SlashMessage;
-
- /**
* @param message The message to respond to
- * @param options The send message options
+ * @param messageOptions The send message options
*/
- protected constructor(message: CommandMessage | SlashMessage, messageOptions: MessageOptions) {
- this.message = message;
- this.messageOptions = messageOptions;
- }
+ protected constructor(
+ /**
+ * The message that triggered the command
+ */
+ protected message: CommandMessage | SlashMessage,
+
+ /**
+ * Options for sending the message
+ */
+ protected messageOptions: MessageOptions
+ ) {}
/**
* Sends a message with buttons for the user to confirm or cancel the action.
diff --git a/src/lib/common/DeleteButton.ts b/src/lib/common/DeleteButton.ts
index 91f4bfa..b561d94 100644
--- a/src/lib/common/DeleteButton.ts
+++ b/src/lib/common/DeleteButton.ts
@@ -15,23 +15,20 @@ import {
*/
export class DeleteButton {
/**
- * Options for sending the message
- */
- protected messageOptions: MessageOptions;
-
- /**
- * The message that triggered the command
- */
- protected message: CommandMessage | SlashMessage;
-
- /**
* @param message The message to respond to
- * @param options The send message options
+ * @param messageOptions The send message options
*/
- protected constructor(message: CommandMessage | SlashMessage, options: MessageOptions) {
- this.message = message;
- this.messageOptions = options;
- }
+ protected constructor(
+ /**
+ * The message that triggered the command
+ */
+ protected message: CommandMessage | SlashMessage,
+
+ /**
+ * Options for sending the message
+ */
+ protected messageOptions: MessageOptions
+ ) {}
/**
* Sends a message with a button for the user to delete it.
diff --git a/src/lib/common/HighlightManager.ts b/src/lib/common/HighlightManager.ts
index fdec322..caaa6a5 100644
--- a/src/lib/common/HighlightManager.ts
+++ b/src/lib/common/HighlightManager.ts
@@ -1,7 +1,7 @@
-import { Highlight, type HighlightWord } from '#lib';
+import { addToArray, format, Highlight, removeFromArray, timestamp, type HighlightWord } from '#lib';
import assert from 'assert';
import { Collection, type Message, type Snowflake } from 'discord.js';
-import { Time } from '../utils/BushConstants.js';
+import { colors, Time } from '../utils/BushConstants.js';
const NOTIFY_COOLDOWN = 5 * Time.Minute;
const OWNER_NOTIFY_COOLDOWN = 1 * Time.Minute;
@@ -162,7 +162,7 @@ export class HighlightManager {
if (highlight.words.some((w) => w.word === hl.word)) return `You have already highlighted "${hl.word}".`;
- highlight.words = util.addToArray(highlight.words, hl);
+ highlight.words = addToArray(highlight.words, hl);
return Boolean(await highlight.save().catch(() => false));
}
@@ -189,7 +189,7 @@ export class HighlightManager {
const toRemove = highlight.words.find((w) => w.word === hl);
if (!toRemove) return `Uhhhhh... This shouldn't happen.`;
- highlight.words = util.removeFromArray(highlight.words, toRemove);
+ highlight.words = removeFromArray(highlight.words, toRemove);
return Boolean(await highlight.save().catch(() => false));
}
@@ -271,20 +271,18 @@ export class HighlightManager {
return client.users
.send(user, {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
- content: `In ${util.format.input(message.guild.name)} ${message.channel}, your highlight "${hl.word}" was matched:`,
+ content: `In ${format.input(message.guild.name)} ${message.channel}, your highlight "${hl.word}" was matched:`,
embeds: [
{
description: [...recentMessages, message]
.map(
(m) =>
- `${util.timestamp(m.createdAt, 't')} ${util.format.input(`${m.author.tag}:`)} ${m.cleanContent
- .trim()
- .substring(0, 512)}`
+ `${timestamp(m.createdAt, 't')} ${format.input(`${m.author.tag}:`)} ${m.cleanContent.trim().substring(0, 512)}`
)
.join('\n'),
author: { name: hl.regex ? `/${hl.word}/gi` : hl.word },
fields: [{ name: 'Source message', value: `[Jump to message](${message.url})` }],
- color: util.colors.default,
+ color: colors.default,
footer: { text: 'Triggered' },
timestamp: message.createdAt.toISOString()
}
diff --git a/src/lib/common/Sentry.ts b/src/lib/common/Sentry.ts
index e18555b..34bc06f 100644
--- a/src/lib/common/Sentry.ts
+++ b/src/lib/common/Sentry.ts
@@ -1,7 +1,7 @@
import { RewriteFrames } from '@sentry/integrations';
import * as SentryNode from '@sentry/node';
import { Integrations } from '@sentry/node';
-import config from './../../config/options.js';
+import config from '../../../config/options.js';
export class Sentry {
public constructor(rootdir: string) {
diff --git a/src/lib/common/util/Arg.ts b/src/lib/common/util/Arg.ts
index 51d8065..a7795b1 100644
--- a/src/lib/common/util/Arg.ts
+++ b/src/lib/common/util/Arg.ts
@@ -9,155 +9,150 @@ import { Argument, type Flag, type ParsedValuePredicate } from 'discord-akairo';
import { type Message } from 'discord.js';
/**
- * A wrapper for the {@link Argument} class that adds custom typings.
+ * Casts a phrase to this argument's type.
+ * @param type - The type to cast to.
+ * @param message - Message that called the command.
+ * @param phrase - Phrase to process.
*/
-export class Arg {
- /**
- * Casts a phrase to this argument's type.
- * @param type - The type to cast to.
- * @param message - Message that called the command.
- * @param phrase - Phrase to process.
- */
- public static async cast<T extends ATC>(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise<ATCR<T>>;
- public static async cast<T extends KBAT>(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise<BAT[T]>;
- public static async cast(type: AT | ATC, message: CommandMessage | SlashMessage, phrase: string): Promise<any>;
- public static async cast(type: ATC | AT, message: CommandMessage | SlashMessage, phrase: string): Promise<any> {
- return Argument.cast(type as any, client.commandHandler.resolver, message as Message, phrase);
- }
+export async function cast<T extends ATC>(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise<ATCR<T>>;
+export async function cast<T extends KBAT>(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise<BAT[T]>;
+export async function cast(type: AT | ATC, message: CommandMessage | SlashMessage, phrase: string): Promise<any>;
+export async function cast(type: ATC | AT, message: CommandMessage | SlashMessage, phrase: string): Promise<any> {
+ return Argument.cast(type as any, client.commandHandler.resolver, message as Message, phrase);
+}
- /**
- * Creates a type that is the left-to-right composition of the given types.
- * If any of the types fails, the entire composition fails.
- * @param types - Types to use.
- */
- public static compose<T extends ATC>(...types: T[]): ATCATCR<T>;
- public static compose<T extends KBAT>(...types: T[]): ATCBAT<T>;
- public static compose(...types: (AT | ATC)[]): ATC;
- public static compose(...types: (AT | ATC)[]): ATC {
- return Argument.compose(...(types as any));
- }
+/**
+ * Creates a type that is the left-to-right composition of the given types.
+ * If any of the types fails, the entire composition fails.
+ * @param types - Types to use.
+ */
+export function compose<T extends ATC>(...types: T[]): ATCATCR<T>;
+export function compose<T extends KBAT>(...types: T[]): ATCBAT<T>;
+export function compose(...types: (AT | ATC)[]): ATC;
+export function compose(...types: (AT | ATC)[]): ATC {
+ return Argument.compose(...(types as any));
+}
- /**
- * Creates a type that is the left-to-right composition of the given types.
- * If any of the types fails, the composition still continues with the failure passed on.
- * @param types - Types to use.
- */
- public static composeWithFailure<T extends ATC>(...types: T[]): ATCATCR<T>;
- public static composeWithFailure<T extends KBAT>(...types: T[]): ATCBAT<T>;
- public static composeWithFailure(...types: (AT | ATC)[]): ATC;
- public static composeWithFailure(...types: (AT | ATC)[]): ATC {
- return Argument.composeWithFailure(...(types as any));
- }
+/**
+ * Creates a type that is the left-to-right composition of the given types.
+ * If any of the types fails, the composition still continues with the failure passed on.
+ * @param types - Types to use.
+ */
+export function composeWithFailure<T extends ATC>(...types: T[]): ATCATCR<T>;
+export function composeWithFailure<T extends KBAT>(...types: T[]): ATCBAT<T>;
+export function composeWithFailure(...types: (AT | ATC)[]): ATC;
+export function composeWithFailure(...types: (AT | ATC)[]): ATC {
+ return Argument.composeWithFailure(...(types as any));
+}
- /**
- * Checks if something is null, undefined, or a fail flag.
- * @param value - Value to check.
- */
- public static isFailure(value: any): value is null | undefined | (Flag & { value: any }) {
- return Argument.isFailure(value);
- }
+/**
+ * Checks if something is null, undefined, or a fail flag.
+ * @param value - Value to check.
+ */
+export function isFailure(value: any): value is null | undefined | (Flag & { value: any }) {
+ return Argument.isFailure(value);
+}
- /**
- * Creates a type from multiple types (product type).
- * Only inputs where each type resolves with a non-void value are valid.
- * @param types - Types to use.
- */
- public static product<T extends ATC>(...types: T[]): ATCATCR<T>;
- public static product<T extends KBAT>(...types: T[]): ATCBAT<T>;
- public static product(...types: (AT | ATC)[]): ATC;
- public static product(...types: (AT | ATC)[]): ATC {
- return Argument.product(...(types as any));
- }
+/**
+ * Creates a type from multiple types (product type).
+ * Only inputs where each type resolves with a non-void value are valid.
+ * @param types - Types to use.
+ */
+export function product<T extends ATC>(...types: T[]): ATCATCR<T>;
+export function product<T extends KBAT>(...types: T[]): ATCBAT<T>;
+export function product(...types: (AT | ATC)[]): ATC;
+export function product(...types: (AT | ATC)[]): ATC {
+ return Argument.product(...(types as any));
+}
- /**
- * Creates a type where the parsed value must be within a range.
- * @param type - The type to use.
- * @param min - Minimum value.
- * @param max - Maximum value.
- * @param inclusive - Whether or not to be inclusive on the upper bound.
- */
- public static range<T extends ATC>(type: T, min: number, max: number, inclusive?: boolean): ATCATCR<T>;
- public static range<T extends KBAT>(type: T, min: number, max: number, inclusive?: boolean): ATCBAT<T>;
- public static range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC;
- public static range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC {
- return Argument.range(type as any, min, max, inclusive);
- }
+/**
+ * Creates a type where the parsed value must be within a range.
+ * @param type - The type to use.
+ * @param min - Minimum value.
+ * @param max - Maximum value.
+ * @param inclusive - Whether or not to be inclusive on the upper bound.
+ */
+export function range<T extends ATC>(type: T, min: number, max: number, inclusive?: boolean): ATCATCR<T>;
+export function range<T extends KBAT>(type: T, min: number, max: number, inclusive?: boolean): ATCBAT<T>;
+export function range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC;
+export function range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC {
+ return Argument.range(type as any, min, max, inclusive);
+}
- /**
- * Creates a type that parses as normal but also tags it with some data.
- * Result is in an object `{ tag, value }` and wrapped in `Flag.fail` when failed.
- * @param type - The type to use.
- * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string.
- */
- public static tagged<T extends ATC>(type: T, tag?: any): ATCATCR<T>;
- public static tagged<T extends KBAT>(type: T, tag?: any): ATCBAT<T>;
- public static tagged(type: AT | ATC, tag?: any): ATC;
- public static tagged(type: AT | ATC, tag?: any): ATC {
- return Argument.tagged(type as any, tag);
- }
+/**
+ * Creates a type that parses as normal but also tags it with some data.
+ * Result is in an object `{ tag, value }` and wrapped in `Flag.fail` when failed.
+ * @param type - The type to use.
+ * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string.
+ */
+export function tagged<T extends ATC>(type: T, tag?: any): ATCATCR<T>;
+export function tagged<T extends KBAT>(type: T, tag?: any): ATCBAT<T>;
+export function tagged(type: AT | ATC, tag?: any): ATC;
+export function tagged(type: AT | ATC, tag?: any): ATC {
+ return Argument.tagged(type as any, tag);
+}
- /**
- * Creates a type from multiple types (union type).
- * The first type that resolves to a non-void value is used.
- * Each type will also be tagged using `tagged` with themselves.
- * @param types - Types to use.
- */
- public static taggedUnion<T extends ATC>(...types: T[]): ATCATCR<T>;
- public static taggedUnion<T extends KBAT>(...types: T[]): ATCBAT<T>;
- public static taggedUnion(...types: (AT | ATC)[]): ATC;
- public static taggedUnion(...types: (AT | ATC)[]): ATC {
- return Argument.taggedUnion(...(types as any));
- }
+/**
+ * Creates a type from multiple types (union type).
+ * The first type that resolves to a non-void value is used.
+ * Each type will also be tagged using `tagged` with themselves.
+ * @param types - Types to use.
+ */
+export function taggedUnion<T extends ATC>(...types: T[]): ATCATCR<T>;
+export function taggedUnion<T extends KBAT>(...types: T[]): ATCBAT<T>;
+export function taggedUnion(...types: (AT | ATC)[]): ATC;
+export function taggedUnion(...types: (AT | ATC)[]): ATC {
+ return Argument.taggedUnion(...(types as any));
+}
- /**
- * Creates a type that parses as normal but also tags it with some data and carries the original input.
- * Result is in an object `{ tag, input, value }` and wrapped in `Flag.fail` when failed.
- * @param type - The type to use.
- * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string.
- */
- public static taggedWithInput<T extends ATC>(type: T, tag?: any): ATCATCR<T>;
- public static taggedWithInput<T extends KBAT>(type: T, tag?: any): ATCBAT<T>;
- public static taggedWithInput(type: AT | ATC, tag?: any): ATC;
- public static taggedWithInput(type: AT | ATC, tag?: any): ATC {
- return Argument.taggedWithInput(type as any, tag);
- }
+/**
+ * Creates a type that parses as normal but also tags it with some data and carries the original input.
+ * Result is in an object `{ tag, input, value }` and wrapped in `Flag.fail` when failed.
+ * @param type - The type to use.
+ * @param tag - Tag to add. Defaults to the `type` argument, so useful if it is a string.
+ */
+export function taggedWithInput<T extends ATC>(type: T, tag?: any): ATCATCR<T>;
+export function taggedWithInput<T extends KBAT>(type: T, tag?: any): ATCBAT<T>;
+export function taggedWithInput(type: AT | ATC, tag?: any): ATC;
+export function taggedWithInput(type: AT | ATC, tag?: any): ATC {
+ return Argument.taggedWithInput(type as any, tag);
+}
- /**
- * Creates a type from multiple types (union type).
- * The first type that resolves to a non-void value is used.
- * @param types - Types to use.
- */
- public static union<T extends ATC>(...types: T[]): ATCATCR<T>;
- public static union<T extends KBAT>(...types: T[]): ATCBAT<T>;
- public static union(...types: (AT | ATC)[]): ATC;
- public static union(...types: (AT | ATC)[]): ATC {
- return Argument.union(...(types as any));
- }
+/**
+ * Creates a type from multiple types (union type).
+ * The first type that resolves to a non-void value is used.
+ * @param types - Types to use.
+ */
+export function union<T extends ATC>(...types: T[]): ATCATCR<T>;
+export function union<T extends KBAT>(...types: T[]): ATCBAT<T>;
+export function union(...types: (AT | ATC)[]): ATC;
+export function union(...types: (AT | ATC)[]): ATC {
+ return Argument.union(...(types as any));
+}
- /**
- * Creates a type with extra validation.
- * If the predicate is not true, the value is considered invalid.
- * @param type - The type to use.
- * @param predicate - The predicate function.
- */
- public static validate<T extends ATC>(type: T, predicate: ParsedValuePredicate): ATCATCR<T>;
- public static validate<T extends KBAT>(type: T, predicate: ParsedValuePredicate): ATCBAT<T>;
- public static validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC;
- public static validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC {
- return Argument.validate(type as any, predicate);
- }
+/**
+ * Creates a type with extra validation.
+ * If the predicate is not true, the value is considered invalid.
+ * @param type - The type to use.
+ * @param predicate - The predicate function.
+ */
+export function validate<T extends ATC>(type: T, predicate: ParsedValuePredicate): ATCATCR<T>;
+export function validate<T extends KBAT>(type: T, predicate: ParsedValuePredicate): ATCBAT<T>;
+export function validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC;
+export function validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC {
+ return Argument.validate(type as any, predicate);
+}
- /**
- * Creates a type that parses as normal but also carries the original input.
- * Result is in an object `{ input, value }` and wrapped in `Flag.fail` when failed.
- * @param type - The type to use.
- */
- public static withInput<T extends ATC>(type: T): ATC<ATCR<T>>;
- public static withInput<T extends KBAT>(type: T): ATCBAT<T>;
- public static withInput(type: AT | ATC): ATC;
- public static withInput(type: AT | ATC): ATC {
- return Argument.withInput(type as any);
- }
+/**
+ * Creates a type that parses as normal but also carries the original input.
+ * Result is in an object `{ input, value }` and wrapped in `Flag.fail` when failed.
+ * @param type - The type to use.
+ */
+export function withInput<T extends ATC>(type: T): ATC<ATCR<T>>;
+export function withInput<T extends KBAT>(type: T): ATCBAT<T>;
+export function withInput(type: AT | ATC): ATC;
+export function withInput(type: AT | ATC): ATC {
+ return Argument.withInput(type as any);
}
type BushArgumentTypeCasterReturn<R> = R extends BushArgumentTypeCaster<infer S> ? S : R;
diff --git a/src/lib/common/util/Format.ts b/src/lib/common/util/Format.ts
index 6cb6edc..260a0be 100644
--- a/src/lib/common/util/Format.ts
+++ b/src/lib/common/util/Format.ts
@@ -1,107 +1,112 @@
import { type CodeBlockLang } from '#lib';
-import { EscapeMarkdownOptions, Formatters, Util } from 'discord.js';
+import {
+ escapeBold,
+ escapeCodeBlock,
+ escapeInlineCode,
+ escapeItalic,
+ EscapeMarkdownOptions,
+ escapeSpoiler,
+ escapeStrikethrough,
+ escapeUnderline,
+ Formatters
+} from 'discord.js';
/**
- * Formats and escapes content for formatting
+ * Wraps the content inside a codeblock with no language.
+ * @param content The content to wrap.
*/
-export class Format {
- /**
- * Wraps the content inside a codeblock with no language.
- * @param content The content to wrap.
- */
- public static codeBlock(content: string): string;
+export function codeBlock(content: string): string;
- /**
- * Wraps the content inside a codeblock with the specified language.
- * @param language The language for the codeblock.
- * @param content The content to wrap.
- */
- public static codeBlock(language: CodeBlockLang, content: string): string;
- public static codeBlock(languageOrContent: string, content?: string): string {
- return typeof content === 'undefined'
- ? Formatters.codeBlock(Util.escapeCodeBlock(`${languageOrContent}`))
- : Formatters.codeBlock(`${languageOrContent}`, Util.escapeCodeBlock(`${content}`));
- }
+/**
+ * Wraps the content inside a codeblock with the specified language.
+ * @param language The language for the codeblock.
+ * @param content The content to wrap.
+ */
+export function codeBlock(language: CodeBlockLang, content: string): string;
+export function codeBlock(languageOrContent: string, content?: string): string {
+ return typeof content === 'undefined'
+ ? Formatters.codeBlock(escapeCodeBlock(`${languageOrContent}`))
+ : Formatters.codeBlock(`${languageOrContent}`, escapeCodeBlock(`${content}`));
+}
- /**
- * Wraps the content inside \`backticks\`, which formats it as inline code.
- * @param content The content to wrap.
- */
- public static inlineCode(content: string): string {
- return Formatters.inlineCode(Util.escapeInlineCode(`${content}`));
- }
+/**
+ * Wraps the content inside \`backticks\`, which formats it as inline code.
+ * @param content The content to wrap.
+ */
+export function inlineCode(content: string): string {
+ return Formatters.inlineCode(escapeInlineCode(`${content}`));
+}
- /**
- * Formats the content into italic text.
- * @param content The content to wrap.
- */
- public static italic(content: string): string {
- return Formatters.italic(Util.escapeItalic(`${content}`));
- }
+/**
+ * Formats the content into italic text.
+ * @param content The content to wrap.
+ */
+export function italic(content: string): string {
+ return Formatters.italic(escapeItalic(`${content}`));
+}
- /**
- * Formats the content into bold text.
- * @param content The content to wrap.
- */
- public static bold(content: string): string {
- return Formatters.bold(Util.escapeBold(`${content}`));
- }
+/**
+ * Formats the content into bold text.
+ * @param content The content to wrap.
+ */
+export function bold(content: string): string {
+ return Formatters.bold(escapeBold(`${content}`));
+}
- /**
- * Formats the content into underscored text.
- * @param content The content to wrap.
- */
- public static underscore(content: string): string {
- return Formatters.underscore(Util.escapeUnderline(`${content}`));
- }
+/**
+ * Formats the content into underscored text.
+ * @param content The content to wrap.
+ */
+export function underscore(content: string): string {
+ return Formatters.underscore(escapeUnderline(`${content}`));
+}
- /**
- * Formats the content into strike-through text.
- * @param content The content to wrap.
- */
- public static strikethrough(content: string): string {
- return Formatters.strikethrough(Util.escapeStrikethrough(`${content}`));
- }
+/**
+ * Formats the content into strike-through text.
+ * @param content The content to wrap.
+ */
+export function strikethrough(content: string): string {
+ return Formatters.strikethrough(escapeStrikethrough(`${content}`));
+}
- /**
- * Wraps the content inside spoiler (hidden text).
- * @param content The content to wrap.
- */
- public static spoiler(content: string): string {
- return Formatters.spoiler(Util.escapeSpoiler(`${content}`));
- }
+/**
+ * Wraps the content inside spoiler (hidden text).
+ * @param content The content to wrap.
+ */
+export function spoiler(content: string): string {
+ return Formatters.spoiler(escapeSpoiler(`${content}`));
+}
- /**
- * Escapes any Discord-flavour markdown in a string.
- * @param text Content to escape
- * @param options Options for escaping the markdown
- */
- public static escapeMarkdown(text: string, options?: EscapeMarkdownOptions): string {
- return Util.escapeMarkdown(`${text}`, options);
- }
+/**
+ * Escapes any Discord-flavour markdown in a string.
+ * @param text Content to escape
+ * @param options Options for escaping the markdown
+ */
+export function escapeMarkdown(text: string, options?: EscapeMarkdownOptions): string {
+ return escapeMarkdown(`${text}`, options);
+}
- /**
- * Formats input: makes it bold and escapes any other markdown
- * @param text The input
- */
- public static input(text: string): string {
- return this.bold(this.escapeMarkdown(this.sanitizeWtlAndControl(`${text}`)));
- }
+/**
+ * Formats input: makes it bold and escapes any other markdown
+ * @param text The input
+ */
+export function input(text: string): string {
+ return bold(escapeMarkdown(sanitizeWtlAndControl(`${text}`)));
+}
- /**
- * Formats input for logs: makes it highlighted
- * @param text The input
- */
- public static inputLog(text: string): string {
- return `<<${this.sanitizeWtlAndControl(`${text}`)}>>`;
- }
+/**
+ * Formats input for logs: makes it highlighted
+ * @param text The input
+ */
+export function inputLog(text: string): string {
+ return `<<${sanitizeWtlAndControl(`${text}`)}>>`;
+}
- /**
- * Removes all characters in a string that are either control characters or change the direction of text etc.
- * @param str The string you would like sanitized
- */
- public static sanitizeWtlAndControl(str: string) {
- // eslint-disable-next-line no-control-regex
- return `${str}`.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, '');
- }
+/**
+ * Removes all characters in a string that are either control characters or change the direction of text etc.
+ * @param str The string you would like sanitized
+ */
+export function sanitizeWtlAndControl(str: string) {
+ // eslint-disable-next-line no-control-regex
+ return `${str}`.replace(/[\u0000-\u001F\u007F-\u009F\u200B]/g, '');
}
diff --git a/src/lib/common/util/Moderation.ts b/src/lib/common/util/Moderation.ts
index 6cdc141..a08dfa4 100644
--- a/src/lib/common/util/Moderation.ts
+++ b/src/lib/common/util/Moderation.ts
@@ -1,4 +1,16 @@
-import { ActivePunishment, ActivePunishmentType, Guild as GuildDB, ModLog, type ModLogType } from '#lib';
+import {
+ ActivePunishment,
+ ActivePunishmentType,
+ colors,
+ emojis,
+ format,
+ Guild as GuildDB,
+ handleError,
+ humanizeDuration,
+ ModLog,
+ resolveNonCachedUser,
+ type ModLogType
+} from '#lib';
import assert from 'assert';
import {
ActionRowBuilder,
@@ -40,275 +52,270 @@ enum reversedPunishMap {
}
/**
- * A utility class with moderation-related methods.
+ * Checks if a moderator can perform a moderation action on another user.
+ * @param moderator The person trying to perform the action.
+ * @param victim The person getting punished.
+ * @param type The type of punishment - used to format the response.
+ * @param checkModerator Whether or not to check if the victim is a moderator.
+ * @param force Override permissions checks.
+ * @returns `true` if the moderator can perform the action otherwise a reason why they can't.
*/
-export class Moderation {
- /**
- * Checks if a moderator can perform a moderation action on another user.
- * @param moderator The person trying to perform the action.
- * @param victim The person getting punished.
- * @param type The type of punishment - used to format the response.
- * @param checkModerator Whether or not to check if the victim is a moderator.
- * @param force Override permissions checks.
- * @returns `true` if the moderator can perform the action otherwise a reason why they can't.
- */
- public static async permissionCheck(
- moderator: GuildMember,
- victim: GuildMember,
- type:
- | 'mute'
- | 'unmute'
- | 'warn'
- | 'kick'
- | 'ban'
- | 'unban'
- | 'add a punishment role to'
- | 'remove a punishment role from'
- | 'block'
- | 'unblock'
- | 'timeout'
- | 'untimeout',
- checkModerator = true,
- force = false
- ): Promise<true | string> {
- if (force) return true;
-
- // If the victim is not in the guild anymore it will be undefined
- if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true;
-
- if (moderator.guild.id !== victim.guild.id) {
- throw new Error('moderator and victim not in same guild');
- }
-
- const isOwner = moderator.guild.ownerId === moderator.id;
- if (moderator.id === victim.id && !type.startsWith('un')) {
- return `${util.emojis.error} You cannot ${type} yourself.`;
- }
- if (
- moderator.roles.highest.position <= victim.roles.highest.position &&
- !isOwner &&
- !(type.startsWith('un') && moderator.id === victim.id)
- ) {
- return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`;
- }
- if (
- victim.roles.highest.position >= victim.guild.members.me!.roles.highest.position &&
- !(type.startsWith('un') && moderator.id === victim.id)
- ) {
- return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`;
- }
- if (
- checkModerator &&
- victim.permissions.has(PermissionFlagsBits.ManageMessages) &&
- !(type.startsWith('un') && moderator.id === victim.id)
- ) {
- if (await moderator.guild.hasFeature('modsCanPunishMods')) {
- return true;
- } else {
- return `${util.emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`;
- }
- }
- return true;
+export async function permissionCheck(
+ moderator: GuildMember,
+ victim: GuildMember,
+ type:
+ | 'mute'
+ | 'unmute'
+ | 'warn'
+ | 'kick'
+ | 'ban'
+ | 'unban'
+ | 'add a punishment role to'
+ | 'remove a punishment role from'
+ | 'block'
+ | 'unblock'
+ | 'timeout'
+ | 'untimeout',
+ checkModerator = true,
+ force = false
+): Promise<true | string> {
+ if (force) return true;
+
+ // If the victim is not in the guild anymore it will be undefined
+ if ((!victim || !victim.guild) && !['ban', 'unban'].includes(type)) return true;
+
+ if (moderator.guild.id !== victim.guild.id) {
+ throw new Error('moderator and victim not in same guild');
}
- /**
- * Creates a modlog entry for a punishment.
- * @param options Options for creating a modlog entry.
- * @param getCaseNumber Whether or not to get the case number of the entry.
- * @returns An object with the modlog and the case number.
- */
- public static async createModLogEntry(
- options: CreateModLogEntryOptions,
- getCaseNumber = false
- ): Promise<{ log: ModLog | null; caseNum: number | null }> {
- const user = (await util.resolveNonCachedUser(options.user))!.id;
- const moderator = (await util.resolveNonCachedUser(options.moderator))!.id;
- const guild = client.guilds.resolveId(options.guild)!;
-
- return this.createModLogEntrySimple(
- {
- ...options,
- user: user,
- moderator: moderator,
- guild: guild
- },
- getCaseNumber
- );
+ const isOwner = moderator.guild.ownerId === moderator.id;
+ if (moderator.id === victim.id && !type.startsWith('un')) {
+ return `${emojis.error} You cannot ${type} yourself.`;
}
-
- /**
- * Creates a modlog entry with already resolved ids.
- * @param options Options for creating a modlog entry.
- * @param getCaseNumber Whether or not to get the case number of the entry.
- * @returns An object with the modlog and the case number.
- */
- public static async createModLogEntrySimple(
- options: SimpleCreateModLogEntryOptions,
- getCaseNumber = false
- ): Promise<{ log: ModLog | null; caseNum: number | null }> {
- // If guild does not exist create it so the modlog can reference a guild.
- await GuildDB.findOrCreate({
- where: { id: options.guild },
- defaults: { id: options.guild }
- });
-
- const modLogEntry = ModLog.build({
- type: options.type,
- user: options.user,
- moderator: options.moderator,
- reason: options.reason,
- duration: options.duration ? options.duration : undefined,
- guild: options.guild,
- pseudo: options.pseudo ?? false,
- evidence: options.evidence,
- hidden: options.hidden ?? false
- });
- const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => {
- await util.handleError('createModLogEntry', e);
- return null;
- });
-
- if (!getCaseNumber) return { log: saveResult, caseNum: null };
-
- const caseNum = (
- await ModLog.findAll({ where: { type: options.type, user: options.user, guild: options.guild, hidden: false } })
- )?.length;
- return { log: saveResult, caseNum };
+ if (
+ moderator.roles.highest.position <= victim.roles.highest.position &&
+ !isOwner &&
+ !(type.startsWith('un') && moderator.id === victim.id)
+ ) {
+ return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as you do.`;
}
-
- /**
- * Creates a punishment entry.
- * @param options Options for creating the punishment entry.
- * @returns The database entry, or null if no entry is created.
- */
- public static async createPunishmentEntry(options: CreatePunishmentEntryOptions): Promise<ActivePunishment | null> {
- const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined;
- const user = (await util.resolveNonCachedUser(options.user))!.id;
- const guild = client.guilds.resolveId(options.guild)!;
- const type = this.findTypeEnum(options.type)!;
-
- const entry = ActivePunishment.build(
- options.extraInfo
- ? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo }
- : { user, type, guild, expires, modlog: options.modlog }
- );
- return await entry.save().catch(async (e) => {
- await util.handleError('createPunishmentEntry', e);
- return null;
- });
+ if (
+ victim.roles.highest.position >= victim.guild.members.me!.roles.highest.position &&
+ !(type.startsWith('un') && moderator.id === victim.id)
+ ) {
+ return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they have higher or equal role hierarchy as I do.`;
}
-
- /**
- * Destroys a punishment entry.
- * @param options Options for destroying the punishment entry.
- * @returns Whether or not the entry was destroyed.
- */
- public static async removePunishmentEntry(options: RemovePunishmentEntryOptions): Promise<boolean> {
- const user = await util.resolveNonCachedUser(options.user);
- const guild = client.guilds.resolveId(options.guild);
- const type = this.findTypeEnum(options.type);
-
- if (!user || !guild) return false;
-
- let success = true;
-
- const entries = await ActivePunishment.findAll({
- // finding all cases of a certain type incase there were duplicates or something
- where: options.extraInfo
- ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo }
- : { user: user.id, guild: guild, type }
- }).catch(async (e) => {
- await util.handleError('removePunishmentEntry', e);
- success = false;
- });
- if (entries) {
- const promises = entries.map(async (entry) =>
- entry.destroy().catch(async (e) => {
- await util.handleError('removePunishmentEntry', e);
- success = false;
- })
- );
-
- await Promise.all(promises);
+ if (
+ checkModerator &&
+ victim.permissions.has(PermissionFlagsBits.ManageMessages) &&
+ !(type.startsWith('un') && moderator.id === victim.id)
+ ) {
+ if (await moderator.guild.hasFeature('modsCanPunishMods')) {
+ return true;
+ } else {
+ return `${emojis.error} You cannot ${type} **${victim.user.tag}** because they are a moderator.`;
}
- return success;
}
+ return true;
+}
- /**
- * Returns the punishment type enum for the given type.
- * @param type The type of the punishment.
- * @returns The punishment type enum.
- */
- private static findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') {
- const typeMap = {
- ['mute']: ActivePunishmentType.MUTE,
- ['ban']: ActivePunishmentType.BAN,
- ['role']: ActivePunishmentType.ROLE,
- ['block']: ActivePunishmentType.BLOCK
- };
- return typeMap[type];
- }
+/**
+ * Creates a modlog entry for a punishment.
+ * @param options Options for creating a modlog entry.
+ * @param getCaseNumber Whether or not to get the case number of the entry.
+ * @returns An object with the modlog and the case number.
+ */
+export async function createModLogEntry(
+ options: CreateModLogEntryOptions,
+ getCaseNumber = false
+): Promise<{ log: ModLog | null; caseNum: number | null }> {
+ const user = (await resolveNonCachedUser(options.user))!.id;
+ const moderator = (await resolveNonCachedUser(options.moderator))!.id;
+ const guild = client.guilds.resolveId(options.guild)!;
+
+ return createModLogEntrySimple(
+ {
+ ...options,
+ user: user,
+ moderator: moderator,
+ guild: guild
+ },
+ getCaseNumber
+ );
+}
- public static punishmentToPresentTense(punishment: PunishmentTypeDM): PunishmentTypePresent {
- return punishMap[punishment];
- }
+/**
+ * Creates a modlog entry with already resolved ids.
+ * @param options Options for creating a modlog entry.
+ * @param getCaseNumber Whether or not to get the case number of the entry.
+ * @returns An object with the modlog and the case number.
+ */
+export async function createModLogEntrySimple(
+ options: SimpleCreateModLogEntryOptions,
+ getCaseNumber = false
+): Promise<{ log: ModLog | null; caseNum: number | null }> {
+ // If guild does not exist create it so the modlog can reference a guild.
+ await GuildDB.findOrCreate({
+ where: { id: options.guild },
+ defaults: { id: options.guild }
+ });
+
+ const modLogEntry = ModLog.build({
+ type: options.type,
+ user: options.user,
+ moderator: options.moderator,
+ reason: options.reason,
+ duration: options.duration ? options.duration : undefined,
+ guild: options.guild,
+ pseudo: options.pseudo ?? false,
+ evidence: options.evidence,
+ hidden: options.hidden ?? false
+ });
+ const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => {
+ await handleError('createModLogEntry', e);
+ return null;
+ });
+
+ if (!getCaseNumber) return { log: saveResult, caseNum: null };
+
+ const caseNum = (
+ await ModLog.findAll({ where: { type: options.type, user: options.user, guild: options.guild, hidden: false } })
+ )?.length;
+ return { log: saveResult, caseNum };
+}
- public static punishmentToPastTense(punishment: PunishmentTypePresent): PunishmentTypeDM {
- return reversedPunishMap[punishment];
- }
+/**
+ * Creates a punishment entry.
+ * @param options Options for creating the punishment entry.
+ * @returns The database entry, or null if no entry is created.
+ */
+export async function createPunishmentEntry(options: CreatePunishmentEntryOptions): Promise<ActivePunishment | null> {
+ const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined;
+ const user = (await resolveNonCachedUser(options.user))!.id;
+ const guild = client.guilds.resolveId(options.guild)!;
+ const type = findTypeEnum(options.type)!;
+
+ const entry = ActivePunishment.build(
+ options.extraInfo
+ ? { user, type, guild, expires, modlog: options.modlog, extraInfo: options.extraInfo }
+ : { user, type, guild, expires, modlog: options.modlog }
+ );
+ return await entry.save().catch(async (e) => {
+ await handleError('createPunishmentEntry', e);
+ return null;
+ });
+}
- /**
- * Notifies the specified user of their punishment.
- * @param options Options for notifying the user.
- * @returns Whether or not the dm was successfully sent.
- */
- public static async punishDM(options: PunishDMOptions): Promise<boolean> {
- const ending = await options.guild.getSetting('punishmentEnding');
- const dmEmbed =
- ending && ending.length && options.sendFooter
- ? new EmbedBuilder().setDescription(ending).setColor(util.colors.newBlurple)
- : undefined;
-
- const appealsEnabled = !!(
- (await options.guild.hasFeature('punishmentAppeals')) && (await options.guild.getLogChannel('appeals'))
+/**
+ * Destroys a punishment entry.
+ * @param options Options for destroying the punishment entry.
+ * @returns Whether or not the entry was destroyed.
+ */
+export async function removePunishmentEntry(options: RemovePunishmentEntryOptions): Promise<boolean> {
+ const user = await resolveNonCachedUser(options.user);
+ const guild = client.guilds.resolveId(options.guild);
+ const type = findTypeEnum(options.type);
+
+ if (!user || !guild) return false;
+
+ let success = true;
+
+ const entries = await ActivePunishment.findAll({
+ // finding all cases of a certain type incase there were duplicates or something
+ where: options.extraInfo
+ ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo }
+ : { user: user.id, guild: guild, type }
+ }).catch(async (e) => {
+ await handleError('removePunishmentEntry', e);
+ success = false;
+ });
+ if (entries) {
+ const promises = entries.map(async (entry) =>
+ entry.destroy().catch(async (e) => {
+ await handleError('removePunishmentEntry', e);
+ success = false;
+ })
);
- let content = `You have been ${options.punishment} `;
- if (options.punishment.includes('blocked')) {
- assert(options.channel);
- content += `from <#${options.channel}> `;
- }
- content += `in ${util.format.input(options.guild.name)} `;
- if (options.duration !== null && options.duration !== undefined)
- content += options.duration ? `for ${util.humanizeDuration(options.duration)} ` : 'permanently ';
- const reason = options.reason?.trim() ? options.reason?.trim() : 'No reason provided';
- content += `for ${util.format.input(reason)}.`;
-
- let components;
- if (appealsEnabled && options.modlog)
- components = [
- new ActionRowBuilder<ButtonBuilder>({
- components: [
- new ButtonBuilder({
- customId: `appeal;${this.punishmentToPresentTense(options.punishment)};${options.guild.id};${client.users.resolveId(
- options.user
- )};${options.modlog}`,
- style: ButtonStyle.Primary,
- label: 'Appeal'
- }).toJSON()
- ]
- })
- ];
-
- const dmSuccess = await client.users
- .send(options.user, {
- content,
- embeds: dmEmbed ? [dmEmbed] : undefined,
- components
- })
- .catch(() => false);
- return !!dmSuccess;
+ await Promise.all(promises);
}
+ return success;
+}
+
+/**
+ * Returns the punishment type enum for the given type.
+ * @param type The type of the punishment.
+ * @returns The punishment type enum.
+ */
+function findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') {
+ const typeMap = {
+ ['mute']: ActivePunishmentType.MUTE,
+ ['ban']: ActivePunishmentType.BAN,
+ ['role']: ActivePunishmentType.ROLE,
+ ['block']: ActivePunishmentType.BLOCK
+ };
+ return typeMap[type];
+}
+
+export function punishmentToPresentTense(punishment: PunishmentTypeDM): PunishmentTypePresent {
+ return punishMap[punishment];
+}
+
+export function punishmentToPastTense(punishment: PunishmentTypePresent): PunishmentTypeDM {
+ return reversedPunishMap[punishment];
+}
+
+/**
+ * Notifies the specified user of their punishment.
+ * @param options Options for notifying the user.
+ * @returns Whether or not the dm was successfully sent.
+ */
+export async function punishDM(options: PunishDMOptions): Promise<boolean> {
+ const ending = await options.guild.getSetting('punishmentEnding');
+ const dmEmbed =
+ ending && ending.length && options.sendFooter
+ ? new EmbedBuilder().setDescription(ending).setColor(colors.newBlurple)
+ : undefined;
+
+ const appealsEnabled = !!(
+ (await options.guild.hasFeature('punishmentAppeals')) && (await options.guild.getLogChannel('appeals'))
+ );
+
+ let content = `You have been ${options.punishment} `;
+ if (options.punishment.includes('blocked')) {
+ assert(options.channel);
+ content += `from <#${options.channel}> `;
+ }
+ content += `in ${format.input(options.guild.name)} `;
+ if (options.duration !== null && options.duration !== undefined)
+ content += options.duration ? `for ${humanizeDuration(options.duration)} ` : 'permanently ';
+ const reason = options.reason?.trim() ? options.reason?.trim() : 'No reason provided';
+ content += `for ${format.input(reason)}.`;
+
+ let components;
+ if (appealsEnabled && options.modlog)
+ components = [
+ new ActionRowBuilder<ButtonBuilder>({
+ components: [
+ new ButtonBuilder({
+ customId: `appeal;${punishmentToPresentTense(options.punishment)};${options.guild.id};${client.users.resolveId(
+ options.user
+ )};${options.modlog}`,
+ style: ButtonStyle.Primary,
+ label: 'Appeal'
+ }).toJSON()
+ ]
+ })
+ ];
+
+ const dmSuccess = await client.users
+ .send(options.user, {
+ content,
+ embeds: dmEmbed ? [dmEmbed] : undefined,
+ components
+ })
+ .catch(() => false);
+ return !!dmSuccess;
}
interface BaseCreateModLogEntryOptions {
diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts
index 2644231..b382121 100644
--- a/src/lib/extensions/discord-akairo/BushClient.ts
+++ b/src/lib/extensions/discord-akairo/BushClient.ts
@@ -10,7 +10,7 @@ import {
roleWithDuration,
snowflake
} from '#args';
-import type { BushClientEvents, Config } from '#lib';
+import { BushClientEvents, emojis, formatError, inspect } from '#lib';
import { patch, type PatchedElements } from '@notenoughupdates/events-intercept';
import * as Sentry from '@sentry/node';
import {
@@ -18,7 +18,6 @@ import {
ContextMenuCommandHandler,
version as akairoVersion,
type ArgumentPromptData,
- type ClientUtil,
type OtherwiseContentSupplier
} from 'discord-akairo';
import {
@@ -46,6 +45,7 @@ import path from 'path';
import readline from 'readline';
import type { Options as SequelizeOptions, Sequelize as SequelizeType } from 'sequelize';
import { fileURLToPath } from 'url';
+import type { Config } from '../../../../config/Config.js';
import { tinyColor } from '../../../arguments/tinyColor.js';
import UpdateCacheTask from '../../../tasks/updateCache.js';
import UpdateStatsTask from '../../../tasks/updateStats.js';
@@ -63,13 +63,11 @@ import { Shared } from '../../models/shared/Shared.js';
import { Stat } from '../../models/shared/Stat.js';
import { AllowedMentions } from '../../utils/AllowedMentions.js';
import { BushCache } from '../../utils/BushCache.js';
-import { BushConstants } from '../../utils/BushConstants.js';
-import { BushLogger } from '../../utils/BushLogger.js';
+import BushLogger from '../../utils/BushLogger.js';
import { ExtendedGuild } from '../discord.js/ExtendedGuild.js';
import { ExtendedGuildMember } from '../discord.js/ExtendedGuildMember.js';
import { ExtendedMessage } from '../discord.js/ExtendedMessage.js';
import { ExtendedUser } from '../discord.js/ExtendedUser.js';
-import { BushClientUtil } from './BushClientUtil.js';
import { BushCommandHandler } from './BushCommandHandler.js';
import { BushInhibitorHandler } from './BushInhibitorHandler.js';
import { BushListenerHandler } from './BushListenerHandler.js';
@@ -86,10 +84,6 @@ declare module 'discord.js' {
* The ID of the superUser(s).
*/
superUserID: Snowflake | Snowflake[];
- /**
- * Utility methods.
- */
- util: ClientUtil | BushClientUtil;
on<K extends keyof BushClientEvents>(event: K, listener: (...args: BushClientEvents[K]) => Awaitable<void>): this;
once<K extends keyof BushClientEvents>(event: K, listener: (...args: BushClientEvents[K]) => Awaitable<void>): this;
emit<K extends keyof BushClientEvents>(event: K, ...args: BushClientEvents[K]): boolean;
@@ -128,7 +122,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Ready> {
public declare ownerID: Snowflake[];
public declare superUserID: Snowflake[];
- public declare util: BushClientUtil;
/**
* Whether or not the client is ready.
@@ -141,11 +134,6 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
public stats: BushStats = { cpu: undefined, commandsUsed: 0n, slashCommandsUsed: 0n };
/**
- * The configuration for the client.
- */
- public config: Config;
-
- /**
* The handler for the bot's listeners.
*/
public listenerHandler: BushListenerHandler;
@@ -186,11 +174,6 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
public logger = BushLogger;
/**
- * Constants for the bot.
- */
- public constants = BushConstants;
-
- /**
* Cached global and guild database data.
*/
public cache = new BushCache();
@@ -213,7 +196,12 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
/**
* @param config The configuration for the bot.
*/
- public constructor(config: Config) {
+ public constructor(
+ /**
+ * The configuration for the client.
+ */
+ public config: Config
+ ) {
super({
ownerID: config.owners,
intents: Object.keys(GatewayIntentBits)
@@ -233,7 +221,6 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
this.token = config.token as If<Ready, string, string | null>;
this.config = config;
- this.util = new BushClientUtil(this);
/* =-=-= handlers =-=-= */
this.listenerHandler = new BushListenerHandler(this, {
@@ -258,7 +245,7 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
const ending = '\n\n Type **cancel** to cancel the command';
const options = typeof text === 'function' ? await text(message, data) : text;
const search = '{error}',
- replace = this.consts.emojis.error;
+ replace = emojis.error;
if (typeof options === 'string') return (replaceError ? options.replace(search, replace) : options) + ending;
@@ -338,13 +325,6 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
}
/**
- * Constants for the bot.
- */
- public get consts(): typeof BushConstants {
- return this.constants;
- }
-
- /**
* Extends discord.js structures before the client is instantiated.
*/
public static extendStructures(): void {
@@ -422,11 +402,7 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
void this.logger.success('startup', `Successfully loaded <<${handlerName}>>.`, false);
})
.catch((e) => {
- void this.logger.error(
- 'startup',
- `Unable to load loader <<${handlerName}>> with error:\n${util.formatError(e)}`,
- false
- );
+ void this.logger.error('startup', `Unable to load loader <<${handlerName}>> with error:\n${formatError(e)}`, false);
if (process.argv.includes('dry')) process.exit(1);
})
);
@@ -451,7 +427,7 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
} catch (e) {
await this.console.error(
'startup',
- `Failed to connect to <<instance database>> with error:\n${util.inspect(e, { colors: true, depth: 1 })}`,
+ `Failed to connect to <<instance database>> with error:\n${inspect(e, { colors: true, depth: 1 })}`,
false
);
process.exit(2);
@@ -471,7 +447,7 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
} catch (e) {
await this.console.error(
'startup',
- `Failed to connect to <<shared database>> with error:\n${util.inspect(e, { colors: true, depth: 1 })}`,
+ `Failed to connect to <<shared database>> with error:\n${inspect(e, { colors: true, depth: 1 })}`,
false
);
process.exit(2);
@@ -503,7 +479,7 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re
this.stats.slashCommandsUsed = stats.slashCommandsUsed;
await this.login(this.token!);
} catch (e) {
- await this.console.error('start', util.inspect(e, { colors: true, depth: 1 }), false);
+ await this.console.error('start', inspect(e, { colors: true, depth: 1 }), false);
process.exit(1);
}
}
diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts
deleted file mode 100644
index 19810bd..0000000
--- a/src/lib/extensions/discord-akairo/BushClientUtil.ts
+++ /dev/null
@@ -1,1187 +0,0 @@
-import {
- Arg,
- BushConstants,
- CommandMessage,
- Global,
- Shared,
- type BaseBushArgumentType,
- type BushClient,
- type BushInspectOptions,
- type CodeBlockLang,
- type GlobalCache,
- type Pronoun,
- type PronounCode,
- type SharedCache,
- type SlashEditMessageType,
- type SlashMessage,
- type SlashSendMessageType
-} from '#lib';
-import { humanizeDuration } from '@notenoughupdates/humanize-duration';
-import assert from 'assert';
-import { exec } from 'child_process';
-import deepLock from 'deep-lock';
-import { ClientUtil, Util as AkairoUtil } from 'discord-akairo';
-import {
- Constants as DiscordConstants,
- EmbedBuilder,
- GuildMember,
- Message,
- OAuth2Scopes,
- PermissionFlagsBits,
- PermissionsBitField,
- Routes,
- ThreadMember,
- User,
- Util as DiscordUtil,
- type APIEmbed,
- type APIMessage,
- type CommandInteraction,
- type InteractionReplyOptions,
- type PermissionsString,
- type Snowflake,
- type TextChannel,
- type UserResolvable
-} from 'discord.js';
-import got from 'got';
-import _ from 'lodash';
-import { inspect, promisify } from 'util';
-import CommandErrorListener from '../../../listeners/commands/commandError.js';
-import { Format } from '../../common/util/Format.js';
-
-export type StripPrivate<T> = { [K in keyof T]: T[K] extends Record<string, any> ? StripPrivate<T[K]> : T[K] };
-
-export class BushClientUtil extends ClientUtil {
- /**
- * The client.
- */
- public declare readonly client: BushClient;
-
- /**
- * The hastebin urls used to post to hastebin, attempts to post in order
- */
- #hasteURLs: string[] = [
- 'https://hst.sh',
- // 'https://hasteb.in',
- 'https://hastebin.com',
- 'https://mystb.in',
- 'https://haste.clicksminuteper.net',
- 'https://paste.pythondiscord.com',
- 'https://haste.unbelievaboat.com'
- // 'https://haste.tyman.tech'
- ];
-
- /**
- * Creates this client util
- * @param client The client to initialize with
- */
- public constructor(client: BushClient) {
- super(client);
- }
-
- /**
- * Maps an array of user ids to user objects.
- * @param ids The list of IDs to map
- * @returns The list of users mapped
- */
- public async mapIDs(ids: Snowflake[]): Promise<User[]> {
- return await Promise.all(ids.map((id) => client.users.fetch(id)));
- }
-
- /**
- * Capitalizes the first letter of the given text
- * @param text The text to capitalize
- * @returns The capitalized text
- */
- public capitalize(text: string): string {
- return text.charAt(0).toUpperCase() + text.slice(1);
- }
-
- /**
- * Runs a shell command and gives the output
- * @param command The shell command to run
- * @returns The stdout and stderr of the shell command
- */
- public async shell(command: string): Promise<{
- stdout: string;
- stderr: string;
- }> {
- return await promisify(exec)(command);
- }
-
- /**
- * Posts text to hastebin
- * @param content The text to post
- * @returns The url of the posted text
- */
- public async haste(content: string, substr = false): Promise<HasteResults> {
- let isSubstr = false;
- if (content.length > 400_000 && !substr) {
- void this.handleError('haste', new Error(`content over 400,000 characters (${content.length.toLocaleString()})`));
- return { error: 'content too long' };
- } else if (content.length > 400_000) {
- content = content.substring(0, 400_000);
- isSubstr = true;
- }
- for (const url of this.#hasteURLs) {
- try {
- const res: HastebinRes = await got.post(`${url}/documents`, { body: content }).json();
- return { url: `${url}/${res.key}`, error: isSubstr ? 'substr' : undefined };
- } catch {
- void client.console.error('haste', `Unable to upload haste to ${url}`);
- }
- }
- return { error: 'unable to post' };
- }
-
- /**
- * Resolves a user-provided string into a user object, if possible
- * @param text The text to try and resolve
- * @returns The user resolved or null
- */
- public async resolveUserAsync(text: string): Promise<User | null> {
- const idReg = /\d{17,19}/;
- const idMatch = text.match(idReg);
- if (idMatch) {
- try {
- return await client.users.fetch(text as Snowflake);
- } catch {
- // pass
- }
- }
- const mentionReg = /<@!?(?<id>\d{17,19})>/;
- const mentionMatch = text.match(mentionReg);
- if (mentionMatch) {
- try {
- return await client.users.fetch(mentionMatch.groups!.id as Snowflake);
- } catch {
- // pass
- }
- }
- const user = client.users.cache.find((u) => u.username === text);
- if (user) return user;
- return null;
- }
-
- /**
- * Appends the correct ordinal to the given number
- * @param n The number to append an ordinal to
- * @returns The number with the ordinal
- */
- public ordinal(n: number): string {
- const s = ['th', 'st', 'nd', 'rd'],
- v = n % 100;
- return n + (s[(v - 20) % 10] || s[v] || s[0]);
- }
-
- /**
- * Chunks an array to the specified size
- * @param arr The array to chunk
- * @param perChunk The amount of items per chunk
- * @returns The chunked array
- */
- public chunk<T>(arr: T[], perChunk: number): T[][] {
- return arr.reduce((all, one, i) => {
- const ch: number = Math.floor(i / perChunk);
- (all as any[])[ch] = [].concat(all[ch] || [], one as any);
- return all;
- }, []);
- }
-
- /**
- * Commonly Used Colors
- */
- public get colors() {
- return client.consts.colors;
- }
-
- /**
- * Commonly Used Emojis
- */
- public get emojis() {
- return client.consts.emojis;
- }
-
- /**
- * Just the ids of Commonly Used Emojis
- */
- public get emojisRaw() {
- return client.consts.emojisRaw;
- }
-
- /**
- * Fetches a user's uuid from the mojang api.
- * @param username The username to get the uuid of.
- * @returns The the uuid of the user.
- */
- public async mcUUID(username: string, dashed = false): Promise<string> {
- const apiRes = (await got.get(`https://api.ashcon.app/mojang/v2/user/${username}`).json()) as UuidRes;
- return dashed ? apiRes.uuid : apiRes.uuid.replace(/-/g, '');
- }
-
- /**
- * Surrounds text in a code block with the specified language and puts it in a hastebin if its too long.
- * * Embed Description Limit = 4096 characters
- * * Embed Field Limit = 1024 characters
- * @param code The content of the code block.
- * @param length The maximum length of the code block.
- * @param language The language of the code.
- * @param substr Whether or not to substring the code if it is too long.
- * @returns The generated code block
- */
- public async codeblock(code: string, length: number, language: CodeBlockLang | '' = '', substr = false): Promise<string> {
- let hasteOut = '';
- code = this.discord.escapeCodeBlock(code);
- const prefix = `\`\`\`${language}\n`;
- const suffix = '\n```';
- if (code.length + (prefix + suffix).length >= length) {
- const haste = await this.haste(code, substr);
- hasteOut = `Too large to display. ${
- haste.url
- ? `Hastebin: ${haste.url}${language ? `.${language}` : ''}${haste.error ? ` - ${haste.error}` : ''}`
- : `${this.emojis.error} Hastebin: ${haste.error}`
- }`;
- }
-
- const FormattedHaste = hasteOut.length ? `\n${hasteOut}` : '';
- const shortenedCode = hasteOut ? code.substring(0, length - (prefix + FormattedHaste + suffix).length) : code;
- const code3 = code.length ? prefix + shortenedCode + suffix + FormattedHaste : prefix + suffix;
- if (code3.length > length) {
- void client.console.warn(`codeblockError`, `Required Length: ${length}. Actual Length: ${code3.length}`, true);
- void client.console.warn(`codeblockError`, code3, true);
- throw new Error('code too long');
- }
- return code3;
- }
-
- /**
- * Generate defaults for {@link inspect}.
- * @param options The options to create defaults with.
- * @returns The default options combined with the specified options.
- */
- #getDefaultInspectOptions(options?: BushInspectOptions): BushInspectOptions {
- return {
- showHidden: options?.showHidden ?? false,
- depth: options?.depth ?? 2,
- colors: options?.colors ?? false,
- customInspect: options?.customInspect ?? true,
- showProxy: options?.showProxy ?? false,
- maxArrayLength: options?.maxArrayLength ?? Infinity,
- maxStringLength: options?.maxStringLength ?? Infinity,
- breakLength: options?.breakLength ?? 80,
- compact: options?.compact ?? 3,
- sorted: options?.sorted ?? false,
- getters: options?.getters ?? true
- };
- }
-
- /**
- * Maps the key of a credential with a readable version when redacting.
- * @param key The key of the credential.
- * @returns The readable version of the key or the original key if there isn't a mapping.
- */
- #mapCredential(key: string): string {
- const mapping = {
- token: 'Main Token',
- devToken: 'Dev Token',
- betaToken: 'Beta Token',
- hypixelApiKey: 'Hypixel Api Key',
- wolframAlphaAppId: 'Wolfram|Alpha App ID',
- dbPassword: 'Database Password'
- };
- return mapping[key as keyof typeof mapping] || key;
- }
-
- /**
- * Redacts credentials from a string.
- * @param text The text to redact credentials from.
- * @returns The redacted text.
- */
- public redact(text: string) {
- for (const credentialName in { ...client.config.credentials, dbPassword: client.config.db.password }) {
- const credential = { ...client.config.credentials, dbPassword: client.config.db.password }[
- credentialName as keyof typeof client.config.credentials
- ];
- const replacement = this.#mapCredential(credentialName);
- const escapeRegex = /[.*+?^${}()|[\]\\]/g;
- text = text.replace(new RegExp(credential.toString().replace(escapeRegex, '\\$&'), 'g'), `[${replacement} Omitted]`);
- text = text.replace(
- new RegExp([...credential.toString()].reverse().join('').replace(escapeRegex, '\\$&'), 'g'),
- `[${replacement} Omitted]`
- );
- }
- return text;
- }
-
- /**
- * Uses {@link inspect} with custom defaults.
- * @param object - The object you would like to inspect.
- * @param options - The options you would like to use to inspect the object.
- * @returns The inspected object.
- */
- public inspect(object: any, options?: BushInspectOptions): string {
- const optionsWithDefaults = this.#getDefaultInspectOptions(options);
-
- if (!optionsWithDefaults.inspectStrings && typeof object === 'string') return object;
-
- return inspect(object, optionsWithDefaults);
- }
-
- /**
- * Takes an any value, inspects it, redacts credentials, and puts it in a codeblock
- * (and uploads to hast if the content is too long).
- * @param input The object to be inspect, redacted, and put into a codeblock.
- * @param language The language to make the codeblock.
- * @param inspectOptions The options for {@link BushClientUtil.inspect}.
- * @param length The maximum length that the codeblock can be.
- * @returns The generated codeblock.
- */
- public async inspectCleanRedactCodeblock(
- input: any,
- language?: CodeBlockLang | '',
- inspectOptions?: BushInspectOptions,
- length = 1024
- ) {
- input = this.inspect(input, inspectOptions ?? undefined);
- if (inspectOptions) inspectOptions.inspectStrings = undefined;
- input = this.discord.cleanCodeBlockContent(input);
- input = this.redact(input);
- return this.codeblock(input, length, language, true);
- }
-
- /**
- * Takes an any value, inspects it, redacts credentials, and uploads it to haste.
- * @param input The object to be inspect, redacted, and upload.
- * @param inspectOptions The options for {@link BushClientUtil.inspect}.
- * @returns The {@link HasteResults}.
- */
- public async inspectCleanRedactHaste(input: any, inspectOptions?: BushInspectOptions): Promise<HasteResults> {
- input = this.inspect(input, inspectOptions ?? undefined);
- input = this.redact(input);
- return this.haste(input, true);
- }
-
- /**
- * Takes an any value, inspects it and redacts credentials.
- * @param input The object to be inspect and redacted.
- * @param inspectOptions The options for {@link BushClientUtil.inspect}.
- * @returns The redacted and inspected object.
- */
- public inspectAndRedact(input: any, inspectOptions?: BushInspectOptions): string {
- input = this.inspect(input, inspectOptions ?? undefined);
- return this.redact(input);
- }
-
- /**
- * Responds to a slash command interaction.
- * @param interaction The interaction to respond to.
- * @param responseOptions The options for the response.
- * @returns The message sent.
- */
- public async slashRespond(
- interaction: CommandInteraction,
- responseOptions: SlashSendMessageType | SlashEditMessageType
- ): Promise<Message | APIMessage | undefined> {
- const newResponseOptions = typeof responseOptions === 'string' ? { content: responseOptions } : responseOptions;
- if (interaction.replied || interaction.deferred) {
- delete (newResponseOptions as InteractionReplyOptions).ephemeral; // Cannot change a preexisting message to be ephemeral
- return (await interaction.editReply(newResponseOptions)) as Message | APIMessage;
- } else {
- await interaction.reply(newResponseOptions);
- return await interaction.fetchReply().catch(() => undefined);
- }
- }
-
- /**
- * Gets a a configured channel as a TextChannel.
- * @channel The channel to retrieve.
- */
- public async getConfigChannel(channel: keyof typeof client['config']['channels']): Promise<TextChannel> {
- return (await client.channels.fetch(client.config.channels[channel])) as unknown as TextChannel;
- }
-
- /**
- * Takes an array and combines the elements using the supplied conjunction.
- * @param array The array to combine.
- * @param conjunction The conjunction to use.
- * @param ifEmpty What to return if the array is empty.
- * @returns The combined elements or `ifEmpty`.
- *
- * @example
- * const permissions = oxford(['Administrator', 'SendMessages', 'ManageMessages'], 'and', 'none');
- * console.log(permissions); // Administrator, SendMessages and ManageMessages
- */
- public oxford(array: string[], conjunction: string, ifEmpty?: string): string | undefined {
- const l = array.length;
- if (!l) return ifEmpty;
- if (l < 2) return array[0];
- if (l < 3) return array.join(` ${conjunction} `);
- array = array.slice();
- array[l - 1] = `${conjunction} ${array[l - 1]}`;
- return array.join(', ');
- }
-
- /**
- * Get the global cache.
- */
- public getGlobal(): GlobalCache;
- /**
- * Get a key from the global cache.
- * @param key The key to get in the global cache.
- */
- public getGlobal<K extends keyof GlobalCache>(key: K): GlobalCache[K];
- public getGlobal(key?: keyof GlobalCache) {
- return key ? client.cache.global[key] : client.cache.global;
- }
-
- public getShared(): SharedCache;
- public getShared<K extends keyof SharedCache>(key: K): SharedCache[K];
- public getShared(key?: keyof SharedCache) {
- return key ? client.cache.shared[key] : client.cache.shared;
- }
-
- /**
- * Add or remove an element from an array stored in the Globals database.
- * @param action Either `add` or `remove` an element.
- * @param key The key of the element in the global cache to update.
- * @param value The value to add/remove from the array.
- */
- public async insertOrRemoveFromGlobal<K extends keyof typeof client['cache']['global']>(
- action: 'add' | 'remove',
- key: K,
- value: typeof client['cache']['global'][K][0]
- ): Promise<Global | void> {
- const row =
- (await Global.findByPk(client.config.environment)) ?? (await Global.create({ environment: client.config.environment }));
- const oldValue: any[] = row[key];
- const newValue = this.addOrRemoveFromArray(action, oldValue, value);
- row[key] = newValue;
- client.cache.global[key] = newValue;
- return await row.save().catch((e) => this.handleError('insertOrRemoveFromGlobal', e));
- }
-
- /**
- * Add or remove an element from an array stored in the Shared database.
- * @param action Either `add` or `remove` an element.
- * @param key The key of the element in the shared cache to update.
- * @param value The value to add/remove from the array.
- */
- public async insertOrRemoveFromShared<K extends Exclude<keyof typeof client['cache']['shared'], 'badWords' | 'autoBanCode'>>(
- action: 'add' | 'remove',
- key: K,
- value: typeof client['cache']['shared'][K][0]
- ): Promise<Shared | void> {
- const row = (await Shared.findByPk(0)) ?? (await Shared.create());
- const oldValue: any[] = row[key];
- const newValue = this.addOrRemoveFromArray(action, oldValue, value);
- row[key] = newValue;
- client.cache.shared[key] = newValue;
- return await row.save().catch((e) => this.handleError('insertOrRemoveFromShared', e));
- }
-
- /**
- * Updates an element in the Globals database.
- * @param key The key in the global cache to update.
- * @param value The value to set the key to.
- */
- public async setGlobal<K extends keyof typeof client['cache']['global']>(
- key: K,
- value: typeof client['cache']['global'][K]
- ): Promise<Global | void> {
- const row =
- (await Global.findByPk(client.config.environment)) ?? (await Global.create({ environment: client.config.environment }));
- row[key] = value;
- client.cache.global[key] = value;
- return await row.save().catch((e) => this.handleError('setGlobal', e));
- }
-
- /**
- * Updates an element in the Shared database.
- * @param key The key in the shared cache to update.
- * @param value The value to set the key to.
- */
- public async setShared<K extends Exclude<keyof typeof client['cache']['shared'], 'badWords' | 'autoBanCode'>>(
- key: K,
- value: typeof client['cache']['shared'][K]
- ): Promise<Shared | void> {
- const row = (await Shared.findByPk(0)) ?? (await Shared.create());
- row[key] = value;
- client.cache.shared[key] = value;
- return await row.save().catch((e) => this.handleError('setShared', e));
- }
-
- /**
- * Add or remove an item from an array. All duplicates will be removed.
- * @param action Either `add` or `remove` an element.
- * @param array The array to add/remove an element from.
- * @param value The element to add/remove from the array.
- */
- public addOrRemoveFromArray<T>(action: 'add' | 'remove', array: T[], value: T): T[] {
- const set = new Set(array);
- action === 'add' ? set.add(value) : set.delete(value);
- return [...set];
- }
-
- /**
- * Remove an item from an array. All duplicates will be removed.
- * @param array The array to remove an element from.
- * @param value The element to remove from the array.
- */
- public removeFromArray<T>(array: T[], value: T): T[] {
- return this.addOrRemoveFromArray('remove', array, value);
- }
-
- /**
- * Add an item from an array. All duplicates will be removed.
- * @param array The array to add an element to.
- * @param value The element to add to the array.
- */
- public addToArray<T>(array: T[], value: T): T[] {
- return this.addOrRemoveFromArray('add', array, value);
- }
-
- /**
- * Surrounds a string to the begging an end of each element in an array.
- * @param array The array you want to surround.
- * @param surroundChar1 The character placed in the beginning of the element.
- * @param surroundChar2 The character placed in the end of the element. Defaults to `surroundChar1`.
- */
- public surroundArray(array: string[], surroundChar1: string, surroundChar2?: string): string[] {
- return array.map((a) => `${surroundChar1}${a}${surroundChar2 ?? surroundChar1}`);
- }
-
- /**
- * Gets the duration from a specified string.
- * @param content The string to look for a duration in.
- * @param remove Whether or not to remove the duration from the original string.
- * @returns The {@link ParsedDuration}.
- */
- public parseDuration(content: string, remove = true): ParsedDuration {
- if (!content) return { duration: 0, content: null };
-
- // eslint-disable-next-line prefer-const
- let duration: number | null = null;
- // Try to reduce false positives by requiring a space before the duration, this makes sure it still matches if it is
- // in the beginning of the argument
- let contentWithoutTime = ` ${content}`;
-
- for (const unit in BushConstants.timeUnits) {
- const regex = BushConstants.timeUnits[unit as keyof typeof BushConstants.timeUnits].match;
- const match = regex.exec(contentWithoutTime);
- const value = Number(match?.groups?.[unit]);
- if (!isNaN(value)) duration! += value * BushConstants.timeUnits[unit as keyof typeof BushConstants.timeUnits].value;
-
- if (remove) contentWithoutTime = contentWithoutTime.replace(regex, '');
- }
- // remove the space added earlier
- if (contentWithoutTime.startsWith(' ')) contentWithoutTime.replace(' ', '');
- return { duration, content: contentWithoutTime };
- }
-
- /**
- * Converts a duration in milliseconds to a human readable form.
- * @param duration The duration in milliseconds to convert.
- * @param largest The maximum number of units to display for the duration.
- * @param round Whether or not to round the smallest unit displayed.
- * @returns A humanized string of the duration.
- */
- public humanizeDuration(duration: number, largest?: number, round = true): string {
- if (largest) return humanizeDuration(duration, { language: 'en', maxDecimalPoints: 2, largest, round })!;
- else return humanizeDuration(duration, { language: 'en', maxDecimalPoints: 2, round })!;
- }
-
- /**
- * Creates a formatted relative timestamp from a duration in milliseconds.
- * @param duration The duration in milliseconds.
- * @returns The formatted relative timestamp.
- */
- public timestampDuration(duration: number): string {
- return `<t:${Math.round(new Date().getTime() / 1_000 + duration / 1_000)}:R>`;
- }
-
- /**
- * Creates a timestamp from a date.
- * @param date The date to create a timestamp from.
- * @param style The style of the timestamp.
- * @returns The formatted timestamp.
- *
- * @see
- * **Styles:**
- * - **t**: Short Time ex. `16:20`
- * - **T**: Long Time ex. `16:20:30 `
- * - **d**: Short Date ex. `20/04/2021`
- * - **D**: Long Date ex. `20 April 2021`
- * - **f**: Short Date/Time ex. `20 April 2021 16:20`
- * - **F**: Long Date/Time ex. `Tuesday, 20 April 2021 16:20`
- * - **R**: Relative Time ex. `2 months ago`
- */
- public timestamp<D extends Date | undefined | null>(date: D, style: TimestampStyle = 'f'): D extends Date ? string : undefined {
- if (!date) return date as unknown as D extends Date ? string : undefined;
- return `<t:${Math.round(date.getTime() / 1_000)}:${style}>` as unknown as D extends Date ? string : undefined;
- }
-
- /**
- * Creates a human readable representation between a date and the current time.
- * @param date The date to be compared with the current time.
- * @param largest The maximum number of units to display for the duration.
- * @param round Whether or not to round the smallest unit displayed.
- * @returns A humanized string of the delta.
- */
- public dateDelta(date: Date, largest?: number, round = true): string {
- return this.humanizeDuration(new Date().getTime() - date.getTime(), largest ?? 3, round);
- }
-
- /**
- * Combines {@link timestamp} and {@link dateDelta}
- * @param date The date to be compared with the current time.
- * @param style The style of the timestamp.
- * @returns The formatted timestamp.
- *
- * @see
- * **Styles:**
- * - **t**: Short Time ex. `16:20`
- * - **T**: Long Time ex. `16:20:30 `
- * - **d**: Short Date ex. `20/04/2021`
- * - **D**: Long Date ex. `20 April 2021`
- * - **f**: Short Date/Time ex. `20 April 2021 16:20`
- * - **F**: Long Date/Time ex. `Tuesday, 20 April 2021 16:20`
- * - **R**: Relative Time ex. `2 months ago`
- */
- public timestampAndDelta(date: Date, style: TimestampStyle = 'D'): string {
- return `${this.timestamp(date, style)} (${this.dateDelta(date)} ago)`;
- }
-
- /**
- * Convert a hex code to an rbg value.
- * @param hex The hex code to convert.
- * @returns The rbg value.
- */
- public hexToRgb(hex: string): string {
- const arrBuff = new ArrayBuffer(4);
- const vw = new DataView(arrBuff);
- vw.setUint32(0, parseInt(hex, 16), false);
- const arrByte = new Uint8Array(arrBuff);
-
- return `${arrByte[1]}, ${arrByte[2]}, ${arrByte[3]}`;
- }
-
- /**
- * Capitalize the first letter of a string.
- * @param string The string to capitalize the first letter of.
- * @returns The string with the first letter capitalized.
- */
- public capitalizeFirstLetter(string: string): string {
- return string.charAt(0)?.toUpperCase() + string.slice(1);
- }
-
- /**
- * Wait an amount in milliseconds.
- * @returns A promise that resolves after the specified amount of milliseconds
- */
- public get sleep() {
- return promisify(setTimeout);
- }
-
- /**
- * Send a message in the error logging channel and console for an error.
- * @param context
- * @param error
- */
- public async handleError(context: string, error: Error) {
- await client.console.error(_.camelCase(context), `An error occurred:\n${util.formatError(error, false)}`, false);
- await client.console.channelError({
- embeds: await CommandErrorListener.generateErrorEmbed({ type: 'unhandledRejection', error: error, context })
- });
- }
-
- /**
- * Fetches a user from discord.
- * @param user The user to fetch
- * @returns Undefined if the user is not found, otherwise the user.
- */
- public async resolveNonCachedUser(user: UserResolvable | undefined | null): Promise<User | undefined> {
- if (user == null) return undefined;
- const resolvedUser =
- user instanceof User
- ? user
- : user instanceof GuildMember
- ? user.user
- : user instanceof ThreadMember
- ? user.user
- : user instanceof Message
- ? user.author
- : undefined;
-
- return resolvedUser ?? (await client.users.fetch(user as Snowflake).catch(() => undefined));
- }
-
- /**
- * Get the pronouns of a discord user from pronoundb.org
- * @param user The user to retrieve the promises of.
- * @returns The human readable pronouns of the user, or undefined if they do not have any.
- */
- 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()
- .catch(() => undefined)) as { pronouns: PronounCode } | undefined;
-
- if (!apiRes) return undefined;
- assert(apiRes.pronouns);
-
- return client.constants.pronounMapping[apiRes.pronouns!]!;
- }
-
- /**
- * List the methods of an object.
- * @param obj The object to get the methods of.
- * @returns A string with each method on a new line.
- */
- public getMethods(obj: Record<string, any>): string {
- // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class/31055217#31055217
- let props: string[] = [];
- let obj_: Record<string, any> = new Object(obj);
-
- do {
- const l = Object.getOwnPropertyNames(obj_)
- .concat(Object.getOwnPropertySymbols(obj_).map((s) => s.toString()))
- .sort()
- .filter(
- (p, i, arr) =>
- typeof Object.getOwnPropertyDescriptor(obj_, p)?.['get'] !== 'function' && // ignore getters
- typeof Object.getOwnPropertyDescriptor(obj_, p)?.['set'] !== 'function' && // ignore setters
- typeof obj_[p] === 'function' && // only the methods
- p !== 'constructor' && // not the constructor
- (i == 0 || p !== arr[i - 1]) && // not overriding in this prototype
- props.indexOf(p) === -1 // not overridden in a child
- );
-
- const reg = /\(([\s\S]*?)\)/;
- props = props.concat(
- l.map(
- (p) =>
- `${obj_[p] && obj_[p][Symbol.toStringTag] === 'AsyncFunction' ? 'async ' : ''}function ${p}(${
- reg.exec(obj_[p].toString())?.[1]
- ? reg
- .exec(obj_[p].toString())?.[1]
- .split(', ')
- .map((arg) => arg.split('=')[0].trim())
- .join(', ')
- : ''
- });`
- )
- );
- } while (
- (obj_ = Object.getPrototypeOf(obj_)) && // walk-up the prototype chain
- Object.getPrototypeOf(obj_) // not the the Object prototype methods (hasOwnProperty, etc...)
- );
-
- return props.join('\n');
- }
-
- /**
- * List the symbols of an object.
- * @param obj The object to get the symbols of.
- * @returns An array of the symbols of the object.
- */
- public getSymbols(obj: Record<string, any>): symbol[] {
- let symbols: symbol[] = [];
- let obj_: Record<string, any> = new Object(obj);
-
- do {
- const l = Object.getOwnPropertySymbols(obj_).sort();
-
- symbols = [...symbols, ...l];
- } while (
- (obj_ = Object.getPrototypeOf(obj_)) && // walk-up the prototype chain
- Object.getPrototypeOf(obj_) // not the the Object prototype methods (hasOwnProperty, etc...)
- );
-
- return symbols;
- }
-
- /**
- * Uploads an image to imgur.
- * @param image The image to upload.
- * @returns The url of the imgur.
- */
- 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()) as { data: { link: string } };
-
- return resp.data.link;
- }
-
- /**
- * Checks if a user has a certain guild permission (doesn't check channel permissions).
- * @param message The message to check the user from.
- * @param permissions The permissions to check for.
- * @returns The missing permissions or null if none are missing.
- */
- public userGuildPermCheck(
- message: CommandMessage | SlashMessage,
- permissions: typeof PermissionFlagsBits[keyof typeof PermissionFlagsBits][]
- ): PermissionsString[] | null {
- if (!message.inGuild()) return null;
- const missing = message.member?.permissions.missing(permissions) ?? [];
-
- return missing.length ? missing : null;
- }
-
- /**
- * Check if the client has certain permissions in the guild (doesn't check channel permissions).
- * @param message The message to check the client user from.
- * @param permissions The permissions to check for.
- * @returns The missing permissions or null if none are missing.
- */
- public clientGuildPermCheck(message: CommandMessage | SlashMessage, permissions: bigint[]): PermissionsString[] | null {
- const missing = message.guild?.members.me?.permissions.missing(permissions) ?? [];
-
- return missing.length ? missing : null;
- }
-
- /**
- * Check if the client has permission to send messages in the channel as well as check if they have other permissions
- * in the guild (or the channel if `checkChannel` is `true`).
- * @param message The message to check the client user from.
- * @param permissions The permissions to check for.
- * @param checkChannel Whether to check the channel permissions instead of the guild permissions.
- * @returns The missing permissions or null if none are missing.
- */
- public clientSendAndPermCheck(
- message: CommandMessage | SlashMessage,
- permissions: bigint[] = [],
- checkChannel = false
- ): PermissionsString[] | null {
- const missing: PermissionsString[] = [];
- const sendPerm = message.channel!.isThread() ? 'SendMessages' : 'SendMessagesInThreads';
- if (!message.inGuild()) return null;
-
- if (!message.guild.members.me!.permissionsIn(message.channel!.id).has(sendPerm)) missing.push(sendPerm);
-
- missing.push(
- ...(checkChannel
- ? message.guild!.members.me!.permissionsIn(message.channel!.id!).missing(permissions)
- : this.clientGuildPermCheck(message, permissions) ?? [])
- );
-
- return missing.length ? missing : null;
- }
-
- /**
- * Gets the prefix based off of the message.
- * @param message The message to get the prefix from.
- * @returns The prefix.
- */
- public prefix(message: CommandMessage | SlashMessage): string {
- return message.util.isSlash
- ? '/'
- : client.config.isDevelopment
- ? 'dev '
- : message.util.parsed?.prefix ?? client.config.prefix;
- }
-
- /**
- * Recursively apply provided options operations on object
- * and all of the object properties that are either object or function.
- *
- * By default freezes object.
- *
- * @param obj - The object to which will be applied `freeze`, `seal` or `preventExtensions`
- * @param options default `{ action: 'freeze' }`
- * @param options.action
- * ```
- * | action | Add | Modify | Delete | Reconfigure |
- * | ----------------- | --- | ------ | ------ | ----------- |
- * | preventExtensions | - | + | + | + |
- * | seal | - | + | - | - |
- * | freeze | - | - | - | - |
- * ```
- *
- * @returns Initial object with applied options action
- */
- public get deepFreeze() {
- return deepLock;
- }
-
- /**
- * Recursively apply provided options operations on object
- * and all of the object properties that are either object or function.
- *
- * By default freezes object.
- *
- * @param obj - The object to which will be applied `freeze`, `seal` or `preventExtensions`
- * @param options default `{ action: 'freeze' }`
- * @param options.action
- * ```
- * | action | Add | Modify | Delete | Reconfigure |
- * | ----------------- | --- | ------ | ------ | ----------- |
- * | preventExtensions | - | + | + | + |
- * | seal | - | + | - | - |
- * | freeze | - | - | - | - |
- * ```
- *
- * @returns Initial object with applied options action
- */
- public static get deepFreeze() {
- return deepLock;
- }
-
- /**
- * The link to invite the bot with all permissions.
- */
- public get invite() {
- return client.generateInvite({
- permissions:
- PermissionsBitField.All -
- PermissionFlagsBits.UseEmbeddedActivities -
- PermissionFlagsBits.ViewGuildInsights -
- PermissionFlagsBits.Stream,
- scopes: [OAuth2Scopes.Bot, OAuth2Scopes.ApplicationsCommands]
- });
- }
-
- /**
- * Asset multiple statements at a time.
- * @param args
- */
- public assertAll(...args: any[]): void {
- for (let i = 0; i < args.length; i++) {
- assert(args[i], `assertAll index ${i} failed`);
- }
- }
-
- /**
- * Casts a string to a duration and reason for slash commands.
- * @param arg The argument received.
- * @param message The message that triggered the command.
- * @returns The casted argument.
- */
- public async castDurationContent(
- arg: string | ParsedDuration | null,
- message: CommandMessage | SlashMessage
- ): Promise<ParsedDurationRes> {
- const res = typeof arg === 'string' ? await util.arg.cast('contentWithDuration', message, arg) : arg;
-
- return { duration: res?.duration ?? 0, content: res?.content ?? '' };
- }
-
- /**
- * Casts a string to a the specified argument type.
- * @param type The type of the argument to cast to.
- * @param arg The argument received.
- * @param message The message that triggered the command.
- * @returns The casted argument.
- */
- public async cast<T extends keyof BaseBushArgumentType>(
- type: T,
- arg: BaseBushArgumentType[T] | string,
- message: CommandMessage | SlashMessage
- ) {
- return typeof arg === 'string' ? await util.arg.cast(type, message, arg) : arg;
- }
-
- /**
- * Overflows the description of an embed into multiple embeds.
- * @param embed The options to be applied to the (first) embed.
- * @param lines Each line of the description as an element in an array.
- */
- public overflowEmbed(embed: Omit<APIEmbed, 'description'>, lines: string[], maxLength = 4096): EmbedBuilder[] {
- const embeds: EmbedBuilder[] = [];
-
- const makeEmbed = () => {
- embeds.push(new EmbedBuilder().setColor(embed.color ?? null));
- return embeds.at(-1)!;
- };
-
- for (const line of lines) {
- let current = embeds.length ? embeds.at(-1)! : makeEmbed();
- let joined = current.data.description ? `${current.data.description}\n${line}` : line;
- if (joined.length > maxLength) {
- current = makeEmbed();
- joined = line;
- }
-
- current.setDescription(joined);
- }
-
- if (!embeds.length) makeEmbed();
-
- if (embed.author) embeds.at(0)?.setAuthor(embed.author);
- if (embed.title) embeds.at(0)?.setTitle(embed.title);
- if (embed.url) embeds.at(0)?.setURL(embed.url);
- if (embed.fields) embeds.at(-1)?.setFields(embed.fields);
- if (embed.thumbnail) embeds.at(-1)?.setThumbnail(embed.thumbnail.url);
- if (embed.footer) embeds.at(-1)?.setFooter(embed.footer);
- if (embed.image) embeds.at(-1)?.setImage(embed.image.url);
- if (embed.timestamp) embeds.at(-1)?.setTimestamp(new Date(embed.timestamp));
-
- return embeds;
- }
-
- public async resolveMessageLinks(content: string | null): Promise<MessageLinkParts[]> {
- const res: MessageLinkParts[] = [];
-
- if (!content) return res;
-
- const regex = new RegExp(this.regex.messageLink);
- let match: RegExpExecArray | null;
- while (((match = regex.exec(content)), match !== null)) {
- const input = match.input;
- if (!match.groups || !input) continue;
- if (input.startsWith('<') && input.endsWith('>')) continue;
-
- const { guild_id, channel_id, message_id } = match.groups;
- if (!guild_id || !channel_id || !message_id) continue;
-
- res.push({ guild_id, channel_id, message_id });
- }
-
- return res;
- }
-
- public async resolveMessagesFromLinks(content: string): Promise<APIMessage[]> {
- const res: APIMessage[] = [];
-
- const links = await this.resolveMessageLinks(content);
- if (!links.length) return [];
-
- for (const { guild_id, channel_id, message_id } of links) {
- const guild = client.guilds.cache.get(guild_id);
- if (!guild) continue;
- const channel = guild.channels.cache.get(channel_id);
- if (!channel || (!channel.isTextBased() && !channel.isThread())) continue;
-
- const message = (await client.rest
- .get(Routes.channelMessage(channel_id, message_id))
- .catch(() => null)) as APIMessage | null;
- if (!message) continue;
-
- res.push(message);
- }
-
- return res;
- }
-
- /**
- * Formats an error into a string.
- * @param error The error to format.
- * @param colors Whether to use colors in the output.
- * @returns The formatted error.
- */
- public formatError(error: Error | any, colors = false): string {
- if (!error) return error;
- if (typeof error !== 'object') return String.prototype.toString.call(error);
- if (
- this.getSymbols(error)
- .map((s) => s.toString())
- .includes('Symbol(nodejs.util.inspect.custom)')
- )
- return this.inspect(error, { colors });
-
- return error.stack;
- }
-
- /**
- * A wrapper for the Argument class that adds custom typings.
- */
- public get arg() {
- return Arg;
- }
-
- /**
- * Formats and escapes content for formatting
- */
- public get format() {
- return Format;
- }
-
- /**
- * Discord.js's Util class
- */
- public get discord() {
- return DiscordUtil;
- }
-
- /**
- * Discord.js's Util constants
- */
- public get discordConstants() {
- return DiscordConstants;
- }
-
- /**
- * discord-akairo's Util class
- */
- public get akairo() {
- return AkairoUtil;
- }
-
- public get consts() {
- return client.consts;
- }
-
- public get regex() {
- return client.consts.regex;
- }
-}
-
-interface HastebinRes {
- key: string;
-}
-
-export interface UuidRes {
- uuid: string;
- username: string;
- username_history?: { username: string }[] | null;
- textures: {
- custom: boolean;
- slim: boolean;
- skin: {
- url: string;
- data: string;
- };
- raw: {
- value: string;
- signature: string;
- };
- };
- created_at: string;
-}
-
-export interface HasteResults {
- url?: string;
- error?: 'content too long' | 'substr' | 'unable to post';
-}
-
-export interface ParsedDuration {
- duration: number | null;
- content: string | null;
-}
-
-export interface ParsedDurationRes {
- duration: number;
- content: string;
-}
-
-export type TimestampStyle = 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R';
-
-export interface MessageLinkParts {
- guild_id: Snowflake;
- channel_id: Snowflake;
- message_id: Snowflake;
-}
diff --git a/src/lib/extensions/discord-akairo/BushInhibitor.ts b/src/lib/extensions/discord-akairo/BushInhibitor.ts
index 12b2baf..b4e6797 100644
--- a/src/lib/extensions/discord-akairo/BushInhibitor.ts
+++ b/src/lib/extensions/discord-akairo/BushInhibitor.ts
@@ -1,11 +1,9 @@
import { type BushClient, type BushCommand, type CommandMessage, type SlashMessage } from '#lib';
import { Inhibitor } from 'discord-akairo';
-export class BushInhibitor extends Inhibitor {
+export abstract class BushInhibitor extends Inhibitor {
public declare client: BushClient;
-}
-export interface BushInhibitor {
/**
* Checks if message should be blocked.
* A return value of true will block the message.
@@ -16,6 +14,6 @@ export interface BushInhibitor {
* @param message - Message being handled.
* @param command - Command to check.
*/
- exec(message: CommandMessage, command: BushCommand): any;
- exec(message: CommandMessage | SlashMessage, command: BushCommand): any;
+ public abstract override exec(message: CommandMessage, command: BushCommand): any;
+ public abstract override exec(message: CommandMessage | SlashMessage, command: BushCommand): any;
}
diff --git a/src/lib/extensions/discord-akairo/BushListener.ts b/src/lib/extensions/discord-akairo/BushListener.ts
index 3efe527..6917641 100644
--- a/src/lib/extensions/discord-akairo/BushListener.ts
+++ b/src/lib/extensions/discord-akairo/BushListener.ts
@@ -1,16 +1,3 @@
import { Listener } from 'discord-akairo';
-import type EventEmitter from 'events';
-export class BushListener extends Listener {
- public constructor(
- id: string,
- options: {
- emitter: string | EventEmitter;
- event: string;
- type?: 'on' | 'once' | 'prependListener' | 'prependOnceListener';
- category?: string;
- }
- ) {
- super(id, options);
- }
-}
+export abstract class BushListener extends Listener {}
diff --git a/src/lib/extensions/discord-akairo/BushTask.ts b/src/lib/extensions/discord-akairo/BushTask.ts
index 9f5c0cd..1b70c88 100644
--- a/src/lib/extensions/discord-akairo/BushTask.ts
+++ b/src/lib/extensions/discord-akairo/BushTask.ts
@@ -1,3 +1,3 @@
import { Task } from 'discord-akairo';
-export class BushTask extends Task {}
+export abstract class BushTask extends Task {}
diff --git a/src/lib/extensions/discord.js/ExtendedGuild.ts b/src/lib/extensions/discord.js/ExtendedGuild.ts
index b8b7b22..c199899 100644
--- a/src/lib/extensions/discord.js/ExtendedGuild.ts
+++ b/src/lib/extensions/discord.js/ExtendedGuild.ts
@@ -1,7 +1,9 @@
import {
AllowedMentions,
banResponse,
+ colors,
dmResponse,
+ emojis,
permissionsResponse,
punishmentEntryRemove,
type BanResponse,
@@ -36,9 +38,10 @@ import {
type WebhookMessageOptions
} from 'discord.js';
import _ from 'lodash';
-import { Moderation } from '../../common/util/Moderation.js';
+import * as Moderation from '../../common/util/Moderation.js';
import { Guild as GuildDB } from '../../models/instance/Guild.js';
import { ModLogType } from '../../models/instance/ModLog.js';
+import { addOrRemoveFromArray, resolveNonCachedUser } from '../../utils/BushUtils.js';
declare module 'discord.js' {
export interface Guild {
@@ -152,7 +155,7 @@ export class ExtendedGuild extends Guild {
*/
public override async addFeature(feature: GuildFeatures, moderator?: GuildMember): Promise<GuildModel['enabledFeatures']> {
const features = await this.getSetting('enabledFeatures');
- const newFeatures = util.addOrRemoveFromArray('add', features, feature);
+ const newFeatures = addOrRemoveFromArray('add', features, feature);
return (await this.setSetting('enabledFeatures', newFeatures, moderator)).enabledFeatures;
}
@@ -163,7 +166,7 @@ export class ExtendedGuild extends Guild {
*/
public override async removeFeature(feature: GuildFeatures, moderator?: GuildMember): Promise<GuildModel['enabledFeatures']> {
const features = await this.getSetting('enabledFeatures');
- const newFeatures = util.addOrRemoveFromArray('remove', features, feature);
+ const newFeatures = addOrRemoveFromArray('remove', features, feature);
return (await this.setSetting('enabledFeatures', newFeatures, moderator)).enabledFeatures;
}
@@ -251,7 +254,7 @@ export class ExtendedGuild extends Guild {
*/
public override async error(title: string, message: string): Promise<void> {
void client.console.info(_.camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>'));
- void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: util.colors.error }] });
+ void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: colors.error }] });
}
/**
@@ -265,7 +268,7 @@ export class ExtendedGuild extends Guild {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
- const user = await util.resolveNonCachedUser(options.user);
+ const user = await resolveNonCachedUser(options.user);
const moderator = client.users.resolve(options.moderator ?? client.user!);
if (!user || !moderator) return banResponse.CANNOT_RESOLVE_USER;
@@ -408,7 +411,7 @@ export class ExtendedGuild extends Guild {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
- const user = await util.resolveNonCachedUser(options.user);
+ const user = await resolveNonCachedUser(options.user);
const moderator = client.users.resolve(options.moderator ?? client.user!);
if (!user || !moderator) return unbanResponse.CANNOT_RESOLVE_USER;
@@ -534,7 +537,7 @@ export class ExtendedGuild extends Guild {
author: { name: moderator.user.tag, icon_url: moderator.displayAvatarURL() },
title: `This channel has been ${options.unlock ? 'un' : ''}locked`,
description: options.reason ?? 'No reason provided',
- color: options.unlock ? util.colors.Green : util.colors.Red,
+ color: options.unlock ? colors.Green : colors.Red,
timestamp: new Date().toISOString()
}
]
@@ -600,16 +603,16 @@ export class ExtendedGuild extends Guild {
case MessageType.RecipientAdd: {
const recipient = rawQuote.mentions[0];
if (!recipient) {
- sendOptions.content = `${util.emojis.error} Cannot resolve recipient.`;
+ sendOptions.content = `${emojis.error} Cannot resolve recipient.`;
break;
}
if (quote.channel.isThread()) {
const recipientDisplay = quote.guild?.members.cache.get(recipient.id)?.displayName ?? recipient.username;
- sendOptions.content = `${util.emojis.join} ${displayName} added ${recipientDisplay} to the thread.`;
+ sendOptions.content = `${emojis.join} ${displayName} added ${recipientDisplay} to the thread.`;
} else {
// this should never happen
- sendOptions.content = `${util.emojis.join} ${displayName} added ${recipient.username} to the group.`;
+ sendOptions.content = `${emojis.join} ${displayName} added ${recipient.username} to the group.`;
}
break;
@@ -617,16 +620,16 @@ export class ExtendedGuild extends Guild {
case MessageType.RecipientRemove: {
const recipient = rawQuote.mentions[0];
if (!recipient) {
- sendOptions.content = `${util.emojis.error} Cannot resolve recipient.`;
+ sendOptions.content = `${emojis.error} Cannot resolve recipient.`;
break;
}
if (quote.channel.isThread()) {
const recipientDisplay = quote.guild?.members.cache.get(recipient.id)?.displayName ?? recipient.username;
- sendOptions.content = `${util.emojis.leave} ${displayName} removed ${recipientDisplay} from the thread.`;
+ sendOptions.content = `${emojis.leave} ${displayName} removed ${recipientDisplay} from the thread.`;
} else {
// this should never happen
- sendOptions.content = `${util.emojis.leave} ${displayName} removed ${recipient.username} from the group.`;
+ sendOptions.content = `${emojis.leave} ${displayName} removed ${recipient.username} from the group.`;
}
break;
@@ -661,7 +664,7 @@ export class ExtendedGuild extends Guild {
// this is the same way that the discord client decides what message to use.
const message = messages[timestamp % messages.length].replace(/{username}/g, displayName);
- sendOptions.content = `${util.emojis.join} ${message}`;
+ sendOptions.content = `${emojis.join} ${message}`;
break;
}
case MessageType.UserPremiumGuildSubscription:
@@ -717,7 +720,7 @@ export class ExtendedGuild extends Guild {
case MessageType.ChannelIconChange:
case MessageType.Call:
default:
- sendOptions.content = `${util.emojis.error} I cannot quote **${
+ sendOptions.content = `${emojis.error} I cannot quote **${
MessageType[quote.type] || quote.type
}** messages, please report this to my developers.`;
diff --git a/src/lib/extensions/discord.js/ExtendedGuildMember.ts b/src/lib/extensions/discord.js/ExtendedGuildMember.ts
index 28acc1a..ad29236 100644
--- a/src/lib/extensions/discord.js/ExtendedGuildMember.ts
+++ b/src/lib/extensions/discord.js/ExtendedGuildMember.ts
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
-import { BushClientEvents, Moderation, ModLogType, PunishmentTypeDM, Time } from '#lib';
+import { BushClientEvents, formatError, Moderation, ModLogType, PunishmentTypeDM, resolveNonCachedUser, Time } from '#lib';
import {
ChannelType,
GuildChannelResolvable,
@@ -148,7 +148,7 @@ export class ExtendedGuildMember extends GuildMember {
public override async bushWarn(options: BushPunishmentOptions): Promise<{ result: WarnResponse; caseNum: number | null }> {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
- const moderator = await util.resolveNonCachedUser(options.moderator ?? this.guild.members.me);
+ const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me);
if (!moderator) return { result: warnResponse.CANNOT_RESOLVE_USER, caseNum: null };
const ret = await (async (): Promise<{ result: WarnResponse; caseNum: number | null }> => {
@@ -195,7 +195,7 @@ export class ExtendedGuildMember extends GuildMember {
if (ifShouldAddRole !== true) return ifShouldAddRole;
let caseID: string | undefined = undefined;
- const moderator = await util.resolveNonCachedUser(options.moderator ?? this.guild.members.me);
+ const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me);
if (!moderator) return addRoleResponse.CANNOT_RESOLVE_USER;
const ret = await (async () => {
@@ -266,7 +266,7 @@ export class ExtendedGuildMember extends GuildMember {
if (ifShouldAddRole !== true) return ifShouldAddRole;
let caseID: string | undefined = undefined;
- const moderator = await util.resolveNonCachedUser(options.moderator ?? this.guild.members.me);
+ const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me);
if (!moderator) return removeRoleResponse.CANNOT_RESOLVE_USER;
const ret = await (async () => {
@@ -362,7 +362,7 @@ export class ExtendedGuildMember extends GuildMember {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
- const moderator = await util.resolveNonCachedUser(options.moderator ?? this.guild.members.me);
+ const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me);
if (!moderator) return muteResponse.CANNOT_RESOLVE_USER;
const ret = await (async () => {
@@ -448,7 +448,7 @@ export class ExtendedGuildMember extends GuildMember {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
- const moderator = await util.resolveNonCachedUser(options.moderator ?? this.guild.members.me);
+ const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me);
if (!moderator) return unmuteResponse.CANNOT_RESOLVE_USER;
const ret = await (async () => {
@@ -456,7 +456,7 @@ export class ExtendedGuildMember extends GuildMember {
const muteSuccess = await this.roles
.remove(muteRole, `[Unmute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`)
.catch(async (e) => {
- await client.console.warn('muteRoleAddError', util.formatError(e, true));
+ await client.console.warn('muteRoleAddError', formatError(e, true));
return false;
});
if (!muteSuccess) return unmuteResponse.ACTION_ERROR;
@@ -526,7 +526,7 @@ export class ExtendedGuildMember extends GuildMember {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
- const moderator = await util.resolveNonCachedUser(options.moderator ?? this.guild.members.me);
+ const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me);
if (!moderator) return kickResponse.CANNOT_RESOLVE_USER;
const ret = await (async () => {
// add modlog entry
@@ -580,7 +580,7 @@ export class ExtendedGuildMember extends GuildMember {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
- const moderator = await util.resolveNonCachedUser(options.moderator ?? this.guild.members.me);
+ const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me);
if (!moderator) return banResponse.CANNOT_RESOLVE_USER;
// ignore result, they should still be banned even if their mute cannot be removed
@@ -663,7 +663,7 @@ export class ExtendedGuildMember extends GuildMember {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
- const moderator = await util.resolveNonCachedUser(options.moderator ?? this.guild.members.me);
+ const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me);
if (!moderator) return blockResponse.CANNOT_RESOLVE_USER;
const ret = await (async () => {
@@ -754,7 +754,7 @@ export class ExtendedGuildMember extends GuildMember {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
- const moderator = await util.resolveNonCachedUser(options.moderator ?? this.guild.members.me);
+ const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me);
if (!moderator) return unblockResponse.CANNOT_RESOLVE_USER;
const ret = await (async () => {
@@ -839,7 +839,7 @@ export class ExtendedGuildMember extends GuildMember {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
- const moderator = await util.resolveNonCachedUser(options.moderator ?? this.guild.members.me);
+ const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me);
if (!moderator) return timeoutResponse.CANNOT_RESOLVE_USER;
const ret = await (async () => {
@@ -901,7 +901,7 @@ export class ExtendedGuildMember extends GuildMember {
let caseID: string | undefined = undefined;
let dmSuccessEvent: boolean | undefined = undefined;
- const moderator = await util.resolveNonCachedUser(options.moderator ?? this.guild.members.me);
+ const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me);
if (!moderator) return removeTimeoutResponse.CANNOT_RESOLVE_USER;
const ret = await (async () => {
diff --git a/src/lib/extensions/global.ts b/src/lib/extensions/global.ts
index a6f2b5a..d9cfaec 100644
--- a/src/lib/extensions/global.ts
+++ b/src/lib/extensions/global.ts
@@ -1,16 +1,11 @@
/* eslint-disable no-var */
-import type { BushClient, BushClientUtil } from '#lib';
+import type { BushClient } from '#lib';
declare global {
/**
* The bushbot client.
*/
var client: BushClient;
- /**
- * The bushbot client util.
- */
- var util: BushClientUtil;
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface ReadonlyArray<T> {
includes<S, R extends `${Extract<S, string>}`>(
diff --git a/src/lib/index.ts b/src/lib/index.ts
index 221f360..3e57f9e 100644
--- a/src/lib/index.ts
+++ b/src/lib/index.ts
@@ -4,12 +4,21 @@ export * from './common/ConfirmationPrompt.js';
export * from './common/DeleteButton.js';
export type { BushInspectOptions } from './common/typings/BushInspectOptions.js';
export type { CodeBlockLang } from './common/typings/CodeBlockLang.js';
-export * from './common/util/Arg.js';
-export * from './common/util/Format.js';
-export * from './common/util/Moderation.js';
+export * as Arg from './common/util/Arg.js';
+export * as Format from './common/util/Format.js';
+export * as Moderation from './common/util/Moderation.js';
+export type {
+ AppealButtonId,
+ CreateModLogEntryOptions,
+ CreatePunishmentEntryOptions,
+ PunishDMOptions,
+ PunishmentTypeDM,
+ PunishmentTypePresent,
+ RemovePunishmentEntryOptions,
+ SimpleCreateModLogEntryOptions
+} from './common/util/Moderation.js';
export * from './extensions/discord-akairo/BushArgumentTypeCaster.js';
export * from './extensions/discord-akairo/BushClient.js';
-export * from './extensions/discord-akairo/BushClientUtil.js';
export * from './extensions/discord-akairo/BushCommand.js';
export * from './extensions/discord-akairo/BushCommandHandler.js';
export * from './extensions/discord-akairo/BushInhibitor.js';
@@ -40,5 +49,5 @@ export * from './utils/AllowedMentions.js';
export * from './utils/BushCache.js';
export * from './utils/BushConstants.js';
export * from './utils/BushLogger.js';
+export * from './utils/BushUtils.js';
export * from './utils/CanvasProgressBar.js';
-export * from './utils/Config.js';
diff --git a/src/lib/utils/BushConstants.ts b/src/lib/utils/BushConstants.ts
index 8c3d27f..0f9311f 100644
--- a/src/lib/utils/BushConstants.ts
+++ b/src/lib/utils/BushConstants.ts
@@ -1,6 +1,11 @@
-import { ArgumentMatches, ArgumentTypes, BuiltInReasons, CommandHandlerEvents } from 'discord-akairo/dist/src/util/Constants.js';
+import deepLock from 'deep-lock';
+import {
+ ArgumentMatches as AkairoArgumentMatches,
+ ArgumentTypes as AkairoArgumentTypes,
+ BuiltInReasons,
+ CommandHandlerEvents as AkairoCommandHandlerEvents
+} from 'discord-akairo/dist/src/util/Constants.js';
import { Colors, GuildFeature } from 'discord.js';
-import { BushClientUtil } from '../extensions/discord-akairo/BushClientUtil.js';
const rawCapeUrl = 'https://raw.githubusercontent.com/NotEnoughUpdates/capes/master/';
@@ -49,207 +54,206 @@ export const enum Time {
Year = Day * 365.25 // average with leap years
}
-export class BushConstants {
- public static emojis = Object.freeze({
- success: '<:success:837109864101707807>',
- warn: '<:warn:848726900876247050>',
- error: '<:error:837123021016924261>',
- successFull: '<:success_full:850118767576088646>',
- warnFull: '<:warn_full:850118767391539312>',
- errorFull: '<:error_full:850118767295201350>',
- mad: '<:mad:783046135392239626>',
- join: '<:join:850198029809614858>',
- leave: '<:leave:850198048205307919>',
- loading: '<a:Loading:853419254619963392>',
- offlineCircle: '<:offline:787550565382750239>',
- dndCircle: '<:dnd:787550487633330176>',
- idleCircle: '<:idle:787550520956551218>',
- onlineCircle: '<:online:787550449435803658>',
- cross: '<:cross:878319362539421777>',
- check: '<:check:878320135297961995>'
- } as const);
-
- public static emojisRaw = Object.freeze({
- success: '837109864101707807',
- warn: '848726900876247050',
- error: '837123021016924261',
- successFull: '850118767576088646',
- warnFull: '850118767391539312',
- errorFull: '850118767295201350',
- mad: '783046135392239626',
- join: '850198029809614858',
- leave: '850198048205307919',
- loading: '853419254619963392',
- offlineCircle: '787550565382750239',
- dndCircle: '787550487633330176',
- idleCircle: '787550520956551218',
- onlineCircle: '787550449435803658',
- cross: '878319362539421777',
- check: '878320135297961995'
- } as const);
-
- public static colors = Object.freeze({
- default: 0x1fd8f1,
- error: 0xef4947,
- warn: 0xfeba12,
- success: 0x3bb681,
- info: 0x3b78ff,
- red: 0xff0000,
- blue: 0x0055ff,
- aqua: 0x00bbff,
- purple: 0x8400ff,
- blurple: 0x5440cd,
- newBlurple: 0x5865f2,
- pink: 0xff00e6,
- green: 0x00ff1e,
- darkGreen: 0x008f11,
- gold: 0xb59400,
- yellow: 0xffff00,
- white: 0xffffff,
- gray: 0xa6a6a6,
- lightGray: 0xcfcfcf,
- darkGray: 0x7a7a7a,
- black: 0x000000,
- orange: 0xe86100,
- ...Colors
- } as const);
-
- // Somewhat stolen from @Mzato0001
- public static timeUnits = BushClientUtil.deepFreeze({
- milliseconds: {
- match: / (?:(?<milliseconds>-?(?:\d+)?\.?\d+) *(?:milliseconds?|msecs?|ms))/im,
- value: Time.Millisecond
- },
- seconds: {
- match: / (?:(?<seconds>-?(?:\d+)?\.?\d+) *(?:seconds?|secs?|s))/im,
- value: Time.Second
- },
- minutes: {
- match: / (?:(?<minutes>-?(?:\d+)?\.?\d+) *(?:minutes?|mins?|m))/im,
- value: Time.Minute
- },
- hours: {
- match: / (?:(?<hours>-?(?:\d+)?\.?\d+) *(?:hours?|hrs?|h))/im,
- value: Time.Hour
- },
- days: {
- match: / (?:(?<days>-?(?:\d+)?\.?\d+) *(?:days?|d))/im,
- value: Time.Day
- },
- weeks: {
- match: / (?:(?<weeks>-?(?:\d+)?\.?\d+) *(?:weeks?|w))/im,
- value: Time.Week
- },
- months: {
- match: / (?:(?<months>-?(?:\d+)?\.?\d+) *(?:months?|mon|mo))/im,
- value: Time.Month
- },
- years: {
- match: / (?:(?<years>-?(?:\d+)?\.?\d+) *(?:years?|y))/im,
- value: Time.Year
- }
- } as const);
-
- public static regex = BushClientUtil.deepFreeze({
- snowflake: /^\d{15,21}$/im,
-
- discordEmoji: /<a?:(?<name>[a-zA-Z0-9_]+):(?<id>\d{15,21})>/im,
-
- /*
- * Taken with permission from Geek:
- * https://github.com/FireDiscordBot/bot/blob/5d1990e5f8b52fcc72261d786aa3c7c7c65ab5e8/lib/util/constants.ts#L276
- */
- /** **This has the global flag, make sure to handle it correctly.** */
- messageLink:
- /<?(?:ptb\.|canary\.|staging\.)?discord(?:app)?\.com?\/channels\/(?<guild_id>\d{15,21})\/(?<channel_id>\d{15,21})\/(?<message_id>\d{15,21})>?/gim
- } as const);
-
- /**
- * Maps the response from pronoundb.org to a readable format
+export const emojis = Object.freeze({
+ success: '<:success:837109864101707807>',
+ warn: '<:warn:848726900876247050>',
+ error: '<:error:837123021016924261>',
+ successFull: '<:success_full:850118767576088646>',
+ warnFull: '<:warn_full:850118767391539312>',
+ errorFull: '<:error_full:850118767295201350>',
+ mad: '<:mad:783046135392239626>',
+ join: '<:join:850198029809614858>',
+ leave: '<:leave:850198048205307919>',
+ loading: '<a:Loading:853419254619963392>',
+ offlineCircle: '<:offline:787550565382750239>',
+ dndCircle: '<:dnd:787550487633330176>',
+ idleCircle: '<:idle:787550520956551218>',
+ onlineCircle: '<:online:787550449435803658>',
+ cross: '<:cross:878319362539421777>',
+ check: '<:check:878320135297961995>'
+} as const);
+
+export const emojisRaw = Object.freeze({
+ success: '837109864101707807',
+ warn: '848726900876247050',
+ error: '837123021016924261',
+ successFull: '850118767576088646',
+ warnFull: '850118767391539312',
+ errorFull: '850118767295201350',
+ mad: '783046135392239626',
+ join: '850198029809614858',
+ leave: '850198048205307919',
+ loading: '853419254619963392',
+ offlineCircle: '787550565382750239',
+ dndCircle: '787550487633330176',
+ idleCircle: '787550520956551218',
+ onlineCircle: '787550449435803658',
+ cross: '878319362539421777',
+ check: '878320135297961995'
+} as const);
+
+export const colors = Object.freeze({
+ default: 0x1fd8f1,
+ error: 0xef4947,
+ warn: 0xfeba12,
+ success: 0x3bb681,
+ info: 0x3b78ff,
+ red: 0xff0000,
+ blue: 0x0055ff,
+ aqua: 0x00bbff,
+ purple: 0x8400ff,
+ blurple: 0x5440cd,
+ newBlurple: 0x5865f2,
+ pink: 0xff00e6,
+ green: 0x00ff1e,
+ darkGreen: 0x008f11,
+ gold: 0xb59400,
+ yellow: 0xffff00,
+ white: 0xffffff,
+ gray: 0xa6a6a6,
+ lightGray: 0xcfcfcf,
+ darkGray: 0x7a7a7a,
+ black: 0x000000,
+ orange: 0xe86100,
+ ...Colors
+} as const);
+
+// Somewhat stolen from @Mzato0001
+export const timeUnits = deepLock({
+ milliseconds: {
+ match: / (?:(?<milliseconds>-?(?:\d+)?\.?\d+) *(?:milliseconds?|msecs?|ms))/im,
+ value: Time.Millisecond
+ },
+ seconds: {
+ match: / (?:(?<seconds>-?(?:\d+)?\.?\d+) *(?:seconds?|secs?|s))/im,
+ value: Time.Second
+ },
+ minutes: {
+ match: / (?:(?<minutes>-?(?:\d+)?\.?\d+) *(?:minutes?|mins?|m))/im,
+ value: Time.Minute
+ },
+ hours: {
+ match: / (?:(?<hours>-?(?:\d+)?\.?\d+) *(?:hours?|hrs?|h))/im,
+ value: Time.Hour
+ },
+ days: {
+ match: / (?:(?<days>-?(?:\d+)?\.?\d+) *(?:days?|d))/im,
+ value: Time.Day
+ },
+ weeks: {
+ match: / (?:(?<weeks>-?(?:\d+)?\.?\d+) *(?:weeks?|w))/im,
+ value: Time.Week
+ },
+ months: {
+ match: / (?:(?<months>-?(?:\d+)?\.?\d+) *(?:months?|mon|mo))/im,
+ value: Time.Month
+ },
+ years: {
+ match: / (?:(?<years>-?(?:\d+)?\.?\d+) *(?:years?|y))/im,
+ value: Time.Year
+ }
+} as const);
+
+export const regex = deepLock({
+ snowflake: /^\d{15,21}$/im,
+
+ discordEmoji: /<a?:(?<name>[a-zA-Z0-9_]+):(?<id>\d{15,21})>/im,
+
+ /*
+ * Taken with permission from Geek:
+ * https://github.com/FireDiscordBot/bot/blob/5d1990e5f8b52fcc72261d786aa3c7c7c65ab5e8/lib/util/constants.ts#L276
*/
- public static pronounMapping = Object.freeze({
- unspecified: 'Unspecified',
- hh: 'He/Him',
- hi: 'He/It',
- hs: 'He/She',
- ht: 'He/They',
- ih: 'It/Him',
- ii: 'It/Its',
- is: 'It/She',
- it: 'It/They',
- shh: 'She/He',
- sh: 'She/Her',
- si: 'She/It',
- st: 'She/They',
- th: 'They/He',
- ti: 'They/It',
- ts: 'They/She',
- tt: 'They/Them',
- any: 'Any pronouns',
- other: 'Other pronouns',
- ask: 'Ask me my pronouns',
- avoid: 'Avoid pronouns, use my name'
- } as const);
+ /** **This has the global flag, make sure to handle it correctly.** */
+ messageLink:
+ /<?(?:ptb\.|canary\.|staging\.)?discord(?:app)?\.com?\/channels\/(?<guild_id>\d{15,21})\/(?<channel_id>\d{15,21})\/(?<message_id>\d{15,21})>?/gim
+} as const);
- /**
- * A bunch of mappings
- */
- public static mappings = BushClientUtil.deepFreeze({
- guilds: {
- bush: '516977525906341928',
- tree: '767448775450820639',
- staff: '784597260465995796',
- space_ship: '717176538717749358',
- sbr: '839287012409999391'
- },
-
- permissions: {
- CreateInstantInvite: { name: 'Create Invite', important: false },
- KickMembers: { name: 'Kick Members', important: true },
- BanMembers: { name: 'Ban Members', important: true },
- Administrator: { name: 'Administrator', important: true },
- ManageChannels: { name: 'Manage Channels', important: true },
- ManageGuild: { name: 'Manage Server', important: true },
- AddReactions: { name: 'Add Reactions', important: false },
- ViewAuditLog: { name: 'View Audit Log', important: true },
- PrioritySpeaker: { name: 'Priority Speaker', important: true },
- Stream: { name: 'Video', important: false },
- ViewChannel: { name: 'View Channel', important: false },
- SendMessages: { name: 'Send Messages', important: false },
- SendTTSMessages: { name: 'Send Text-to-Speech Messages', important: true },
- ManageMessages: { name: 'Manage Messages', important: true },
- EmbedLinks: { name: 'Embed Links', important: false },
- AttachFiles: { name: 'Attach Files', important: false },
- ReadMessageHistory: { name: 'Read Message History', important: false },
- MentionEveryone: { name: 'Mention @\u200Beveryone, @\u200Bhere, and All Roles', important: true }, // name has a zero-width space to prevent accidents
- UseExternalEmojis: { name: 'Use External Emoji', important: false },
- ViewGuildInsights: { name: 'View Server Insights', important: true },
- Connect: { name: 'Connect', important: false },
- Speak: { name: 'Speak', important: false },
- MuteMembers: { name: 'Mute Members', important: true },
- DeafenMembers: { name: 'Deafen Members', important: true },
- MoveMembers: { name: 'Move Members', important: true },
- UseVAD: { name: 'Use Voice Activity', important: false },
- ChangeNickname: { name: 'Change Nickname', important: false },
- ManageNicknames: { name: 'Change Nicknames', important: true },
- ManageRoles: { name: 'Manage Roles', important: true },
- ManageWebhooks: { name: 'Manage Webhooks', important: true },
- ManageEmojisAndStickers: { name: 'Manage Emojis and Stickers', important: true },
- UseApplicationCommands: { name: 'Use Slash Commands', important: false },
- RequestToSpeak: { name: 'Request to Speak', important: false },
- ManageEvents: { name: 'Manage Events', important: true },
- ManageThreads: { name: 'Manage Threads', important: true },
- CreatePublicThreads: { name: 'Create Public Threads', important: false },
- CreatePrivateThreads: { name: 'Create Private Threads', important: false },
- UseExternalStickers: { name: 'Use External Stickers', important: false },
- SendMessagesInThreads: { name: 'Send Messages In Threads', important: false },
- StartEmbeddedActivities: { name: 'Start Activities', important: false },
- ModerateMembers: { name: 'Timeout Members', important: true },
- UseEmbeddedActivities: { name: 'Use Activities', important: false }
- },
+/**
+ * Maps the response from pronoundb.org to a readable format
+ */
+export const pronounMapping = Object.freeze({
+ unspecified: 'Unspecified',
+ hh: 'He/Him',
+ hi: 'He/It',
+ hs: 'He/She',
+ ht: 'He/They',
+ ih: 'It/Him',
+ ii: 'It/Its',
+ is: 'It/She',
+ it: 'It/They',
+ shh: 'She/He',
+ sh: 'She/Her',
+ si: 'She/It',
+ st: 'She/They',
+ th: 'They/He',
+ ti: 'They/It',
+ ts: 'They/She',
+ tt: 'They/Them',
+ any: 'Any pronouns',
+ other: 'Other pronouns',
+ ask: 'Ask me my pronouns',
+ avoid: 'Avoid pronouns, use my name'
+} as const);
- // prettier-ignore
- features: {
+/**
+ * A bunch of mappings
+ */
+export const mappings = deepLock({
+ guilds: {
+ bush: '516977525906341928',
+ tree: '767448775450820639',
+ staff: '784597260465995796',
+ space_ship: '717176538717749358',
+ sbr: '839287012409999391'
+ },
+
+ permissions: {
+ CreateInstantInvite: { name: 'Create Invite', important: false },
+ KickMembers: { name: 'Kick Members', important: true },
+ BanMembers: { name: 'Ban Members', important: true },
+ Administrator: { name: 'Administrator', important: true },
+ ManageChannels: { name: 'Manage Channels', important: true },
+ ManageGuild: { name: 'Manage Server', important: true },
+ AddReactions: { name: 'Add Reactions', important: false },
+ ViewAuditLog: { name: 'View Audit Log', important: true },
+ PrioritySpeaker: { name: 'Priority Speaker', important: true },
+ Stream: { name: 'Video', important: false },
+ ViewChannel: { name: 'View Channel', important: false },
+ SendMessages: { name: 'Send Messages', important: false },
+ SendTTSMessages: { name: 'Send Text-to-Speech Messages', important: true },
+ ManageMessages: { name: 'Manage Messages', important: true },
+ EmbedLinks: { name: 'Embed Links', important: false },
+ AttachFiles: { name: 'Attach Files', important: false },
+ ReadMessageHistory: { name: 'Read Message History', important: false },
+ MentionEveryone: { name: 'Mention @\u200Beveryone, @\u200Bhere, and All Roles', important: true }, // name has a zero-width space to prevent accidents
+ UseExternalEmojis: { name: 'Use External Emoji', important: false },
+ ViewGuildInsights: { name: 'View Server Insights', important: true },
+ Connect: { name: 'Connect', important: false },
+ Speak: { name: 'Speak', important: false },
+ MuteMembers: { name: 'Mute Members', important: true },
+ DeafenMembers: { name: 'Deafen Members', important: true },
+ MoveMembers: { name: 'Move Members', important: true },
+ UseVAD: { name: 'Use Voice Activity', important: false },
+ ChangeNickname: { name: 'Change Nickname', important: false },
+ ManageNicknames: { name: 'Change Nicknames', important: true },
+ ManageRoles: { name: 'Manage Roles', important: true },
+ ManageWebhooks: { name: 'Manage Webhooks', important: true },
+ ManageEmojisAndStickers: { name: 'Manage Emojis and Stickers', important: true },
+ UseApplicationCommands: { name: 'Use Slash Commands', important: false },
+ RequestToSpeak: { name: 'Request to Speak', important: false },
+ ManageEvents: { name: 'Manage Events', important: true },
+ ManageThreads: { name: 'Manage Threads', important: true },
+ CreatePublicThreads: { name: 'Create Public Threads', important: false },
+ CreatePrivateThreads: { name: 'Create Private Threads', important: false },
+ UseExternalStickers: { name: 'Use External Stickers', important: false },
+ SendMessagesInThreads: { name: 'Send Messages In Threads', important: false },
+ StartEmbeddedActivities: { name: 'Start Activities', important: false },
+ ModerateMembers: { name: 'Timeout Members', important: true },
+ UseEmbeddedActivities: { name: 'Use Activities', important: false }
+ },
+
+ // prettier-ignore
+ features: {
[GuildFeature.Verified]: { name: 'Verified', important: true, emoji: '<:verified:850795049817473066>', weight: 0 },
[GuildFeature.Partnered]: { name: 'Partnered', important: true, emoji: '<:partneredServer:850794851955507240>', weight: 1 },
[GuildFeature.MoreStickers]: { name: 'More Stickers', important: true, emoji: null, weight: 2 },
@@ -283,206 +287,123 @@ export class BushConstants {
[GuildFeature.LinkedToHub]: { name: 'Linked To Hub', important: false, emoji: null, weight: 31 },
},
- regions: {
- 'automatic': ':united_nations: Automatic',
- 'brazil': ':flag_br: Brazil',
- 'europe': ':flag_eu: Europe',
- 'hongkong': ':flag_hk: Hongkong',
- 'india': ':flag_in: India',
- 'japan': ':flag_jp: Japan',
- 'russia': ':flag_ru: Russia',
- 'singapore': ':flag_sg: Singapore',
- 'southafrica': ':flag_za: South Africa',
- 'sydney': ':flag_au: Sydney',
- 'us-central': ':flag_us: US Central',
- 'us-east': ':flag_us: US East',
- 'us-south': ':flag_us: US South',
- 'us-west': ':flag_us: US West'
- },
-
- otherEmojis: {
- ServerBooster1: '<:serverBooster1:848740052091142145>',
- ServerBooster2: '<:serverBooster2:848740090506510388>',
- ServerBooster3: '<:serverBooster3:848740124992077835>',
- ServerBooster6: '<:serverBooster6:848740155245461514>',
- ServerBooster9: '<:serverBooster9:848740188846030889>',
- ServerBooster12: '<:serverBooster12:848740304365551668>',
- ServerBooster15: '<:serverBooster15:848740354890137680>',
- ServerBooster18: '<:serverBooster18:848740402886606868>',
- ServerBooster24: '<:serverBooster24:848740444628320256>',
- Nitro: '<:nitro:848740498054971432>',
- Booster: '<:booster:848747775020892200>',
- Owner: '<:owner:848746439311753286>',
- Admin: '<:admin:848963914628333598>',
- Superuser: '<:superUser:848947986326224926>',
- Developer: '<:developer:848954538111139871>',
- BushVerified: '<:verfied:853360152090771497>',
- BoostTier1: '<:boostitle:853363736679940127>',
- BoostTier2: '<:boostitle:853363752728789075>',
- BoostTier3: '<:boostitle:853363769132056627>',
- ChannelText: '<:text:853375537791893524>',
- ChannelNews: '<:announcements:853375553531674644>',
- ChannelVoice: '<:voice:853375566735212584>',
- ChannelStage: '<:stage:853375583521210468>',
- // ChannelStore: '<:store:853375601175691266>',
- ChannelCategory: '<:category:853375615260819476>',
- ChannelThread: '<:thread:865033845753249813>'
- },
-
- userFlags: {
- Staff: '<:discordEmployee:848742947826434079>',
- Partner: '<:partneredServerOwner:848743051593777152>',
- Hypesquad: '<:hypeSquadEvents:848743108283072553>',
- BugHunterLevel1: '<:bugHunter:848743239850393640>',
- HypeSquadOnlineHouse1: '<:hypeSquadBravery:848742910563844127>',
- HypeSquadOnlineHouse2: '<:hypeSquadBrilliance:848742840649646101>',
- HypeSquadOnlineHouse3: '<:hypeSquadBalance:848742877537370133>',
- PremiumEarlySupporter: '<:earlySupporter:848741030102171648>',
- TeamPseudoUser: 'TeamPseudoUser',
- BugHunterLevel2: '<:bugHunterGold:848743283080822794>',
- VerifiedBot: '<:verifiedbot_rebrand1:938928232667947028><:verifiedbot_rebrand2:938928355707879475>',
- VerifiedDeveloper: '<:earlyVerifiedBotDeveloper:848741079875846174>',
- CertifiedModerator: '<:discordCertifiedModerator:877224285901582366>',
- BotHTTPInteractions: 'BotHTTPInteractions',
- Spammer: 'Spammer'
- },
-
- status: {
- online: '<:online:848937141639577690>',
- idle: '<:idle:848937158261211146>',
- dnd: '<:dnd:848937173780135986>',
- offline: '<:offline:848939387277672448>',
- streaming: '<:streaming:848937187479519242>'
- },
-
- maybeNitroDiscrims: ['1111', '2222', '3333', '4444', '5555', '6666', '6969', '7777', '8888', '9999'],
-
- capes: [
- /* supporter capes */
- { name: 'patreon1', purchasable: false /* moulberry no longer offers */ },
- { name: 'patreon2', purchasable: false /* moulberry no longer offers */ },
- { name: 'fade', custom: `${rawCapeUrl}fade.gif`, purchasable: true },
- { name: 'lava', custom: `${rawCapeUrl}lava.gif`, purchasable: true },
- { name: 'mcworld', custom: `${rawCapeUrl}mcworld_compressed.gif`, purchasable: true },
- { name: 'negative', custom: `${rawCapeUrl}negative_compressed.gif`, purchasable: true },
- { name: 'space', custom: `${rawCapeUrl}space_compressed.gif`, purchasable: true },
- { name: 'void', custom: `${rawCapeUrl}void.gif`, purchasable: true },
- { name: 'tunnel', custom: `${rawCapeUrl}tunnel.gif`, purchasable: true },
- /* Staff capes */
- { name: 'contrib' },
- { name: 'mbstaff' },
- { name: 'ironmoon' },
- { name: 'gravy' },
- { name: 'nullzee' },
- /* partner capes */
- { name: 'thebakery' },
- { name: 'dsm' },
- { name: 'packshq' },
- { name: 'furf' },
- { name: 'skytils' },
- { name: 'sbp' },
- { name: 'subreddit_light' },
- { name: 'subreddit_dark' },
- { name: 'skyclient' },
- { name: 'sharex' },
- { name: 'sharex_white' },
- /* streamer capes */
- { name: 'alexxoffi' },
- { name: 'jakethybro' },
- { name: 'krusty' },
- { name: 'krusty_day' },
- { name: 'krusty_night' },
- { name: 'krusty_sunset' },
- { name: 'soldier' },
- { name: 'zera' },
- { name: 'secondpfirsisch' },
- { name: 'stormy_lh' }
- ].map((value, index) => ({ ...value, index })),
-
- roleMap: [
- { name: '*', id: '792453550768390194' },
- { name: 'Admin Perms', id: '746541309853958186' },
- { name: 'Sr. Moderator', id: '782803470205190164' },
- { name: 'Moderator', id: '737308259823910992' },
- { name: 'Helper', id: '737440116230062091' },
- { name: 'Trial Helper', id: '783537091946479636' },
- { name: 'Contributor', id: '694431057532944425' },
- { name: 'Giveaway Donor', id: '784212110263451649' },
- { name: 'Giveaway (200m)', id: '810267756426690601' },
- { name: 'Giveaway (100m)', id: '801444430522613802' },
- { name: 'Giveaway (50m)', id: '787497512981757982' },
- { name: 'Giveaway (25m)', id: '787497515771232267' },
- { name: 'Giveaway (10m)', id: '787497518241153025' },
- { name: 'Giveaway (5m)', id: '787497519768403989' },
- { name: 'Giveaway (1m)', id: '787497521084891166' },
- { name: 'Suggester', id: '811922322767609877' },
- { name: 'Partner', id: '767324547312779274' },
- { name: 'Level Locked', id: '784248899044769792' },
- { name: 'No Files', id: '786421005039173633' },
- { name: 'No Reactions', id: '786421270924361789' },
- { name: 'No Links', id: '786421269356740658' },
- { name: 'No Bots', id: '786804858765312030' },
- { name: 'No VC', id: '788850482554208267' },
- { name: 'No Giveaways', id: '808265422334984203' },
- { name: 'No Support', id: '790247359824396319' }
- ],
-
- roleWhitelist: {
- 'Partner': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'Suggester': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper', 'Trial Helper', 'Contributor'],
- 'Level Locked': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'No Files': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'No Reactions': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'No Links': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'No Bots': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'No VC': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'No Giveaways': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper'],
- 'No Support': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'Giveaway Donor': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'Giveaway (200m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'Giveaway (100m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'Giveaway (50m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'Giveaway (25m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'Giveaway (10m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'Giveaway (5m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
- 'Giveaway (1m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator']
- }
- } as const);
-
- public static ArgumentMatches = Object.freeze({
- ...ArgumentMatches
- } as const);
-
- public static ArgumentTypes = Object.freeze({
- ...ArgumentTypes,
- DURATION: 'duration',
- CONTENT_WITH_DURATION: 'contentWithDuration',
- PERMISSION: 'permission',
- SNOWFLAKE: 'snowflake',
- DISCORD_EMOJI: 'discordEmoji',
- ROLE_WITH_DURATION: 'roleWithDuration',
- ABBREVIATED_NUMBER: 'abbreviatedNumber',
- GLOBAL_USER: 'globalUser'
- } as const);
-
- public static BlockedReasons = Object.freeze({
- ...BuiltInReasons,
- DISABLED_GUILD: 'disabledGuild',
- DISABLED_GLOBAL: 'disabledGlobal',
- ROLE_BLACKLIST: 'roleBlacklist',
- USER_GUILD_BLACKLIST: 'userGuildBlacklist',
- USER_GLOBAL_BLACKLIST: 'userGlobalBlacklist',
- RESTRICTED_GUILD: 'restrictedGuild',
- CHANNEL_GUILD_BLACKLIST: 'channelGuildBlacklist',
- CHANNEL_GLOBAL_BLACKLIST: 'channelGlobalBlacklist',
- RESTRICTED_CHANNEL: 'restrictedChannel'
- } as const);
-
- public static CommandHandlerEvents = Object.freeze({
- ...CommandHandlerEvents
- } as const);
-
- public static moulberryBushRoleMap = BushClientUtil.deepFreeze([
+ regions: {
+ 'automatic': ':united_nations: Automatic',
+ 'brazil': ':flag_br: Brazil',
+ 'europe': ':flag_eu: Europe',
+ 'hongkong': ':flag_hk: Hongkong',
+ 'india': ':flag_in: India',
+ 'japan': ':flag_jp: Japan',
+ 'russia': ':flag_ru: Russia',
+ 'singapore': ':flag_sg: Singapore',
+ 'southafrica': ':flag_za: South Africa',
+ 'sydney': ':flag_au: Sydney',
+ 'us-central': ':flag_us: US Central',
+ 'us-east': ':flag_us: US East',
+ 'us-south': ':flag_us: US South',
+ 'us-west': ':flag_us: US West'
+ },
+
+ otherEmojis: {
+ ServerBooster1: '<:serverBooster1:848740052091142145>',
+ ServerBooster2: '<:serverBooster2:848740090506510388>',
+ ServerBooster3: '<:serverBooster3:848740124992077835>',
+ ServerBooster6: '<:serverBooster6:848740155245461514>',
+ ServerBooster9: '<:serverBooster9:848740188846030889>',
+ ServerBooster12: '<:serverBooster12:848740304365551668>',
+ ServerBooster15: '<:serverBooster15:848740354890137680>',
+ ServerBooster18: '<:serverBooster18:848740402886606868>',
+ ServerBooster24: '<:serverBooster24:848740444628320256>',
+ Nitro: '<:nitro:848740498054971432>',
+ Booster: '<:booster:848747775020892200>',
+ Owner: '<:owner:848746439311753286>',
+ Admin: '<:admin:848963914628333598>',
+ Superuser: '<:superUser:848947986326224926>',
+ Developer: '<:developer:848954538111139871>',
+ BushVerified: '<:verfied:853360152090771497>',
+ BoostTier1: '<:boostitle:853363736679940127>',
+ BoostTier2: '<:boostitle:853363752728789075>',
+ BoostTier3: '<:boostitle:853363769132056627>',
+ ChannelText: '<:text:853375537791893524>',
+ ChannelNews: '<:announcements:853375553531674644>',
+ ChannelVoice: '<:voice:853375566735212584>',
+ ChannelStage: '<:stage:853375583521210468>',
+ // ChannelStore: '<:store:853375601175691266>',
+ ChannelCategory: '<:category:853375615260819476>',
+ ChannelThread: '<:thread:865033845753249813>'
+ },
+
+ userFlags: {
+ Staff: '<:discordEmployee:848742947826434079>',
+ Partner: '<:partneredServerOwner:848743051593777152>',
+ Hypesquad: '<:hypeSquadEvents:848743108283072553>',
+ BugHunterLevel1: '<:bugHunter:848743239850393640>',
+ HypeSquadOnlineHouse1: '<:hypeSquadBravery:848742910563844127>',
+ HypeSquadOnlineHouse2: '<:hypeSquadBrilliance:848742840649646101>',
+ HypeSquadOnlineHouse3: '<:hypeSquadBalance:848742877537370133>',
+ PremiumEarlySupporter: '<:earlySupporter:848741030102171648>',
+ TeamPseudoUser: 'TeamPseudoUser',
+ BugHunterLevel2: '<:bugHunterGold:848743283080822794>',
+ VerifiedBot: '<:verifiedbot_rebrand1:938928232667947028><:verifiedbot_rebrand2:938928355707879475>',
+ VerifiedDeveloper: '<:earlyVerifiedBotDeveloper:848741079875846174>',
+ CertifiedModerator: '<:discordCertifiedModerator:877224285901582366>',
+ BotHTTPInteractions: 'BotHTTPInteractions',
+ Spammer: 'Spammer'
+ },
+
+ status: {
+ online: '<:online:848937141639577690>',
+ idle: '<:idle:848937158261211146>',
+ dnd: '<:dnd:848937173780135986>',
+ offline: '<:offline:848939387277672448>',
+ streaming: '<:streaming:848937187479519242>'
+ },
+
+ maybeNitroDiscrims: ['1111', '2222', '3333', '4444', '5555', '6666', '6969', '7777', '8888', '9999'],
+
+ capes: [
+ /* supporter capes */
+ { name: 'patreon1', purchasable: false /* moulberry no longer offers */ },
+ { name: 'patreon2', purchasable: false /* moulberry no longer offers */ },
+ { name: 'fade', custom: `${rawCapeUrl}fade.gif`, purchasable: true },
+ { name: 'lava', custom: `${rawCapeUrl}lava.gif`, purchasable: true },
+ { name: 'mcworld', custom: `${rawCapeUrl}mcworld_compressed.gif`, purchasable: true },
+ { name: 'negative', custom: `${rawCapeUrl}negative_compressed.gif`, purchasable: true },
+ { name: 'space', custom: `${rawCapeUrl}space_compressed.gif`, purchasable: true },
+ { name: 'void', custom: `${rawCapeUrl}void.gif`, purchasable: true },
+ { name: 'tunnel', custom: `${rawCapeUrl}tunnel.gif`, purchasable: true },
+ /* Staff capes */
+ { name: 'contrib' },
+ { name: 'mbstaff' },
+ { name: 'ironmoon' },
+ { name: 'gravy' },
+ { name: 'nullzee' },
+ /* partner capes */
+ { name: 'thebakery' },
+ { name: 'dsm' },
+ { name: 'packshq' },
+ { name: 'furf' },
+ { name: 'skytils' },
+ { name: 'sbp' },
+ { name: 'subreddit_light' },
+ { name: 'subreddit_dark' },
+ { name: 'skyclient' },
+ { name: 'sharex' },
+ { name: 'sharex_white' },
+ /* streamer capes */
+ { name: 'alexxoffi' },
+ { name: 'jakethybro' },
+ { name: 'krusty' },
+ { name: 'krusty_day' },
+ { name: 'krusty_night' },
+ { name: 'krusty_sunset' },
+ { name: 'soldier' },
+ { name: 'zera' },
+ { name: 'secondpfirsisch' },
+ { name: 'stormy_lh' }
+ ].map((value, index) => ({ ...value, index })),
+
+ roleMap: [
{ name: '*', id: '792453550768390194' },
{ name: 'Admin Perms', id: '746541309853958186' },
{ name: 'Sr. Moderator', id: '782803470205190164' },
@@ -508,8 +429,90 @@ export class BushConstants {
{ name: 'No VC', id: '788850482554208267' },
{ name: 'No Giveaways', id: '808265422334984203' },
{ name: 'No Support', id: '790247359824396319' }
- ] as const);
-}
-
-export type PronounCode = keyof typeof BushConstants['pronounMapping'];
-export type Pronoun = typeof BushConstants['pronounMapping'][PronounCode];
+ ],
+
+ roleWhitelist: {
+ 'Partner': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Suggester': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper', 'Trial Helper', 'Contributor'],
+ 'Level Locked': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'No Files': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'No Reactions': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'No Links': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'No Bots': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'No VC': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'No Giveaways': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator', 'Helper'],
+ 'No Support': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway Donor': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (200m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (100m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (50m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (25m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (10m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (5m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator'],
+ 'Giveaway (1m)': ['*', 'Admin Perms', 'Sr. Moderator', 'Moderator']
+ }
+} as const);
+
+export const ArgumentMatches = Object.freeze({
+ ...AkairoArgumentMatches
+} as const);
+
+export const ArgumentTypes = Object.freeze({
+ ...AkairoArgumentTypes,
+ DURATION: 'duration',
+ CONTENT_WITH_DURATION: 'contentWithDuration',
+ PERMISSION: 'permission',
+ SNOWFLAKE: 'snowflake',
+ DISCORD_EMOJI: 'discordEmoji',
+ ROLE_WITH_DURATION: 'roleWithDuration',
+ ABBREVIATED_NUMBER: 'abbreviatedNumber',
+ GLOBAL_USER: 'globalUser'
+} as const);
+
+export const BlockedReasons = Object.freeze({
+ ...BuiltInReasons,
+ DISABLED_GUILD: 'disabledGuild',
+ DISABLED_GLOBAL: 'disabledGlobal',
+ ROLE_BLACKLIST: 'roleBlacklist',
+ USER_GUILD_BLACKLIST: 'userGuildBlacklist',
+ USER_GLOBAL_BLACKLIST: 'userGlobalBlacklist',
+ RESTRICTED_GUILD: 'restrictedGuild',
+ CHANNEL_GUILD_BLACKLIST: 'channelGuildBlacklist',
+ CHANNEL_GLOBAL_BLACKLIST: 'channelGlobalBlacklist',
+ RESTRICTED_CHANNEL: 'restrictedChannel'
+} as const);
+
+export const CommandHandlerEvents = Object.freeze({
+ ...AkairoCommandHandlerEvents
+} as const);
+
+export const moulberryBushRoleMap = deepLock([
+ { name: '*', id: '792453550768390194' },
+ { name: 'Admin Perms', id: '746541309853958186' },
+ { name: 'Sr. Moderator', id: '782803470205190164' },
+ { name: 'Moderator', id: '737308259823910992' },
+ { name: 'Helper', id: '737440116230062091' },
+ { name: 'Trial Helper', id: '783537091946479636' },
+ { name: 'Contributor', id: '694431057532944425' },
+ { name: 'Giveaway Donor', id: '784212110263451649' },
+ { name: 'Giveaway (200m)', id: '810267756426690601' },
+ { name: 'Giveaway (100m)', id: '801444430522613802' },
+ { name: 'Giveaway (50m)', id: '787497512981757982' },
+ { name: 'Giveaway (25m)', id: '787497515771232267' },
+ { name: 'Giveaway (10m)', id: '787497518241153025' },
+ { name: 'Giveaway (5m)', id: '787497519768403989' },
+ { name: 'Giveaway (1m)', id: '787497521084891166' },
+ { name: 'Suggester', id: '811922322767609877' },
+ { name: 'Partner', id: '767324547312779274' },
+ { name: 'Level Locked', id: '784248899044769792' },
+ { name: 'No Files', id: '786421005039173633' },
+ { name: 'No Reactions', id: '786421270924361789' },
+ { name: 'No Links', id: '786421269356740658' },
+ { name: 'No Bots', id: '786804858765312030' },
+ { name: 'No VC', id: '788850482554208267' },
+ { name: 'No Giveaways', id: '808265422334984203' },
+ { name: 'No Support', id: '790247359824396319' }
+] as const);
+
+export type PronounCode = keyof typeof pronounMapping;
+export type Pronoun = typeof pronounMapping[PronounCode];
diff --git a/src/lib/utils/BushLogger.ts b/src/lib/utils/BushLogger.ts
index 073b8e2..7d42574 100644
--- a/src/lib/utils/BushLogger.ts
+++ b/src/lib/utils/BushLogger.ts
@@ -1,10 +1,11 @@
import chalk from 'chalk';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
-import { EmbedBuilder, Util, type Message, type PartialTextBasedChannelFields } from 'discord.js';
+import { EmbedBuilder, escapeMarkdown, PartialTextBasedChannelFields, type Message } from 'discord.js';
import repl, { REPLServer, REPL_MODE_STRICT } from 'repl';
import { WriteStream } from 'tty';
-import { inspect } from 'util';
import { type SendMessageType } from '../extensions/discord-akairo/BushClient.js';
+import { colors } from './BushConstants.js';
+import { getConfigChannel, inspect } from './BushUtils.js';
let REPL: REPLServer;
let replGone = false;
@@ -59,78 +60,78 @@ export function init() {
}
/**
- * Custom logging utility for the bot.
+ * Parses the content surrounding by `<<>>` and emphasizes it with the given color or by making it bold.
+ * @param content The content to parse.
+ * @param color The color to emphasize the content with.
+ * @param discordFormat Whether or not to format the content for discord.
+ * @returns The formatted content.
*/
-export class BushLogger {
- /**
- * Parses the content surrounding by `<<>>` and emphasizes it with the given color or by making it bold.
- * @param content The content to parse.
- * @param color The color to emphasize the content with.
- * @param discordFormat Whether or not to format the content for discord.
- * @returns The formatted content.
- */
- static #parseFormatting(
- content: any,
- color: 'blueBright' | 'blackBright' | 'redBright' | 'yellowBright' | 'greenBright' | '',
- discordFormat = false
- ): string | typeof content {
- if (typeof content !== 'string') return content;
- const newContent: Array<string> = content.split(/<<|>>/);
- const tempParsedArray: Array<string> = [];
- newContent.forEach((value, index) => {
- if (index % 2 !== 0) {
- tempParsedArray.push(discordFormat ? `**${Util.escapeMarkdown(value)}**` : color ? chalk[color](value) : value);
- } else {
- tempParsedArray.push(discordFormat ? Util.escapeMarkdown(value) : value);
- }
- });
- return tempParsedArray.join('');
- }
-
- /**
- * Inspects the content and returns a string.
- * @param content The content to inspect.
- * @param depth The depth the content will inspected. Defaults to `2`.
- * @param colors Whether or not to use colors in the output. Defaults to `true`.
- * @returns The inspected content.
- */
- static #inspectContent(content: any, depth = 2, colors = true): string {
- if (typeof content !== 'string') {
- return inspect(content, { depth, colors });
+function parseFormatting(
+ content: any,
+ color: 'blueBright' | 'blackBright' | 'redBright' | 'yellowBright' | 'greenBright' | '',
+ discordFormat = false
+): string | typeof content {
+ if (typeof content !== 'string') return content;
+ const newContent: Array<string> = content.split(/<<|>>/);
+ const tempParsedArray: Array<string> = [];
+ newContent.forEach((value, index) => {
+ if (index % 2 !== 0) {
+ tempParsedArray.push(discordFormat ? `**${escapeMarkdown(value)}**` : color ? chalk[color](value) : value);
+ } else {
+ tempParsedArray.push(discordFormat ? escapeMarkdown(value) : value);
}
- return content;
- }
+ });
+ return tempParsedArray.join('');
+}
- /**
- * Strips ANSI color codes from a string.
- * @param text The string to strip color codes from.
- * @returns A string without ANSI color codes.
- */
- static #stripColor(text: string): string {
- return text.replace(
- // eslint-disable-next-line no-control-regex
- /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
- ''
- );
+/**
+ * Inspects the content and returns a string.
+ * @param content The content to inspect.
+ * @param depth The depth the content will inspected. Defaults to `2`.
+ * @param colors Whether or not to use colors in the output. Defaults to `true`.
+ * @returns The inspected content.
+ */
+function inspectContent(content: any, depth = 2, colors = true): string {
+ if (typeof content !== 'string') {
+ return inspect(content, { depth, colors });
}
+ return content;
+}
- /**
- * Generates a formatted timestamp for logging.
- * @returns The formatted timestamp.
- */
- static #getTimeStamp(): string {
- const now = new Date();
- const hours = now.getHours();
- const minute = now.getMinutes();
- let hour = hours;
- let amOrPm: 'AM' | 'PM' = 'AM';
- if (hour > 12) {
- amOrPm = 'PM';
- hour = hour - 12;
- }
- return `${hour >= 10 ? hour : `0${hour}`}:${minute >= 10 ? minute : `0${minute}`} ${amOrPm}`;
+/**
+ * Strips ANSI color codes from a string.
+ * @param text The string to strip color codes from.
+ * @returns A string without ANSI color codes.
+ */
+function stripColor(text: string): string {
+ return text.replace(
+ // eslint-disable-next-line no-control-regex
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
+ ''
+ );
+}
+
+/**
+ * Generates a formatted timestamp for logging.
+ * @returns The formatted timestamp.
+ */
+function getTimeStamp(): string {
+ const now = new Date();
+ const hours = now.getHours();
+ const minute = now.getMinutes();
+ let hour = hours;
+ let amOrPm: 'AM' | 'PM' = 'AM';
+ if (hour > 12) {
+ amOrPm = 'PM';
+ hour = hour - 12;
}
+ return `${hour >= 10 ? hour : `0${hour}`}:${minute >= 10 ? minute : `0${minute}`} ${amOrPm}`;
+}
+/**
+ * Custom logging utility for the bot.
+ */
+export default {
/**
* Logs information. Highlight information by surrounding it in `<<>>`.
* @param header The header displayed before the content, displayed in cyan.
@@ -138,31 +139,31 @@ export class BushLogger {
* @param sendChannel Should this also be logged to discord? Defaults to false.
* @param depth The depth the content will inspected. Defaults to 0.
*/
- public static get log() {
- return BushLogger.info;
- }
+ get log() {
+ return this.info;
+ },
/**
* Sends a message to the log channel.
* @param message The parameter to pass to {@link PartialTextBasedChannelFields.send}.
* @returns The message sent.
*/
- public static async channelLog(message: SendMessageType): Promise<Message | null> {
- const channel = await util.getConfigChannel('log');
+ async channelLog(message: SendMessageType): Promise<Message | null> {
+ const channel = await getConfigChannel('log');
return await channel.send(message).catch(() => null);
- }
+ },
/**
* Sends a message to the error channel.
* @param message The parameter to pass to {@link PartialTextBasedChannelFields.send}.
* @returns The message sent.
*/
- public static async channelError(message: SendMessageType): Promise<Message | null> {
- const channel = await util.getConfigChannel('error');
+ async channelError(message: SendMessageType): Promise<Message | null> {
+ const channel = await getConfigChannel('error');
if (!channel) {
void this.error(
'BushLogger',
- `Could not find error channel, was originally going to send: \n${util.inspect(message, {
+ `Could not find error channel, was originally going to send: \n${inspect(message, {
colors: true
})}\n${new Error().stack?.substring(8)}`,
false
@@ -170,27 +171,27 @@ export class BushLogger {
return null;
}
return await channel.send(message);
- }
+ },
/**
* Logs debug information. Only works in dev is enabled in the config.
* @param content The content to log.
* @param depth The depth the content will inspected. Defaults to `0`.
*/
- public static debug(content: any, depth = 0): void {
+ debug(content: any, depth = 0): void {
if (!client.config.isDevelopment) return;
- const newContent = this.#inspectContent(content, depth, true);
- console.log(`${chalk.bgMagenta(this.#getTimeStamp())} ${chalk.magenta('[Debug]')} ${newContent}`);
- }
+ const newContent = inspectContent(content, depth, true);
+ console.log(`${chalk.bgMagenta(getTimeStamp())} ${chalk.magenta('[Debug]')} ${newContent}`);
+ },
/**
* Logs raw debug information. Only works in dev is enabled in the config.
* @param content The content to log.
*/
- public static debugRaw(...content: any): void {
+ debugRaw(...content: any): void {
if (!client.config.isDevelopment) return;
- console.log(`${chalk.bgMagenta(this.#getTimeStamp())} ${chalk.magenta('[Debug]')}`, ...content);
- }
+ console.log(`${chalk.bgMagenta(getTimeStamp())} ${chalk.magenta('[Debug]')}`, ...content);
+ },
/**
* Logs verbose information. Highlight information by surrounding it in `<<>>`.
@@ -199,19 +200,17 @@ export class BushLogger {
* @param sendChannel Should this also be logged to discord? Defaults to `false`.
* @param depth The depth the content will inspected. Defaults to `0`.
*/
- public static async verbose(header: string, content: any, sendChannel = false, depth = 0): Promise<void> {
+ async verbose(header: string, content: any, sendChannel = false, depth = 0): Promise<void> {
if (!client.config.logging.verbose) return;
- const newContent = this.#inspectContent(content, depth, true);
- console.log(
- `${chalk.bgGrey(this.#getTimeStamp())} ${chalk.grey(`[${header}]`)} ${this.#parseFormatting(newContent, 'blackBright')}`
- );
+ const newContent = inspectContent(content, depth, true);
+ console.log(`${chalk.bgGrey(getTimeStamp())} ${chalk.grey(`[${header}]`)} ${parseFormatting(newContent, 'blackBright')}`);
if (!sendChannel) return;
const embed = new EmbedBuilder()
- .setDescription(`**[${header}]** ${this.#parseFormatting(this.#stripColor(newContent), '', true)}`)
- .setColor(util.colors.gray)
+ .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`)
+ .setColor(colors.gray)
.setTimestamp();
await this.channelLog({ embeds: [embed] });
- }
+ },
/**
* Logs very verbose information. Highlight information by surrounding it in `<<>>`.
@@ -219,23 +218,23 @@ export class BushLogger {
* @param content The content to log, highlights displayed in bright black.
* @param depth The depth the content will inspected. Defaults to `0`.
*/
- public static async superVerbose(header: string, content: any, depth = 0): Promise<void> {
+ async superVerbose(header: string, content: any, depth = 0): Promise<void> {
if (!client.config.logging.verbose) return;
- const newContent = this.#inspectContent(content, depth, true);
+ const newContent = inspectContent(content, depth, true);
console.log(
- `${chalk.bgHex('#949494')(this.#getTimeStamp())} ${chalk.hex('#949494')(`[${header}]`)} ${chalk.hex('#b3b3b3')(newContent)}`
+ `${chalk.bgHex('#949494')(getTimeStamp())} ${chalk.hex('#949494')(`[${header}]`)} ${chalk.hex('#b3b3b3')(newContent)}`
);
- }
+ },
/**
* Logs raw very verbose information.
* @param header The header printed before the content, displayed in purple.
* @param content The content to log.
*/
- public static async superVerboseRaw(header: string, ...content: any[]): Promise<void> {
+ async superVerboseRaw(header: string, ...content: any[]): Promise<void> {
if (!client.config.logging.verbose) return;
- console.log(`${chalk.bgHex('#a3a3a3')(this.#getTimeStamp())} ${chalk.hex('#a3a3a3')(`[${header}]`)}`, ...content);
- }
+ console.log(`${chalk.bgHex('#a3a3a3')(getTimeStamp())} ${chalk.hex('#a3a3a3')(`[${header}]`)}`, ...content);
+ },
/**
* Logs information. Highlight information by surrounding it in `<<>>`.
@@ -244,19 +243,17 @@ export class BushLogger {
* @param sendChannel Should this also be logged to discord? Defaults to `false`.
* @param depth The depth the content will inspected. Defaults to `0`.
*/
- public static async info(header: string, content: any, sendChannel = true, depth = 0): Promise<void> {
+ async info(header: string, content: any, sendChannel = true, depth = 0): Promise<void> {
if (!client.config.logging.info) return;
- const newContent = this.#inspectContent(content, depth, true);
- console.log(
- `${chalk.bgCyan(this.#getTimeStamp())} ${chalk.cyan(`[${header}]`)} ${this.#parseFormatting(newContent, 'blueBright')}`
- );
+ const newContent = inspectContent(content, depth, true);
+ console.log(`${chalk.bgCyan(getTimeStamp())} ${chalk.cyan(`[${header}]`)} ${parseFormatting(newContent, 'blueBright')}`);
if (!sendChannel) return;
const embed = new EmbedBuilder()
- .setDescription(`**[${header}]** ${this.#parseFormatting(this.#stripColor(newContent), '', true)}`)
- .setColor(util.colors.info)
+ .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`)
+ .setColor(colors.info)
.setTimestamp();
await this.channelLog({ embeds: [embed] });
- }
+ },
/**
* Logs warnings. Highlight information by surrounding it in `<<>>`.
@@ -265,22 +262,19 @@ export class BushLogger {
* @param sendChannel Should this also be logged to discord? Defaults to `false`.
* @param depth The depth the content will inspected. Defaults to `0`.
*/
- public static async warn(header: string, content: any, sendChannel = true, depth = 0): Promise<void> {
- const newContent = this.#inspectContent(content, depth, true);
+ async warn(header: string, content: any, sendChannel = true, depth = 0): Promise<void> {
+ const newContent = inspectContent(content, depth, true);
console.warn(
- `${chalk.bgYellow(this.#getTimeStamp())} ${chalk.yellow(`[${header}]`)} ${this.#parseFormatting(
- newContent,
- 'yellowBright'
- )}`
+ `${chalk.bgYellow(getTimeStamp())} ${chalk.yellow(`[${header}]`)} ${parseFormatting(newContent, 'yellowBright')}`
);
if (!sendChannel) return;
const embed = new EmbedBuilder()
- .setDescription(`**[${header}]** ${this.#parseFormatting(this.#stripColor(newContent), '', true)}`)
- .setColor(util.colors.warn)
+ .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`)
+ .setColor(colors.warn)
.setTimestamp();
await this.channelError({ embeds: [embed] });
- }
+ },
/**
* Logs errors. Highlight information by surrounding it in `<<>>`.
@@ -289,22 +283,19 @@ export class BushLogger {
* @param sendChannel Should this also be logged to discord? Defaults to `false`.
* @param depth The depth the content will inspected. Defaults to `0`.
*/
- public static async error(header: string, content: any, sendChannel = true, depth = 0): Promise<void> {
- const newContent = this.#inspectContent(content, depth, true);
+ async error(header: string, content: any, sendChannel = true, depth = 0): Promise<void> {
+ const newContent = inspectContent(content, depth, true);
console.warn(
- `${chalk.bgRedBright(this.#getTimeStamp())} ${chalk.redBright(`[${header}]`)} ${this.#parseFormatting(
- newContent,
- 'redBright'
- )}`
+ `${chalk.bgRedBright(getTimeStamp())} ${chalk.redBright(`[${header}]`)} ${parseFormatting(newContent, 'redBright')}`
);
if (!sendChannel) return;
const embed = new EmbedBuilder()
- .setDescription(`**[${header}]** ${this.#parseFormatting(this.#stripColor(newContent), '', true)}`)
- .setColor(util.colors.error)
+ .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`)
+ .setColor(colors.error)
.setTimestamp();
await this.channelError({ embeds: [embed] });
return;
- }
+ },
/**
* Logs successes. Highlight information by surrounding it in `<<>>`.
@@ -313,21 +304,18 @@ export class BushLogger {
* @param sendChannel Should this also be logged to discord? Defaults to `false`.
* @param depth The depth the content will inspected. Defaults to `0`.
*/
- public static async success(header: string, content: any, sendChannel = true, depth = 0): Promise<void> {
- const newContent = this.#inspectContent(content, depth, true);
+ async success(header: string, content: any, sendChannel = true, depth = 0): Promise<void> {
+ const newContent = inspectContent(content, depth, true);
console.log(
- `${chalk.bgGreen(this.#getTimeStamp())} ${chalk.greenBright(`[${header}]`)} ${this.#parseFormatting(
- newContent,
- 'greenBright'
- )}`
+ `${chalk.bgGreen(getTimeStamp())} ${chalk.greenBright(`[${header}]`)} ${parseFormatting(newContent, 'greenBright')}`
);
if (!sendChannel) return;
const embed = new EmbedBuilder()
- .setDescription(`**[${header}]** ${this.#parseFormatting(this.#stripColor(newContent), '', true)}`)
- .setColor(util.colors.success)
+ .setDescription(`**[${header}]** ${parseFormatting(stripColor(newContent), '', true)}`)
+ .setColor(colors.success)
.setTimestamp();
await this.channelLog({ embeds: [embed] }).catch(() => {});
}
-}
+};
/** @typedef {PartialTextBasedChannelFields} vscodeDontDeleteMyImportTy */
diff --git a/src/lib/utils/BushUtils.ts b/src/lib/utils/BushUtils.ts
new file mode 100644
index 0000000..8a84d80
--- /dev/null
+++ b/src/lib/utils/BushUtils.ts
@@ -0,0 +1,1058 @@
+import {
+ Arg,
+ BushClient,
+ CodeBlockLang,
+ CommandMessage,
+ emojis,
+ Global,
+ Pronoun,
+ pronounMapping,
+ regex,
+ Shared,
+ SlashEditMessageType,
+ SlashSendMessageType,
+ timeUnits,
+ type BaseBushArgumentType,
+ type BushInspectOptions,
+ type GlobalCache,
+ type PronounCode,
+ type SharedCache,
+ type SlashMessage
+} from '#lib';
+import { humanizeDuration as humanizeDurationMod } from '@notenoughupdates/humanize-duration';
+import assert from 'assert';
+import { exec } from 'child_process';
+import deepLock from 'deep-lock';
+import { Util as AkairoUtil } from 'discord-akairo';
+import {
+ cleanCodeBlockContent,
+ Constants as DiscordConstants,
+ EmbedBuilder,
+ escapeCodeBlock,
+ GuildMember,
+ Message,
+ OAuth2Scopes,
+ PermissionFlagsBits,
+ PermissionsBitField,
+ Routes,
+ ThreadMember,
+ User,
+ UserResolvable,
+ type APIEmbed,
+ type APIMessage,
+ type CommandInteraction,
+ type InteractionReplyOptions,
+ type PermissionsString,
+ type Snowflake,
+ type TextChannel
+} from 'discord.js';
+import got from 'got';
+import _ from 'lodash';
+import { inspect as inspectUtil, promisify } from 'util';
+import CommandErrorListener from '../../listeners/commands/commandError.js';
+import * as Format from '../common/util/Format.js';
+
+export type StripPrivate<T> = { [K in keyof T]: T[K] extends Record<string, any> ? StripPrivate<T[K]> : T[K] };
+
+/**
+ * The hastebin urls used to post to hastebin, attempts to post in order
+ */
+const hasteURLs: string[] = [
+ 'https://hst.sh',
+ // 'https://hasteb.in',
+ 'https://hastebin.com',
+ 'https://mystb.in',
+ 'https://haste.clicksminuteper.net',
+ 'https://paste.pythondiscord.com',
+ 'https://haste.unbelievaboat.com'
+ // 'https://haste.tyman.tech'
+];
+
+/**
+ * Maps an array of user ids to user objects.
+ * @param ids The list of IDs to map
+ * @returns The list of users mapped
+ */
+export async function mapIDs(ids: Snowflake[]): Promise<User[]> {
+ return await Promise.all(ids.map((id) => client.users.fetch(id)));
+}
+
+/**
+ * Capitalizes the first letter of the given text
+ * @param text The text to capitalize
+ * @returns The capitalized text
+ */
+export function capitalize(text: string): string {
+ return text.charAt(0).toUpperCase() + text.slice(1);
+}
+
+/**
+ * Runs a shell command and gives the output
+ * @param command The shell command to run
+ * @returns The stdout and stderr of the shell command
+ */
+export async function shell(command: string): Promise<{ stdout: string; stderr: string }> {
+ return await promisify(exec)(command);
+}
+
+/**
+ * Posts text to hastebin
+ * @param content The text to post
+ * @returns The url of the posted text
+ */
+export async function haste(content: string, substr = false): Promise<HasteResults> {
+ let isSubstr = false;
+ if (content.length > 400_000 && !substr) {
+ void handleError('haste', new Error(`content over 400,000 characters (${content.length.toLocaleString()})`));
+ return { error: 'content too long' };
+ } else if (content.length > 400_000) {
+ content = content.substring(0, 400_000);
+ isSubstr = true;
+ }
+ for (const url of hasteURLs) {
+ try {
+ const res: HastebinRes = await got.post(`${url}/documents`, { body: content }).json();
+ return { url: `${url}/${res.key}`, error: isSubstr ? 'substr' : undefined };
+ } catch {
+ void client.console.error('haste', `Unable to upload haste to ${url}`);
+ }
+ }
+ return { error: 'unable to post' };
+}
+
+/**
+ * Resolves a user-provided string into a user object, if possible
+ * @param text The text to try and resolve
+ * @returns The user resolved or null
+ */
+export async function resolveUserAsync(text: string): Promise<User | null> {
+ const idReg = /\d{17,19}/;
+ const idMatch = text.match(idReg);
+ if (idMatch) {
+ try {
+ return await client.users.fetch(text as Snowflake);
+ } catch {
+ // pass
+ }
+ }
+ const mentionReg = /<@!?(?<id>\d{17,19})>/;
+ const mentionMatch = text.match(mentionReg);
+ if (mentionMatch) {
+ try {
+ return await client.users.fetch(mentionMatch.groups!.id as Snowflake);
+ } catch {
+ // pass
+ }
+ }
+ const user = client.users.cache.find((u) => u.username === text);
+ if (user) return user;
+ return null;
+}
+
+/**
+ * Appends the correct ordinal to the given number
+ * @param n The number to append an ordinal to
+ * @returns The number with the ordinal
+ */
+export function ordinal(n: number): string {
+ const s = ['th', 'st', 'nd', 'rd'],
+ v = n % 100;
+ return n + (s[(v - 20) % 10] || s[v] || s[0]);
+}
+
+/**
+ * Chunks an array to the specified size
+ * @param arr The array to chunk
+ * @param perChunk The amount of items per chunk
+ * @returns The chunked array
+ */
+export function chunk<T>(arr: T[], perChunk: number): T[][] {
+ return arr.reduce((all, one, i) => {
+ const ch: number = Math.floor(i / perChunk);
+ (all as any[])[ch] = [].concat(all[ch] || [], one as any);
+ return all;
+ }, []);
+}
+
+/**
+ * Fetches a user's uuid from the mojang api.
+ * @param username The username to get the uuid of.
+ * @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;
+ return dashed ? apiRes.uuid : apiRes.uuid.replace(/-/g, '');
+}
+
+/**
+ * Surrounds text in a code block with the specified language and puts it in a hastebin if its too long.
+ * * Embed Description Limit = 4096 characters
+ * * Embed Field Limit = 1024 characters
+ * @param code The content of the code block.
+ * @param length The maximum length of the code block.
+ * @param language The language of the code.
+ * @param substr Whether or not to substring the code if it is too long.
+ * @returns The generated code block
+ */
+export async function codeblock(
+ code: string,
+ length: number,
+ language: CodeBlockLang | '' = '',
+ substr = false
+): Promise<string> {
+ let hasteOut = '';
+ code = escapeCodeBlock(code);
+ const prefix = `\`\`\`${language}\n`;
+ const suffix = '\n```';
+ if (code.length + (prefix + suffix).length >= length) {
+ const haste_ = await haste(code, substr);
+ hasteOut = `Too large to display. ${
+ haste_.url
+ ? `Hastebin: ${haste_.url}${language ? `.${language}` : ''}${haste_.error ? ` - ${haste_.error}` : ''}`
+ : `${emojis.error} Hastebin: ${haste_.error}`
+ }`;
+ }
+
+ const FormattedHaste = hasteOut.length ? `\n${hasteOut}` : '';
+ const shortenedCode = hasteOut ? code.substring(0, length - (prefix + FormattedHaste + suffix).length) : code;
+ const code3 = code.length ? prefix + shortenedCode + suffix + FormattedHaste : prefix + suffix;
+ if (code3.length > length) {
+ void client.console.warn(`codeblockError`, `Required Length: ${length}. Actual Length: ${code3.length}`, true);
+ void client.console.warn(`codeblockError`, code3, true);
+ throw new Error('code too long');
+ }
+ return code3;
+}
+
+/**
+ * Generate defaults for {@link inspect}.
+ * @param options The options to create defaults with.
+ * @returns The default options combined with the specified options.
+ */
+function getDefaultInspectOptions(options?: BushInspectOptions): BushInspectOptions {
+ return {
+ showHidden: options?.showHidden ?? false,
+ depth: options?.depth ?? 2,
+ colors: options?.colors ?? false,
+ customInspect: options?.customInspect ?? true,
+ showProxy: options?.showProxy ?? false,
+ maxArrayLength: options?.maxArrayLength ?? Infinity,
+ maxStringLength: options?.maxStringLength ?? Infinity,
+ breakLength: options?.breakLength ?? 80,
+ compact: options?.compact ?? 3,
+ sorted: options?.sorted ?? false,
+ getters: options?.getters ?? true
+ };
+}
+
+/**
+ * Maps the key of a credential with a readable version when redacting.
+ * @param key The key of the credential.
+ * @returns The readable version of the key or the original key if there isn't a mapping.
+ */
+function mapCredential(key: string): string {
+ const mapping = {
+ token: 'Main Token',
+ devToken: 'Dev Token',
+ betaToken: 'Beta Token',
+ hypixelApiKey: 'Hypixel Api Key',
+ wolframAlphaAppId: 'Wolfram|Alpha App ID',
+ dbPassword: 'Database Password'
+ };
+ return mapping[key as keyof typeof mapping] || key;
+}
+
+/**
+ * Redacts credentials from a string.
+ * @param text The text to redact credentials from.
+ * @returns The redacted text.
+ */
+export function redact(text: string) {
+ for (const credentialName in { ...client.config.credentials, dbPassword: client.config.db.password }) {
+ const credential = { ...client.config.credentials, dbPassword: client.config.db.password }[
+ credentialName as keyof typeof client.config.credentials
+ ];
+ const replacement = mapCredential(credentialName);
+ const escapeRegex = /[.*+?^${}()|[\]\\]/g;
+ text = text.replace(new RegExp(credential.toString().replace(escapeRegex, '\\$&'), 'g'), `[${replacement} Omitted]`);
+ text = text.replace(
+ new RegExp([...credential.toString()].reverse().join('').replace(escapeRegex, '\\$&'), 'g'),
+ `[${replacement} Omitted]`
+ );
+ }
+ return text;
+}
+
+/**
+ * Uses {@link inspect} with custom defaults.
+ * @param object - The object you would like to inspect.
+ * @param options - The options you would like to use to inspect the object.
+ * @returns The inspected object.
+ */
+export function inspect(object: any, options?: BushInspectOptions): string {
+ const optionsWithDefaults = getDefaultInspectOptions(options);
+
+ if (!optionsWithDefaults.inspectStrings && typeof object === 'string') return object;
+
+ return inspectUtil(object, optionsWithDefaults);
+}
+
+/**
+ * Takes an any value, inspects it, redacts credentials, and puts it in a codeblock
+ * (and uploads to hast if the content is too long).
+ * @param input The object to be inspect, redacted, and put into a codeblock.
+ * @param language The language to make the codeblock.
+ * @param inspectOptions The options for {@link BushClientUtil.inspect}.
+ * @param length The maximum length that the codeblock can be.
+ * @returns The generated codeblock.
+ */
+export async function inspectCleanRedactCodeblock(
+ input: any,
+ language?: CodeBlockLang | '',
+ inspectOptions?: BushInspectOptions,
+ length = 1024
+) {
+ input = inspect(input, inspectOptions ?? undefined);
+ if (inspectOptions) inspectOptions.inspectStrings = undefined;
+ input = cleanCodeBlockContent(input);
+ input = redact(input);
+ return codeblock(input, length, language, true);
+}
+
+/**
+ * Takes an any value, inspects it, redacts credentials, and uploads it to haste.
+ * @param input The object to be inspect, redacted, and upload.
+ * @param inspectOptions The options for {@link BushClientUtil.inspect}.
+ * @returns The {@link HasteResults}.
+ */
+export async function inspectCleanRedactHaste(input: any, inspectOptions?: BushInspectOptions): Promise<HasteResults> {
+ input = inspect(input, inspectOptions ?? undefined);
+ input = redact(input);
+ return haste(input, true);
+}
+
+/**
+ * Takes an any value, inspects it and redacts credentials.
+ * @param input The object to be inspect and redacted.
+ * @param inspectOptions The options for {@link BushClientUtil.inspect}.
+ * @returns The redacted and inspected object.
+ */
+export function inspectAndRedact(input: any, inspectOptions?: BushInspectOptions): string {
+ input = inspect(input, inspectOptions ?? undefined);
+ return redact(input);
+}
+
+/**
+ * Responds to a slash command interaction.
+ * @param interaction The interaction to respond to.
+ * @param responseOptions The options for the response.
+ * @returns The message sent.
+ */
+export async function slashRespond(
+ interaction: CommandInteraction,
+ responseOptions: SlashSendMessageType | SlashEditMessageType
+): Promise<Message | APIMessage | undefined> {
+ const newResponseOptions = typeof responseOptions === 'string' ? { content: responseOptions } : responseOptions;
+ if (interaction.replied || interaction.deferred) {
+ delete (newResponseOptions as InteractionReplyOptions).ephemeral; // Cannot change a preexisting message to be ephemeral
+ return (await interaction.editReply(newResponseOptions)) as Message | APIMessage;
+ } else {
+ await interaction.reply(newResponseOptions);
+ return await interaction.fetchReply().catch(() => undefined);
+ }
+}
+
+/**
+ * Gets a a configured channel as a TextChannel.
+ * @channel The channel to retrieve.
+ */
+export async function getConfigChannel(channel: keyof typeof client['config']['channels']): Promise<TextChannel> {
+ return (await client.channels.fetch(client.config.channels[channel])) as unknown as TextChannel;
+}
+
+/**
+ * Takes an array and combines the elements using the supplied conjunction.
+ * @param array The array to combine.
+ * @param conjunction The conjunction to use.
+ * @param ifEmpty What to return if the array is empty.
+ * @returns The combined elements or `ifEmpty`.
+ *
+ * @example
+ * const permissions = oxford(['Administrator', 'SendMessages', 'ManageMessages'], 'and', 'none');
+ * console.log(permissions); // Administrator, SendMessages and ManageMessages
+ */
+export function oxford(array: string[], conjunction: string, ifEmpty?: string): string | undefined {
+ const l = array.length;
+ if (!l) return ifEmpty;
+ if (l < 2) return array[0];
+ if (l < 3) return array.join(` ${conjunction} `);
+ array = array.slice();
+ array[l - 1] = `${conjunction} ${array[l - 1]}`;
+ return array.join(', ');
+}
+
+/**
+ * Get the global cache.
+ */
+export function getGlobal(): GlobalCache;
+/**
+ * Get a key from the global cache.
+ * @param key The key to get in the global cache.
+ */
+export function getGlobal<K extends keyof GlobalCache>(key: K): GlobalCache[K];
+export function getGlobal(key?: keyof GlobalCache) {
+ return key ? client.cache.global[key] : client.cache.global;
+}
+
+export function getShared(): SharedCache;
+export function getShared<K extends keyof SharedCache>(key: K): SharedCache[K];
+export function getShared(key?: keyof SharedCache) {
+ return key ? client.cache.shared[key] : client.cache.shared;
+}
+
+/**
+ * Add or remove an element from an array stored in the Globals database.
+ * @param action Either `add` or `remove` an element.
+ * @param key The key of the element in the global cache to update.
+ * @param value The value to add/remove from the array.
+ */
+export async function insertOrRemoveFromGlobal<K extends keyof typeof client['cache']['global']>(
+ action: 'add' | 'remove',
+ key: K,
+ value: typeof client['cache']['global'][K][0]
+): Promise<Global | void> {
+ const row =
+ (await Global.findByPk(client.config.environment)) ?? (await Global.create({ environment: client.config.environment }));
+ const oldValue: any[] = row[key];
+ const newValue = addOrRemoveFromArray(action, oldValue, value);
+ row[key] = newValue;
+ client.cache.global[key] = newValue;
+ return await row.save().catch((e) => handleError('insertOrRemoveFromGlobal', e));
+}
+
+/**
+ * Add or remove an element from an array stored in the Shared database.
+ * @param action Either `add` or `remove` an element.
+ * @param key The key of the element in the shared cache to update.
+ * @param value The value to add/remove from the array.
+ */
+export async function insertOrRemoveFromShared<
+ K extends Exclude<keyof typeof client['cache']['shared'], 'badWords' | 'autoBanCode'>
+>(action: 'add' | 'remove', key: K, value: typeof client['cache']['shared'][K][0]): Promise<Shared | void> {
+ const row = (await Shared.findByPk(0)) ?? (await Shared.create());
+ const oldValue: any[] = row[key];
+ const newValue = addOrRemoveFromArray(action, oldValue, value);
+ row[key] = newValue;
+ client.cache.shared[key] = newValue;
+ return await row.save().catch((e) => handleError('insertOrRemoveFromShared', e));
+}
+
+/**
+ * Updates an element in the Globals database.
+ * @param key The key in the global cache to update.
+ * @param value The value to set the key to.
+ */
+export async function setGlobal<K extends keyof typeof client['cache']['global']>(
+ key: K,
+ value: typeof client['cache']['global'][K]
+): Promise<Global | void> {
+ const row =
+ (await Global.findByPk(client.config.environment)) ?? (await Global.create({ environment: client.config.environment }));
+ row[key] = value;
+ client.cache.global[key] = value;
+ return await row.save().catch((e) => handleError('setGlobal', e));
+}
+
+/**
+ * Updates an element in the Shared database.
+ * @param key The key in the shared cache to update.
+ * @param value The value to set the key to.
+ */
+export async function setShared<K extends Exclude<keyof typeof client['cache']['shared'], 'badWords' | 'autoBanCode'>>(
+ key: K,
+ value: typeof client['cache']['shared'][K]
+): Promise<Shared | void> {
+ const row = (await Shared.findByPk(0)) ?? (await Shared.create());
+ row[key] = value;
+ client.cache.shared[key] = value;
+ return await row.save().catch((e) => handleError('setShared', e));
+}
+
+/**
+ * Add or remove an item from an array. All duplicates will be removed.
+ * @param action Either `add` or `remove` an element.
+ * @param array The array to add/remove an element from.
+ * @param value The element to add/remove from the array.
+ */
+export function addOrRemoveFromArray<T>(action: 'add' | 'remove', array: T[], value: T): T[] {
+ const set = new Set(array);
+ action === 'add' ? set.add(value) : set.delete(value);
+ return [...set];
+}
+
+/**
+ * Remove an item from an array. All duplicates will be removed.
+ * @param array The array to remove an element from.
+ * @param value The element to remove from the array.
+ */
+export function removeFromArray<T>(array: T[], value: T): T[] {
+ return addOrRemoveFromArray('remove', array, value);
+}
+
+/**
+ * Add an item from an array. All duplicates will be removed.
+ * @param array The array to add an element to.
+ * @param value The element to add to the array.
+ */
+export function addToArray<T>(array: T[], value: T): T[] {
+ return addOrRemoveFromArray('add', array, value);
+}
+
+/**
+ * Surrounds a string to the begging an end of each element in an array.
+ * @param array The array you want to surround.
+ * @param surroundChar1 The character placed in the beginning of the element.
+ * @param surroundChar2 The character placed in the end of the element. Defaults to `surroundChar1`.
+ */
+export function surroundArray(array: string[], surroundChar1: string, surroundChar2?: string): string[] {
+ return array.map((a) => `${surroundChar1}${a}${surroundChar2 ?? surroundChar1}`);
+}
+
+/**
+ * Gets the duration from a specified string.
+ * @param content The string to look for a duration in.
+ * @param remove Whether or not to remove the duration from the original string.
+ * @returns The {@link ParsedDuration}.
+ */
+export function parseDuration(content: string, remove = true): ParsedDuration {
+ if (!content) return { duration: 0, content: null };
+
+ // eslint-disable-next-line prefer-const
+ let duration: number | null = null;
+ // Try to reduce false positives by requiring a space before the duration, this makes sure it still matches if it is
+ // in the beginning of the argument
+ let contentWithoutTime = ` ${content}`;
+
+ for (const unit in timeUnits) {
+ const regex = timeUnits[unit as keyof typeof timeUnits].match;
+ const match = regex.exec(contentWithoutTime);
+ const value = Number(match?.groups?.[unit]);
+ if (!isNaN(value)) duration! += value * timeUnits[unit as keyof typeof timeUnits].value;
+
+ if (remove) contentWithoutTime = contentWithoutTime.replace(regex, '');
+ }
+ // remove the space added earlier
+ if (contentWithoutTime.startsWith(' ')) contentWithoutTime.replace(' ', '');
+ return { duration, content: contentWithoutTime };
+}
+
+/**
+ * Converts a duration in milliseconds to a human readable form.
+ * @param duration The duration in milliseconds to convert.
+ * @param largest The maximum number of units to display for the duration.
+ * @param round Whether or not to round the smallest unit displayed.
+ * @returns A humanized string of the duration.
+ */
+export function humanizeDuration(duration: number, largest?: number, round = true): string {
+ if (largest) return humanizeDurationMod(duration, { language: 'en', maxDecimalPoints: 2, largest, round })!;
+ else return humanizeDurationMod(duration, { language: 'en', maxDecimalPoints: 2, round })!;
+}
+
+/**
+ * Creates a formatted relative timestamp from a duration in milliseconds.
+ * @param duration The duration in milliseconds.
+ * @returns The formatted relative timestamp.
+ */
+export function timestampDuration(duration: number): string {
+ return `<t:${Math.round(new Date().getTime() / 1_000 + duration / 1_000)}:R>`;
+}
+
+/**
+ * Creates a timestamp from a date.
+ * @param date The date to create a timestamp from.
+ * @param style The style of the timestamp.
+ * @returns The formatted timestamp.
+ *
+ * @see
+ * **Styles:**
+ * - **t**: Short Time ex. `16:20`
+ * - **T**: Long Time ex. `16:20:30 `
+ * - **d**: Short Date ex. `20/04/2021`
+ * - **D**: Long Date ex. `20 April 2021`
+ * - **f**: Short Date/Time ex. `20 April 2021 16:20`
+ * - **F**: Long Date/Time ex. `Tuesday, 20 April 2021 16:20`
+ * - **R**: Relative Time ex. `2 months ago`
+ */
+export function timestamp<D extends Date | undefined | null>(
+ date: D,
+ style: TimestampStyle = 'f'
+): D extends Date ? string : undefined {
+ if (!date) return date as unknown as D extends Date ? string : undefined;
+ return `<t:${Math.round(date.getTime() / 1_000)}:${style}>` as unknown as D extends Date ? string : undefined;
+}
+
+/**
+ * Creates a human readable representation between a date and the current time.
+ * @param date The date to be compared with the current time.
+ * @param largest The maximum number of units to display for the duration.
+ * @param round Whether or not to round the smallest unit displayed.
+ * @returns A humanized string of the delta.
+ */
+export function dateDelta(date: Date, largest?: number, round = true): string {
+ return humanizeDuration(new Date().getTime() - date.getTime(), largest ?? 3, round);
+}
+
+/**
+ * Combines {@link timestamp} and {@link dateDelta}
+ * @param date The date to be compared with the current time.
+ * @param style The style of the timestamp.
+ * @returns The formatted timestamp.
+ *
+ * @see
+ * **Styles:**
+ * - **t**: Short Time ex. `16:20`
+ * - **T**: Long Time ex. `16:20:30 `
+ * - **d**: Short Date ex. `20/04/2021`
+ * - **D**: Long Date ex. `20 April 2021`
+ * - **f**: Short Date/Time ex. `20 April 2021 16:20`
+ * - **F**: Long Date/Time ex. `Tuesday, 20 April 2021 16:20`
+ * - **R**: Relative Time ex. `2 months ago`
+ */
+export function timestampAndDelta(date: Date, style: TimestampStyle = 'D'): string {
+ return `${timestamp(date, style)} (${dateDelta(date)} ago)`;
+}
+
+/**
+ * Convert a hex code to an rbg value.
+ * @param hex The hex code to convert.
+ * @returns The rbg value.
+ */
+export function hexToRgb(hex: string): string {
+ const arrBuff = new ArrayBuffer(4);
+ const vw = new DataView(arrBuff);
+ vw.setUint32(0, parseInt(hex, 16), false);
+ const arrByte = new Uint8Array(arrBuff);
+
+ return `${arrByte[1]}, ${arrByte[2]}, ${arrByte[3]}`;
+}
+
+/**
+ * Wait an amount in milliseconds.
+ * @returns A promise that resolves after the specified amount of milliseconds
+ */
+export const sleep = promisify(setTimeout);
+
+/**
+ * Send a message in the error logging channel and console for an error.
+ * @param context
+ * @param error
+ */
+export async function handleError(context: string, error: Error) {
+ await client.console.error(_.camelCase(context), `An error occurred:\n${formatError(error, false)}`, false);
+ await client.console.channelError({
+ embeds: await CommandErrorListener.generateErrorEmbed({ type: 'unhandledRejection', error: error, context })
+ });
+}
+
+/**
+ * Fetches a user from discord.
+ * @param user The user to fetch
+ * @returns Undefined if the user is not found, otherwise the user.
+ */
+export async function resolveNonCachedUser(user: UserResolvable | undefined | null): Promise<User | undefined> {
+ if (user == null) return undefined;
+ const resolvedUser =
+ user instanceof User
+ ? user
+ : user instanceof GuildMember
+ ? user.user
+ : user instanceof ThreadMember
+ ? user.user
+ : user instanceof Message
+ ? user.author
+ : undefined;
+
+ return resolvedUser ?? (await client.users.fetch(user as Snowflake).catch(() => undefined));
+}
+
+/**
+ * Get the pronouns of a discord user from pronoundb.org
+ * @param user The user to retrieve the promises of.
+ * @returns The human readable pronouns of the user, or undefined if they do not have any.
+ */
+export async function getPronounsOf(user: User | Snowflake): Promise<Pronoun | undefined> {
+ const _user = await 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()
+ .catch(() => undefined)) as { pronouns: PronounCode } | undefined;
+
+ if (!apiRes) return undefined;
+ assert(apiRes.pronouns);
+
+ return pronounMapping[apiRes.pronouns!]!;
+}
+
+/**
+ * List the methods of an object.
+ * @param obj The object to get the methods of.
+ * @returns A string with each method on a new line.
+ */
+export function getMethods(obj: Record<string, any>): string {
+ // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class/31055217#31055217
+ let props: string[] = [];
+ let obj_: Record<string, any> = new Object(obj);
+
+ do {
+ const l = Object.getOwnPropertyNames(obj_)
+ .concat(Object.getOwnPropertySymbols(obj_).map((s) => s.toString()))
+ .sort()
+ .filter(
+ (p, i, arr) =>
+ typeof Object.getOwnPropertyDescriptor(obj_, p)?.['get'] !== 'function' && // ignore getters
+ typeof Object.getOwnPropertyDescriptor(obj_, p)?.['set'] !== 'function' && // ignore setters
+ typeof obj_[p] === 'function' && // only the methods
+ p !== 'constructor' && // not the constructor
+ (i == 0 || p !== arr[i - 1]) && // not overriding in this prototype
+ props.indexOf(p) === -1 // not overridden in a child
+ );
+
+ const reg = /\(([\s\S]*?)\)/;
+ props = props.concat(
+ l.map(
+ (p) =>
+ `${obj_[p] && obj_[p][Symbol.toStringTag] === 'AsyncFunction' ? 'async ' : ''}function ${p}(${
+ reg.exec(obj_[p].toString())?.[1]
+ ? reg
+ .exec(obj_[p].toString())?.[1]
+ .split(', ')
+ .map((arg) => arg.split('=')[0].trim())
+ .join(', ')
+ : ''
+ });`
+ )
+ );
+ } while (
+ (obj_ = Object.getPrototypeOf(obj_)) && // walk-up the prototype chain
+ Object.getPrototypeOf(obj_) // not the the Object prototype methods (hasOwnProperty, etc...)
+ );
+
+ return props.join('\n');
+}
+
+/**
+ * List the symbols of an object.
+ * @param obj The object to get the symbols of.
+ * @returns An array of the symbols of the object.
+ */
+export function getSymbols(obj: Record<string, any>): symbol[] {
+ let symbols: symbol[] = [];
+ let obj_: Record<string, any> = new Object(obj);
+
+ do {
+ const l = Object.getOwnPropertySymbols(obj_).sort();
+
+ symbols = [...symbols, ...l];
+ } while (
+ (obj_ = Object.getPrototypeOf(obj_)) && // walk-up the prototype chain
+ Object.getPrototypeOf(obj_) // not the the Object prototype methods (hasOwnProperty, etc...)
+ );
+
+ return symbols;
+}
+
+/**
+ * Uploads an image to imgur.
+ * @param image The image to upload.
+ * @returns The url of the imgur.
+ */
+export async function uploadImageToImgur(image: string) {
+ const clientId = 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()) as { data: { link: string } };
+
+ return resp.data.link;
+}
+
+/**
+ * Checks if a user has a certain guild permission (doesn't check channel permissions).
+ * @param message The message to check the user from.
+ * @param permissions The permissions to check for.
+ * @returns The missing permissions or null if none are missing.
+ */
+export function userGuildPermCheck(
+ message: CommandMessage | SlashMessage,
+ permissions: typeof PermissionFlagsBits[keyof typeof PermissionFlagsBits][]
+): PermissionsString[] | null {
+ if (!message.inGuild()) return null;
+ const missing = message.member?.permissions.missing(permissions) ?? [];
+
+ return missing.length ? missing : null;
+}
+
+/**
+ * Check if the client has certain permissions in the guild (doesn't check channel permissions).
+ * @param message The message to check the client user from.
+ * @param permissions The permissions to check for.
+ * @returns The missing permissions or null if none are missing.
+ */
+export function clientGuildPermCheck(message: CommandMessage | SlashMessage, permissions: bigint[]): PermissionsString[] | null {
+ const missing = message.guild?.members.me?.permissions.missing(permissions) ?? [];
+
+ return missing.length ? missing : null;
+}
+
+/**
+ * Check if the client has permission to send messages in the channel as well as check if they have other permissions
+ * in the guild (or the channel if `checkChannel` is `true`).
+ * @param message The message to check the client user from.
+ * @param permissions The permissions to check for.
+ * @param checkChannel Whether to check the channel permissions instead of the guild permissions.
+ * @returns The missing permissions or null if none are missing.
+ */
+export function clientSendAndPermCheck(
+ message: CommandMessage | SlashMessage,
+ permissions: bigint[] = [],
+ checkChannel = false
+): PermissionsString[] | null {
+ const missing: PermissionsString[] = [];
+ const sendPerm = message.channel!.isThread() ? 'SendMessages' : 'SendMessagesInThreads';
+ if (!message.inGuild()) return null;
+
+ if (!message.guild.members.me!.permissionsIn(message.channel!.id).has(sendPerm)) missing.push(sendPerm);
+
+ missing.push(
+ ...(checkChannel
+ ? message.guild!.members.me!.permissionsIn(message.channel!.id!).missing(permissions)
+ : clientGuildPermCheck(message, permissions) ?? [])
+ );
+
+ return missing.length ? missing : null;
+}
+
+/**
+ * Gets the prefix based off of the message.
+ * @param message The message to get the prefix from.
+ * @returns The prefix.
+ */
+export function prefix(message: CommandMessage | SlashMessage): string {
+ return message.util.isSlash ? '/' : client.config.isDevelopment ? 'dev ' : message.util.parsed?.prefix ?? client.config.prefix;
+}
+
+export { deepLock as deepFreeze };
+export { Arg as arg };
+export { Format as format };
+export { DiscordConstants as discordConstants };
+export { AkairoUtil as akairo };
+
+/**
+ * The link to invite the bot with all permissions.
+ */
+export function invite(client: BushClient) {
+ return client.generateInvite({
+ permissions:
+ PermissionsBitField.All -
+ PermissionFlagsBits.UseEmbeddedActivities -
+ PermissionFlagsBits.ViewGuildInsights -
+ PermissionFlagsBits.Stream,
+ scopes: [OAuth2Scopes.Bot, OAuth2Scopes.ApplicationsCommands]
+ });
+}
+
+/**
+ * Asset multiple statements at a time.
+ * @param args
+ */
+export function assertAll(...args: any[]): void {
+ for (let i = 0; i < args.length; i++) {
+ assert(args[i], `assertAll index ${i} failed`);
+ }
+}
+
+/**
+ * Casts a string to a duration and reason for slash commands.
+ * @param arg The argument received.
+ * @param message The message that triggered the command.
+ * @returns The casted argument.
+ */
+export async function castDurationContent(
+ arg: string | ParsedDuration | null,
+ message: CommandMessage | SlashMessage
+): Promise<ParsedDurationRes> {
+ const res = typeof arg === 'string' ? await Arg.cast('contentWithDuration', message, arg) : arg;
+
+ return { duration: res?.duration ?? 0, content: res?.content ?? '' };
+}
+
+/**
+ * Casts a string to a the specified argument type.
+ * @param type The type of the argument to cast to.
+ * @param arg The argument received.
+ * @param message The message that triggered the command.
+ * @returns The casted argument.
+ */
+export async function cast<T extends keyof BaseBushArgumentType>(
+ type: T,
+ arg: BaseBushArgumentType[T] | string,
+ message: CommandMessage | SlashMessage
+) {
+ return typeof arg === 'string' ? await Arg.cast(type, message, arg) : arg;
+}
+
+/**
+ * Overflows the description of an embed into multiple embeds.
+ * @param embed The options to be applied to the (first) embed.
+ * @param lines Each line of the description as an element in an array.
+ */
+export function overflowEmbed(embed: Omit<APIEmbed, 'description'>, lines: string[], maxLength = 4096): EmbedBuilder[] {
+ const embeds: EmbedBuilder[] = [];
+
+ const makeEmbed = () => {
+ embeds.push(new EmbedBuilder().setColor(embed.color ?? null));
+ return embeds.at(-1)!;
+ };
+
+ for (const line of lines) {
+ let current = embeds.length ? embeds.at(-1)! : makeEmbed();
+ let joined = current.data.description ? `${current.data.description}\n${line}` : line;
+ if (joined.length > maxLength) {
+ current = makeEmbed();
+ joined = line;
+ }
+
+ current.setDescription(joined);
+ }
+
+ if (!embeds.length) makeEmbed();
+
+ if (embed.author) embeds.at(0)?.setAuthor(embed.author);
+ if (embed.title) embeds.at(0)?.setTitle(embed.title);
+ if (embed.url) embeds.at(0)?.setURL(embed.url);
+ if (embed.fields) embeds.at(-1)?.setFields(embed.fields);
+ if (embed.thumbnail) embeds.at(-1)?.setThumbnail(embed.thumbnail.url);
+ if (embed.footer) embeds.at(-1)?.setFooter(embed.footer);
+ if (embed.image) embeds.at(-1)?.setImage(embed.image.url);
+ if (embed.timestamp) embeds.at(-1)?.setTimestamp(new Date(embed.timestamp));
+
+ return embeds;
+}
+
+export async function resolveMessageLinks(content: string | null): Promise<MessageLinkParts[]> {
+ const res: MessageLinkParts[] = [];
+
+ if (!content) return res;
+
+ const regex_ = new RegExp(regex.messageLink);
+ let match: RegExpExecArray | null;
+ while (((match = regex_.exec(content)), match !== null)) {
+ const input = match.input;
+ if (!match.groups || !input) continue;
+ if (input.startsWith('<') && input.endsWith('>')) continue;
+
+ const { guild_id, channel_id, message_id } = match.groups;
+ if (!guild_id || !channel_id || !message_id) continue;
+
+ res.push({ guild_id, channel_id, message_id });
+ }
+
+ return res;
+}
+
+export async function resolveMessagesFromLinks(content: string): Promise<APIMessage[]> {
+ const res: APIMessage[] = [];
+
+ const links = await resolveMessageLinks(content);
+ if (!links.length) return [];
+
+ for (const { guild_id, channel_id, message_id } of links) {
+ const guild = client.guilds.cache.get(guild_id);
+ if (!guild) continue;
+ const channel = guild.channels.cache.get(channel_id);
+ if (!channel || (!channel.isTextBased() && !channel.isThread())) continue;
+
+ const message = (await client.rest.get(Routes.channelMessage(channel_id, message_id)).catch(() => null)) as APIMessage | null;
+ if (!message) continue;
+
+ res.push(message);
+ }
+
+ return res;
+}
+
+/**
+ * Formats an error into a string.
+ * @param error The error to format.
+ * @param colors Whether to use colors in the output.
+ * @returns The formatted error.
+ */
+export function formatError(error: Error | any, colors = false): string {
+ if (!error) return error;
+ if (typeof error !== 'object') return String.prototype.toString.call(error);
+ if (
+ getSymbols(error)
+ .map((s) => s.toString())
+ .includes('Symbol(nodejs.util.inspect.custom)')
+ )
+ return inspect(error, { colors });
+
+ return error.stack;
+}
+
+interface HastebinRes {
+ key: string;
+}
+
+export interface UuidRes {
+ uuid: string;
+ username: string;
+ username_history?: { username: string }[] | null;
+ textures: {
+ custom: boolean;
+ slim: boolean;
+ skin: {
+ url: string;
+ data: string;
+ };
+ raw: {
+ value: string;
+ signature: string;
+ };
+ };
+ created_at: string;
+}
+
+export interface HasteResults {
+ url?: string;
+ error?: 'content too long' | 'substr' | 'unable to post';
+}
+
+export interface ParsedDuration {
+ duration: number | null;
+ content: string | null;
+}
+
+export interface ParsedDurationRes {
+ duration: number;
+ content: string;
+}
+
+export type TimestampStyle = 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R';
+
+export interface MessageLinkParts {
+ guild_id: Snowflake;
+ channel_id: Snowflake;
+ message_id: Snowflake;
+}
diff --git a/src/lib/utils/Config.ts b/src/lib/utils/Config.ts
deleted file mode 100644
index ce5ec06..0000000
--- a/src/lib/utils/Config.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-import { type Snowflake } from 'discord.js';
-
-export class Config {
- public credentials: Credentials;
- public environment: Environment;
- public owners: Snowflake[];
- public prefix: string;
- public channels: Channels;
- public db: DataBase;
- public logging: Logging;
- public supportGuild: SupportGuild;
-
- public constructor(options: ConfigOptions) {
- this.credentials = options.credentials;
- this.environment = options.environment;
- this.owners = options.owners;
- this.prefix = options.prefix;
- this.channels = options.channels;
- this.db = options.db;
- this.logging = options.logging;
- this.supportGuild = options.supportGuild;
- }
-
- public get token(): string {
- return this.environment === 'production'
- ? this.credentials.token
- : this.environment === 'beta'
- ? this.credentials.betaToken
- : this.credentials.devToken;
- }
-
- public get isProduction(): boolean {
- return this.environment === 'production';
- }
-
- public get isBeta(): boolean {
- return this.environment === 'beta';
- }
-
- public get isDevelopment(): boolean {
- return this.environment === 'development';
- }
-}
-
-export interface ConfigOptions {
- credentials: Credentials;
- environment: Environment;
- owners: Snowflake[];
- prefix: string;
- channels: Channels;
- db: DataBase;
- logging: Logging;
- supportGuild: SupportGuild;
-}
-
-interface Credentials {
- token: string;
- betaToken: string;
- devToken: string;
- hypixelApiKey: string;
- wolframAlphaAppId: string;
- imgurClientId: string;
- imgurClientSecret: string;
- sentryDsn: string;
- perspectiveApiKey: string;
-}
-
-type Environment = 'production' | 'beta' | 'development';
-
-interface Channels {
- log: Snowflake;
- error: Snowflake;
- dm: Snowflake;
- servers: Snowflake;
-}
-
-interface DataBase {
- host: string;
- port: number;
- username: string;
- password: string;
-}
-
-interface Logging {
- db: boolean;
- verbose: boolean;
- info: boolean;
-}
-
-interface SupportGuild {
- id: Snowflake;
- invite: string;
-}