From e1c613829950a534d9f45c00a033b83575be3b3c Mon Sep 17 00:00:00 2001 From: IRONM00N <64110067+IRONM00N@users.noreply.github.com> Date: Fri, 17 Jun 2022 20:03:05 -0400 Subject: remove global client variable --- src/lib/common/AutoMod.ts | 30 +- src/lib/common/ButtonPaginator.ts | 2 +- src/lib/common/ConfirmationPrompt.ts | 2 +- src/lib/common/DeleteButton.ts | 2 +- src/lib/common/HighlightManager.ts | 8 +- src/lib/common/util/Arg.ts | 2 +- src/lib/common/util/Moderation.ts | 53 ++- src/lib/extensions/discord-akairo/BushClient.ts | 89 ++-- src/lib/extensions/discord-akairo/BushCommand.ts | 8 +- .../discord-akairo/BushCommandHandler.ts | 7 +- src/lib/extensions/discord-akairo/BushInhibitor.ts | 8 +- src/lib/extensions/discord.js/ExtendedGuild.ts | 48 ++- .../extensions/discord.js/ExtendedGuildMember.ts | 77 ++-- src/lib/extensions/discord.js/ExtendedMessage.ts | 2 +- src/lib/extensions/discord.js/ExtendedUser.ts | 4 +- src/lib/extensions/global.ts | 8 +- src/lib/models/instance/Guild.ts | 4 +- src/lib/utils/BushClientUtils.ts | 480 +++++++++++++++++++++ src/lib/utils/BushLogger.ts | 77 ++-- src/lib/utils/BushUtils.ts | 461 +------------------- 20 files changed, 743 insertions(+), 629 deletions(-) create mode 100644 src/lib/utils/BushClientUtils.ts (limited to 'src/lib') diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts index 7f19e63..0910352 100644 --- a/src/lib/common/AutoMod.ts +++ b/src/lib/common/AutoMod.ts @@ -1,4 +1,4 @@ -import { banResponse, codeblock, colors, emojis, format, formatError, getShared, Moderation, resolveNonCachedUser } from '#lib'; +import { banResponse, colors, emojis, format, formatError, Moderation } from '#lib'; import assert from 'assert'; import chalk from 'chalk'; import { @@ -31,7 +31,7 @@ export class AutoMod { */ private message: Message ) { - if (message.author.id === client.user?.id) return; + if (message.author.id === message.client.user?.id) return; void this.handle(); } @@ -56,9 +56,9 @@ export class AutoMod { traditional: { if (this.isImmune) break traditional; - const badLinksArray = getShared('badLinks'); - const badLinksSecretArray = getShared('badLinksSecret'); - const badWordsRaw = getShared('badWords'); + const badLinksArray = this.message.client.utils.getShared('badLinks'); + const badLinksSecretArray = this.message.client.utils.getShared('badLinksSecret'); + const badWordsRaw = this.message.client.utils.getShared('badWords'); const customAutomodPhrases = (await this.message.guild.getSetting('autoModPhases')) ?? []; const uniqueLinks = [...new Set([...badLinksArray, ...badLinksSecretArray])]; @@ -167,7 +167,9 @@ 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 codeblock(this.message.content, 1024)}` }]) + .addFields([ + { name: 'Message Content', value: `${await this.message.client.utils.codeblock(this.message.content, 1024)}` } + ]) .setColor(color) .setTimestamp() ], @@ -186,12 +188,12 @@ export class AutoMod { private async checkPerspectiveApi() { return; - if (!client.config.isDevelopment) return; + if (!this.message.client.config.isDevelopment) return; if (!this.message.content) return; - client.perspective.comments.analyze( + this.message.client.perspective.comments.analyze( { - key: client.config.credentials.perspectiveApiKey, + key: this.message.client.config.credentials.perspectiveApiKey, resource: { comment: { text: this.message.content @@ -301,7 +303,7 @@ export class AutoMod { { title: 'AutoMod Error', description: `Unable to delete triggered message.`, - fields: [{ name: 'Error', value: await codeblock(`${formatError(e)}`, 1024, 'js', true) }], + fields: [{ name: 'Error', value: await this.message.client.utils.codeblock(`${formatError(e)}`, 1024, 'js', true) }], color: colors.error } ] @@ -316,7 +318,7 @@ export class AutoMod { * @param offences The other offences that were also matched in the message */ private async log(highestOffence: BadWordDetails, color: number, offences: BadWordDetails[]) { - void client.console.info( + void this.message.client.console.info( 'autoMod', `Severity <<${highestOffence.severity}>> action performed on <<${this.message.author.tag}>> (<<${ this.message.author.id @@ -332,7 +334,9 @@ 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 codeblock(this.message.content, 1024)}` }]) + .addFields([ + { name: 'Message Content', value: `${await this.message.client.utils.codeblock(this.message.content, 1024)}` } + ]) .setColor(color) .setTimestamp() .setAuthor({ name: this.message.author.tag, url: this.message.author.displayAvatarURL() }) @@ -386,7 +390,7 @@ export class AutoMod { evidence: (interaction.message as Message).url ?? undefined }); - const victimUserFormatted = (await resolveNonCachedUser(userId))?.tag ?? userId; + const victimUserFormatted = (await interaction.client.utils.resolveNonCachedUser(userId))?.tag ?? userId; if (result === banResponse.SUCCESS) return interaction.reply({ content: `${emojis.success} Successfully banned **${victimUserFormatted}**.`, diff --git a/src/lib/common/ButtonPaginator.ts b/src/lib/common/ButtonPaginator.ts index 9560247..708b374 100644 --- a/src/lib/common/ButtonPaginator.ts +++ b/src/lib/common/ButtonPaginator.ts @@ -97,7 +97,7 @@ export class ButtonPaginator { * @param interaction The interaction received */ protected async collect(interaction: MessageComponentInteraction) { - if (interaction.user.id !== this.message.author.id && !client.config.owners.includes(interaction.user.id)) + if (interaction.user.id !== this.message.author.id && !this.message.client.config.owners.includes(interaction.user.id)) return await interaction?.deferUpdate().catch(() => null); switch (interaction.customId) { diff --git a/src/lib/common/ConfirmationPrompt.ts b/src/lib/common/ConfirmationPrompt.ts index 4593d24..38d078a 100644 --- a/src/lib/common/ConfirmationPrompt.ts +++ b/src/lib/common/ConfirmationPrompt.ts @@ -43,7 +43,7 @@ export class ConfirmationPrompt { collector.on('collect', async (interaction: MessageComponentInteraction) => { await interaction.deferUpdate().catch(() => undefined); - if (interaction.user.id == this.message.author.id || client.config.owners.includes(interaction.user.id)) { + if (interaction.user.id == this.message.author.id || this.message.client.config.owners.includes(interaction.user.id)) { if (interaction.customId === 'confirmationPrompt_confirm') { responded = true; collector.stop(); diff --git a/src/lib/common/DeleteButton.ts b/src/lib/common/DeleteButton.ts index b561d94..bc0da17 100644 --- a/src/lib/common/DeleteButton.ts +++ b/src/lib/common/DeleteButton.ts @@ -45,7 +45,7 @@ export class DeleteButton { collector.on('collect', async (interaction: MessageComponentInteraction) => { await interaction.deferUpdate().catch(() => undefined); - if (interaction.user.id == this.message.author.id || client.config.owners.includes(interaction.user.id)) { + if (interaction.user.id == this.message.author.id || this.message.client.config.owners.includes(interaction.user.id)) { if (msg.deletable && !CommandUtil.deletedMessages.has(msg.id)) await msg.delete(); } }); diff --git a/src/lib/common/HighlightManager.ts b/src/lib/common/HighlightManager.ts index caaa6a5..cd89c89 100644 --- a/src/lib/common/HighlightManager.ts +++ b/src/lib/common/HighlightManager.ts @@ -232,10 +232,10 @@ export class HighlightManager { const lastDM = this.lastedDMedUserCooldown.get(user); if (!lastDM) break dmCooldown; - const cooldown = client.ownerID.includes(user) ? OWNER_NOTIFY_COOLDOWN : NOTIFY_COOLDOWN; + const cooldown = message.client.ownerID.includes(user) ? OWNER_NOTIFY_COOLDOWN : NOTIFY_COOLDOWN; if (new Date().getTime() - lastDM.getTime() < cooldown) { - void client.console.verbose('Highlight', `User <<${user}>> has been dmed recently.`); + void message.client.console.verbose('Highlight', `User <<${user}>> has been dmed recently.`); return false; } } @@ -248,7 +248,7 @@ export class HighlightManager { const talked = lastTalked.getTime(); if (now - talked < LAST_MESSAGE_COOLDOWN) { - void client.console.verbose('Highlight', `User <<${user}>> has talked too recently.`); + void message.client.console.verbose('Highlight', `User <<${user}>> has talked too recently.`); setTimeout(() => { const newTalked = this.userLastTalkedCooldown.get(message.guildId)?.get(user)?.getTime(); @@ -268,7 +268,7 @@ export class HighlightManager { .first(4) .reverse(); - return client.users + return message.client.users .send(user, { // eslint-disable-next-line @typescript-eslint/no-base-to-string content: `In ${format.input(message.guild.name)} ${message.channel}, your highlight "${hl.word}" was matched:`, diff --git a/src/lib/common/util/Arg.ts b/src/lib/common/util/Arg.ts index a7795b1..325f821 100644 --- a/src/lib/common/util/Arg.ts +++ b/src/lib/common/util/Arg.ts @@ -18,7 +18,7 @@ export async function cast(type: T, message: CommandMessage | Sla export async function cast(type: T, message: CommandMessage | SlashMessage, phrase: string): Promise; export async function cast(type: AT | ATC, message: CommandMessage | SlashMessage, phrase: string): Promise; export async function cast(type: ATC | AT, message: CommandMessage | SlashMessage, phrase: string): Promise { - return Argument.cast(type as any, client.commandHandler.resolver, message as Message, phrase); + return Argument.cast(type as any, message.client.commandHandler.resolver, message as Message, phrase); } /** diff --git a/src/lib/common/util/Moderation.ts b/src/lib/common/util/Moderation.ts index a08dfa4..cb6b4db 100644 --- a/src/lib/common/util/Moderation.ts +++ b/src/lib/common/util/Moderation.ts @@ -5,10 +5,8 @@ import { emojis, format, Guild as GuildDB, - handleError, humanizeDuration, ModLog, - resolveNonCachedUser, type ModLogType } from '#lib'; import assert from 'assert'; @@ -16,6 +14,7 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, + Client, EmbedBuilder, PermissionFlagsBits, type Guild, @@ -129,9 +128,9 @@ 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)!; + const user = (await options.client.utils.resolveNonCachedUser(options.user))!.id; + const moderator = (await options.client.utils.resolveNonCachedUser(options.moderator))!.id; + const guild = options.client.guilds.resolveId(options.guild)!; return createModLogEntrySimple( { @@ -172,7 +171,7 @@ export async function createModLogEntrySimple( hidden: options.hidden ?? false }); const saveResult: ModLog | null = await modLogEntry.save().catch(async (e) => { - await handleError('createModLogEntry', e); + await options.client.utils.handleError('createModLogEntry', e); return null; }); @@ -191,8 +190,8 @@ export async function createModLogEntrySimple( */ export async function createPunishmentEntry(options: CreatePunishmentEntryOptions): Promise { 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 user = (await options.client.utils.resolveNonCachedUser(options.user))!.id; + const guild = options.client.guilds.resolveId(options.guild)!; const type = findTypeEnum(options.type)!; const entry = ActivePunishment.build( @@ -201,7 +200,7 @@ export async function createPunishmentEntry(options: CreatePunishmentEntryOption : { user, type, guild, expires, modlog: options.modlog } ); return await entry.save().catch(async (e) => { - await handleError('createPunishmentEntry', e); + await options.client.utils.handleError('createPunishmentEntry', e); return null; }); } @@ -212,8 +211,8 @@ export async function createPunishmentEntry(options: CreatePunishmentEntryOption * @returns Whether or not the entry was destroyed. */ export async function removePunishmentEntry(options: RemovePunishmentEntryOptions): Promise { - const user = await resolveNonCachedUser(options.user); - const guild = client.guilds.resolveId(options.guild); + const user = await options.client.utils.resolveNonCachedUser(options.user); + const guild = options.client.guilds.resolveId(options.guild); const type = findTypeEnum(options.type); if (!user || !guild) return false; @@ -226,13 +225,13 @@ export async function removePunishmentEntry(options: RemovePunishmentEntryOption ? { user: user.id, guild: guild, type, extraInfo: options.extraInfo } : { user: user.id, guild: guild, type } }).catch(async (e) => { - await handleError('removePunishmentEntry', e); + await options.client.utils.handleError('removePunishmentEntry', e); success = false; }); if (entries) { const promises = entries.map(async (entry) => entry.destroy().catch(async (e) => { - await handleError('removePunishmentEntry', e); + await options.client.utils.handleError('removePunishmentEntry', e); success = false; }) ); @@ -298,9 +297,9 @@ export async function punishDM(options: PunishDMOptions): Promise { new ActionRowBuilder({ components: [ new ButtonBuilder({ - customId: `appeal;${punishmentToPresentTense(options.punishment)};${options.guild.id};${client.users.resolveId( - options.user - )};${options.modlog}`, + customId: `appeal;${punishmentToPresentTense(options.punishment)};${ + options.guild.id + };${options.client.users.resolveId(options.user)};${options.modlog}`, style: ButtonStyle.Primary, label: 'Appeal' }).toJSON() @@ -308,7 +307,7 @@ export async function punishDM(options: PunishDMOptions): Promise { }) ]; - const dmSuccess = await client.users + const dmSuccess = await options.client.users .send(options.user, { content, embeds: dmEmbed ? [dmEmbed] : undefined, @@ -318,7 +317,7 @@ export async function punishDM(options: PunishDMOptions): Promise { return !!dmSuccess; } -interface BaseCreateModLogEntryOptions { +interface BaseCreateModLogEntryOptions extends BaseOptions { /** * The type of modlog entry. */ @@ -354,6 +353,11 @@ interface BaseCreateModLogEntryOptions { * Options for creating a modlog entry. */ export interface CreateModLogEntryOptions extends BaseCreateModLogEntryOptions { + /** + * The client. + */ + client: Client; + /** * The user that a modlog entry is created for. */ @@ -393,7 +397,7 @@ export interface SimpleCreateModLogEntryOptions extends BaseCreateModLogEntryOpt /** * Options for creating a punishment entry. */ -export interface CreatePunishmentEntryOptions { +export interface CreatePunishmentEntryOptions extends BaseOptions { /** * The type of punishment. */ @@ -428,7 +432,7 @@ export interface CreatePunishmentEntryOptions { /** * Options for removing a punishment entry. */ -export interface RemovePunishmentEntryOptions { +export interface RemovePunishmentEntryOptions extends BaseOptions { /** * The type of punishment. */ @@ -453,7 +457,7 @@ export interface RemovePunishmentEntryOptions { /** * Options for sending a user a punishment dm. */ -export interface PunishDMOptions { +export interface PunishDMOptions extends BaseOptions { /** * The modlog case id so the user can make an appeal. */ @@ -496,6 +500,13 @@ export interface PunishDMOptions { channel?: Snowflake; } +interface BaseOptions { + /** + * The client. + */ + client: Client; +} + export type PunishmentTypeDM = | 'warned' | 'muted' diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts index b382121..68b2599 100644 --- a/src/lib/extensions/discord-akairo/BushClient.ts +++ b/src/lib/extensions/discord-akairo/BushClient.ts @@ -63,7 +63,8 @@ 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 BushLogger from '../../utils/BushLogger.js'; +import { BushClientUtils } from '../../utils/BushClientUtils.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'; @@ -76,14 +77,44 @@ const { Sequelize } = (await import('sequelize')).default; declare module 'discord.js' { export interface Client extends EventEmitter { - /** - * The ID of the owner(s). - */ + /** The ID of the owner(s). */ ownerID: Snowflake | Snowflake[]; - /** - * The ID of the superUser(s). - */ + /** The ID of the superUser(s). */ superUserID: Snowflake | Snowflake[]; + /** Whether or not the client is ready. */ + customReady: boolean; + /** The configuration for the client. */ + config: Config; + /** Stats for the client. */ + stats: BushStats; + /** The handler for the bot's listeners. */ + listenerHandler: BushListenerHandler; + /** The handler for the bot's command inhibitors. */ + inhibitorHandler: BushInhibitorHandler; + /** The handler for the bot's commands. */ + commandHandler: BushCommandHandler; + /** The handler for the bot's tasks. */ + taskHandler: BushTaskHandler; + /** The handler for the bot's context menu commands. */ + contextMenuCommandHandler: ContextMenuCommandHandler; + /** The database connection for this instance of the bot (production, beta, or development). */ + instanceDB: SequelizeType; + /** The database connection that is shared between all instances of the bot. */ + sharedDB: SequelizeType; + /** A custom logging system for the bot. */ + logger: BushLogger; + /** Cached global and guild database data. */ + cache: BushCache; + /** Sentry error reporting for the bot. */ + sentry: typeof Sentry; + /** Manages most aspects of the highlight command */ + highlightManager: HighlightManager; + /** The perspective api */ + perspective: any; + /** Client utilities. */ + utils: BushClientUtils; + /** A custom logging system for the bot. */ + get console(): BushLogger; on(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; once(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; emit(event: K, ...args: BushClientEvents[K]): boolean; @@ -126,72 +157,77 @@ export class BushClient extends AkairoClient extends AkairoClient extends AkairoClient; - this.config = config; + this.logger = new BushLogger(this); + this.utils = new BushClientUtils(this); /* =-=-= handlers =-=-= */ this.listenerHandler = new BushListenerHandler(this, { @@ -320,7 +357,7 @@ export class BushClient extends AkairoClient extends AkairoClient>.`, false); - const stats = await UpdateStatsTask.init(); + const stats = await UpdateStatsTask.init(this); this.stats.commandsUsed = stats.commandsUsed; this.stats.slashCommandsUsed = stats.slashCommandsUsed; await this.login(this.token!); @@ -500,7 +537,7 @@ export class BushClient extends AkairoClient; public declare categories: Collection>; - - public constructor(client: BushClient, options: CommandHandlerOptions) { - super(client, options); - } } export interface BushCommandHandler extends CommandHandler { diff --git a/src/lib/extensions/discord-akairo/BushInhibitor.ts b/src/lib/extensions/discord-akairo/BushInhibitor.ts index b4e6797..be396cf 100644 --- a/src/lib/extensions/discord-akairo/BushInhibitor.ts +++ b/src/lib/extensions/discord-akairo/BushInhibitor.ts @@ -1,15 +1,15 @@ -import { type BushClient, type BushCommand, type CommandMessage, type SlashMessage } from '#lib'; +import { type BushCommand, type CommandMessage, type SlashMessage } from '#lib'; import { Inhibitor } from 'discord-akairo'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Message } from 'discord.js'; export abstract class BushInhibitor extends Inhibitor { - public declare client: BushClient; - /** * Checks if message should be blocked. * A return value of true will block the message. * If returning a Promise, a resolved value of true will block the message. * - * **Note:** `all` type inhibitors do not have `message.util` defined. + * **Note:** `'all'` type inhibitors do not have {@link Message.util} defined. * * @param message - Message being handled. * @param command - Command to check. diff --git a/src/lib/extensions/discord.js/ExtendedGuild.ts b/src/lib/extensions/discord.js/ExtendedGuild.ts index c199899..c58916c 100644 --- a/src/lib/extensions/discord.js/ExtendedGuild.ts +++ b/src/lib/extensions/discord.js/ExtendedGuild.ts @@ -41,7 +41,7 @@ import _ from 'lodash'; 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'; +import { addOrRemoveFromArray } from '../../utils/BushUtils.js'; declare module 'discord.js' { export interface Guild { @@ -187,7 +187,7 @@ export class ExtendedGuild extends Guild { */ public override async getSetting(setting: K): Promise { return ( - client.cache.guilds.get(this.id)?.[setting] ?? + this.client.cache.guilds.get(this.id)?.[setting] ?? ((await GuildDB.findByPk(this.id)) ?? GuildDB.build({ id: this.id }))[setting] ); } @@ -206,8 +206,8 @@ export class ExtendedGuild extends Guild { const row = (await GuildDB.findByPk(this.id)) ?? GuildDB.build({ id: this.id }); const oldValue = row[setting] as GuildDB[K]; row[setting] = value; - client.cache.guilds.set(this.id, row.toJSON() as GuildDB); - client.emit('bushUpdateSettings', setting, this, oldValue, row[setting], moderator); + this.client.cache.guilds.set(this.id, row.toJSON() as GuildDB); + this.client.emit('bushUpdateSettings', setting, this, oldValue, row[setting], moderator); return await row.save(); } @@ -253,7 +253,7 @@ export class ExtendedGuild extends Guild { * @param message The description of the error embed */ public override async error(title: string, message: string): Promise { - void client.console.info(_.camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>')); + void this.client.console.info(_.camelCase(title), message.replace(/\*\*(.*?)\*\*/g, '<<$1>>')); void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: colors.error }] }); } @@ -268,8 +268,8 @@ export class ExtendedGuild extends Guild { let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; - const user = await resolveNonCachedUser(options.user); - const moderator = client.users.resolve(options.moderator ?? client.user!); + const user = await this.client.utils.resolveNonCachedUser(options.user); + const moderator = this.client.users.resolve(options.moderator ?? this.client.user!); if (!user || !moderator) return banResponse.CANNOT_RESOLVE_USER; if ((await this.bans.fetch()).has(user.id)) return banResponse.ALREADY_BANNED; @@ -277,6 +277,7 @@ export class ExtendedGuild extends Guild { const ret = await (async () => { // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, user: user, moderator: moderator.id, @@ -290,6 +291,7 @@ export class ExtendedGuild extends Guild { // dm user dmSuccessEvent = await Moderation.punishDM({ + client: this.client, modlog: modlog.id, guild: this, user: user, @@ -310,6 +312,7 @@ export class ExtendedGuild extends Guild { // add punishment entry so they can be unbanned later const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, type: 'ban', user: user, guild: this, @@ -323,7 +326,7 @@ export class ExtendedGuild extends Guild { })(); if (!([banResponse.ACTION_ERROR, banResponse.MODLOG_ERROR, banResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret)) - client.emit( + this.client.emit( 'bushBan', user, moderator, @@ -352,6 +355,7 @@ export class ExtendedGuild extends Guild { const ret = await (async () => { // add modlog entry const { log: modlog } = await Moderation.createModLogEntrySimple({ + client: this.client, type: ModLogType.PERM_BAN, user: options.user, moderator: options.moderator, @@ -365,6 +369,7 @@ export class ExtendedGuild extends Guild { // dm user if (this.members.cache.has(options.user)) { dmSuccessEvent = await Moderation.punishDM({ + client: this.client, modlog: modlog.id, guild: this, user: options.user, @@ -386,6 +391,7 @@ export class ExtendedGuild extends Guild { // add punishment entry so they can be unbanned later const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, type: 'ban', user: options.user, guild: this, @@ -411,8 +417,8 @@ export class ExtendedGuild extends Guild { let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; - const user = await resolveNonCachedUser(options.user); - const moderator = client.users.resolve(options.moderator ?? client.user!); + const user = await this.client.utils.resolveNonCachedUser(options.user); + const moderator = this.client.users.resolve(options.moderator ?? this.client.user!); if (!user || !moderator) return unbanResponse.CANNOT_RESOLVE_USER; const ret = await (async () => { @@ -435,6 +441,7 @@ export class ExtendedGuild extends Guild { // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, type: ModLogType.UNBAN, user: user.id, moderator: moderator.id, @@ -447,6 +454,7 @@ export class ExtendedGuild extends Guild { // remove punishment entry const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({ + client: this.client, type: 'ban', user: user.id, guild: this @@ -455,6 +463,7 @@ export class ExtendedGuild extends Guild { // dm user dmSuccessEvent = await Moderation.punishDM({ + client: this.client, guild: this, user: user, punishment: 'unbanned', @@ -470,7 +479,16 @@ export class ExtendedGuild extends Guild { ret ) ) - client.emit('bushUnban', user, moderator, this, options.reason ?? undefined, caseID!, dmSuccessEvent!, options.evidence); + this.client.emit( + 'bushUnban', + user, + moderator, + this, + options.reason ?? undefined, + caseID!, + dmSuccessEvent!, + options.evidence + ); return ret; } @@ -549,7 +567,7 @@ export class ExtendedGuild extends Guild { else return `success: ${success.filter((c) => c === true).size}`; })(); - client.emit(options.unlock ? 'bushUnlockdown' : 'bushLockdown', moderator, options.reason, success, options.all); + this.client.emit(options.unlock ? 'bushUnlockdown' : 'bushLockdown', moderator, options.reason, success, options.all); return ret; } @@ -557,7 +575,7 @@ export class ExtendedGuild extends Guild { if (!channel.isTextBased() || channel.isDMBased() || channel.guildId !== this.id || !this.members.me) return null; if (!channel.permissionsFor(this.members.me).has('ManageWebhooks')) return null; - const quote = new Message(client, rawQuote); + const quote = new Message(this.client, rawQuote); const target = channel instanceof ThreadChannel ? channel.parent : channel; if (!target) return null; @@ -570,8 +588,8 @@ export class ExtendedGuild extends Guild { if (!webhook) webhook = await target .createWebhook({ - name: `${client.user!.username} Quotes #${target.name}`, - avatar: client.user!.displayAvatarURL({ size: 2048 }), + name: `${this.client.user!.username} Quotes #${target.name}`, + avatar: this.client.user!.displayAvatarURL({ size: 2048 }), reason: 'Creating a webhook for quoting' }) .catch(() => null); diff --git a/src/lib/extensions/discord.js/ExtendedGuildMember.ts b/src/lib/extensions/discord.js/ExtendedGuildMember.ts index ad29236..947f9cd 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, formatError, Moderation, ModLogType, PunishmentTypeDM, resolveNonCachedUser, Time } from '#lib'; +import { BushClientEvents, formatError, Moderation, ModLogType, PunishmentTypeDM, Time } from '#lib'; import { ChannelType, GuildChannelResolvable, @@ -129,6 +129,7 @@ export class ExtendedGuildMember extends GuildMember { sendFooter = true ): Promise { return Moderation.punishDM({ + client: this.client, modlog, guild: this.guild, user: this, @@ -148,13 +149,14 @@ 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 resolveNonCachedUser(options.moderator ?? this.guild.members.me); + const moderator = await this.client.utils.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 }> => { // add modlog entry const result = await Moderation.createModLogEntry( { + client: this.client, type: ModLogType.WARN, user: this, moderator: moderator.id, @@ -178,7 +180,7 @@ export class ExtendedGuildMember extends GuildMember { return { result: warnResponse.SUCCESS, caseNum: result.caseNum }; })(); if (!([warnResponse.MODLOG_ERROR] as const).includes(ret.result) && !options.silent) - client.emit('bushWarn', this, moderator, this.guild, options.reason ?? undefined, caseID!, dmSuccessEvent!); + this.client.emit('bushWarn', this, moderator, this.guild, options.reason ?? undefined, caseID!, dmSuccessEvent!); return ret; } @@ -195,12 +197,13 @@ export class ExtendedGuildMember extends GuildMember { if (ifShouldAddRole !== true) return ifShouldAddRole; let caseID: string | undefined = undefined; - const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me); + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); if (!moderator) return addRoleResponse.CANNOT_RESOLVE_USER; const ret = await (async () => { if (options.addToModlog || options.duration) { const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, type: options.duration ? ModLogType.TEMP_PUNISHMENT_ROLE : ModLogType.PERM_PUNISHMENT_ROLE, guild: this.guild, moderator: moderator.id, @@ -216,6 +219,7 @@ export class ExtendedGuildMember extends GuildMember { if (options.addToModlog || options.duration) { const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, type: 'role', user: this, guild: this.guild, @@ -239,7 +243,7 @@ export class ExtendedGuildMember extends GuildMember { options.addToModlog && !options.silent ) - client.emit( + this.client.emit( 'bushPunishRole', this, moderator, @@ -266,12 +270,13 @@ export class ExtendedGuildMember extends GuildMember { if (ifShouldAddRole !== true) return ifShouldAddRole; let caseID: string | undefined = undefined; - const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me); + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); if (!moderator) return removeRoleResponse.CANNOT_RESOLVE_USER; const ret = await (async () => { if (options.addToModlog) { const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, type: ModLogType.REMOVE_PUNISHMENT_ROLE, guild: this.guild, moderator: moderator.id, @@ -285,6 +290,7 @@ export class ExtendedGuildMember extends GuildMember { caseID = modlog.id; const punishmentEntrySuccess = await Moderation.removePunishmentEntry({ + client: this.client, type: 'role', user: this, guild: this.guild, @@ -311,7 +317,7 @@ export class ExtendedGuildMember extends GuildMember { options.addToModlog && !options.silent ) - client.emit( + this.client.emit( 'bushPunishRoleRemove', this, moderator, @@ -362,7 +368,7 @@ export class ExtendedGuildMember extends GuildMember { let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me); + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); if (!moderator) return muteResponse.CANNOT_RESOLVE_USER; const ret = await (async () => { @@ -370,14 +376,15 @@ export class ExtendedGuildMember extends GuildMember { const muteSuccess = await this.roles .add(muteRole, `[Mute] ${moderator.tag} | ${options.reason ?? 'No reason provided.'}`) .catch(async (e) => { - await client.console.warn('muteRoleAddError', e); - client.console.debug(e); + await this.client.console.warn('muteRoleAddError', e); + this.client.console.debug(e); return false; }); if (!muteSuccess) return muteResponse.ACTION_ERROR; // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, type: options.duration ? ModLogType.TEMP_MUTE : ModLogType.PERM_MUTE, user: this, moderator: moderator.id, @@ -393,6 +400,7 @@ export class ExtendedGuildMember extends GuildMember { // add punishment entry so they can be unmuted later const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, type: 'mute', user: this, guild: this.guild, @@ -416,7 +424,7 @@ export class ExtendedGuildMember extends GuildMember { !([muteResponse.ACTION_ERROR, muteResponse.MODLOG_ERROR, muteResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret) && !options.silent ) - client.emit( + this.client.emit( 'bushMute', this, moderator, @@ -448,7 +456,7 @@ export class ExtendedGuildMember extends GuildMember { let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me); + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); if (!moderator) return unmuteResponse.CANNOT_RESOLVE_USER; const ret = await (async () => { @@ -456,13 +464,14 @@ 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', formatError(e, true)); + await this.client.console.warn('muteRoleAddError', formatError(e, true)); return false; }); if (!muteSuccess) return unmuteResponse.ACTION_ERROR; // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, type: ModLogType.UNMUTE, user: this, moderator: moderator.id, @@ -477,6 +486,7 @@ export class ExtendedGuildMember extends GuildMember { // remove mute entry const removePunishmentEntrySuccess = await Moderation.removePunishmentEntry({ + client: this.client, type: 'mute', user: this, guild: this.guild @@ -500,7 +510,7 @@ export class ExtendedGuildMember extends GuildMember { ).includes(ret) && !options.silent ) - client.emit( + this.client.emit( 'bushUnmute', this, moderator, @@ -526,11 +536,12 @@ export class ExtendedGuildMember extends GuildMember { let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me); + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); if (!moderator) return kickResponse.CANNOT_RESOLVE_USER; const ret = await (async () => { // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, type: ModLogType.KICK, user: this, moderator: moderator.id, @@ -554,7 +565,7 @@ export class ExtendedGuildMember extends GuildMember { return kickResponse.SUCCESS; })(); if (!([kickResponse.ACTION_ERROR, kickResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent) - client.emit( + this.client.emit( 'bushKick', this, moderator, @@ -580,7 +591,7 @@ export class ExtendedGuildMember extends GuildMember { let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me); + const moderator = await this.client.utils.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 @@ -593,6 +604,7 @@ export class ExtendedGuildMember extends GuildMember { const ret = await (async () => { // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, type: options.duration ? ModLogType.TEMP_BAN : ModLogType.PERM_BAN, user: this, moderator: moderator.id, @@ -620,6 +632,7 @@ export class ExtendedGuildMember extends GuildMember { // add punishment entry so they can be unbanned later const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, type: 'ban', user: this, guild: this.guild, @@ -635,7 +648,7 @@ export class ExtendedGuildMember extends GuildMember { !([banResponse.ACTION_ERROR, banResponse.MODLOG_ERROR, banResponse.PUNISHMENT_ENTRY_ADD_ERROR] as const).includes(ret) && !options.silent ) - client.emit( + this.client.emit( 'bushBan', this, moderator, @@ -663,7 +676,7 @@ export class ExtendedGuildMember extends GuildMember { let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me); + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); if (!moderator) return blockResponse.CANNOT_RESOLVE_USER; const ret = await (async () => { @@ -677,6 +690,7 @@ export class ExtendedGuildMember extends GuildMember { // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, type: options.duration ? ModLogType.TEMP_CHANNEL_BLOCK : ModLogType.PERM_CHANNEL_BLOCK, user: this, moderator: moderator.id, @@ -690,6 +704,7 @@ export class ExtendedGuildMember extends GuildMember { // add punishment entry so they can be unblocked later const punishmentEntrySuccess = await Moderation.createPunishmentEntry({ + client: this.client, type: 'block', user: this, guild: this.guild, @@ -703,6 +718,7 @@ export class ExtendedGuildMember extends GuildMember { const dmSuccess = options.silent ? null : await Moderation.punishDM({ + client: this.client, punishment: 'blocked', reason: options.reason ?? undefined, duration: options.duration ?? 0, @@ -724,7 +740,7 @@ export class ExtendedGuildMember extends GuildMember { ) && !options.silent ) - client.emit( + this.client.emit( 'bushBlock', this, moderator, @@ -754,7 +770,7 @@ export class ExtendedGuildMember extends GuildMember { let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me); + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); if (!moderator) return unblockResponse.CANNOT_RESOLVE_USER; const ret = await (async () => { @@ -768,6 +784,7 @@ export class ExtendedGuildMember extends GuildMember { // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, type: ModLogType.CHANNEL_UNBLOCK, user: this, moderator: moderator.id, @@ -781,6 +798,7 @@ export class ExtendedGuildMember extends GuildMember { // remove punishment entry const punishmentEntrySuccess = await Moderation.removePunishmentEntry({ + client: this.client, type: 'block', user: this, guild: this.guild, @@ -792,6 +810,7 @@ export class ExtendedGuildMember extends GuildMember { const dmSuccess = options.silent ? null : await Moderation.punishDM({ + client: this.client, punishment: 'unblocked', reason: options.reason ?? undefined, guild: this.guild, @@ -812,7 +831,7 @@ export class ExtendedGuildMember extends GuildMember { !([unblockResponse.ACTION_ERROR, unblockResponse.MODLOG_ERROR, unblockResponse.ACTION_ERROR] as const).includes(ret) && !options.silent ) - client.emit( + this.client.emit( 'bushUnblock', this, moderator, @@ -839,7 +858,7 @@ export class ExtendedGuildMember extends GuildMember { let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me); + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); if (!moderator) return timeoutResponse.CANNOT_RESOLVE_USER; const ret = await (async () => { @@ -852,6 +871,7 @@ export class ExtendedGuildMember extends GuildMember { // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, type: ModLogType.TIMEOUT, user: this, moderator: moderator.id, @@ -876,7 +896,7 @@ export class ExtendedGuildMember extends GuildMember { })(); if (!([timeoutResponse.ACTION_ERROR, timeoutResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent) - client.emit( + this.client.emit( 'bushTimeout', this, moderator, @@ -901,7 +921,7 @@ export class ExtendedGuildMember extends GuildMember { let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; - const moderator = await resolveNonCachedUser(options.moderator ?? this.guild.members.me); + const moderator = await this.client.utils.resolveNonCachedUser(options.moderator ?? this.guild.members.me); if (!moderator) return removeTimeoutResponse.CANNOT_RESOLVE_USER; const ret = await (async () => { @@ -913,6 +933,7 @@ export class ExtendedGuildMember extends GuildMember { // add modlog entry const { log: modlog } = await Moderation.createModLogEntry({ + client: this.client, type: ModLogType.REMOVE_TIMEOUT, user: this, moderator: moderator.id, @@ -936,7 +957,7 @@ export class ExtendedGuildMember extends GuildMember { })(); if (!([removeTimeoutResponse.ACTION_ERROR, removeTimeoutResponse.MODLOG_ERROR] as const).includes(ret) && !options.silent) - client.emit( + this.client.emit( 'bushRemoveTimeout', this, moderator, @@ -953,14 +974,14 @@ export class ExtendedGuildMember extends GuildMember { * Whether or not the user is an owner of the bot. */ public override isOwner(): boolean { - return client.isOwner(this); + return this.client.isOwner(this); } /** * Whether or not the user is a super user of the bot. */ public override isSuperUser(): boolean { - return client.isSuperUser(this); + return this.client.isSuperUser(this); } } diff --git a/src/lib/extensions/discord.js/ExtendedMessage.ts b/src/lib/extensions/discord.js/ExtendedMessage.ts index 4431077..0d8ce37 100644 --- a/src/lib/extensions/discord.js/ExtendedMessage.ts +++ b/src/lib/extensions/discord.js/ExtendedMessage.ts @@ -7,6 +7,6 @@ export class ExtendedMessage extends Message { includes}`>( @@ -15,3 +9,5 @@ declare global { ): searchElement is R & S; } } + +export {}; diff --git a/src/lib/models/instance/Guild.ts b/src/lib/models/instance/Guild.ts index 49bf822..7a19b72 100644 --- a/src/lib/models/instance/Guild.ts +++ b/src/lib/models/instance/Guild.ts @@ -199,12 +199,14 @@ const asGuildSetting = (et: { [K in keyof T]: PartialBy client.config.prefix + replaceNullWith: () => config.prefix }, autoPublishChannels: { name: 'Auto Publish Channels', diff --git a/src/lib/utils/BushClientUtils.ts b/src/lib/utils/BushClientUtils.ts new file mode 100644 index 0000000..44a08ef --- /dev/null +++ b/src/lib/utils/BushClientUtils.ts @@ -0,0 +1,480 @@ +import assert from 'assert'; +import { + cleanCodeBlockContent, + escapeCodeBlock, + GuildMember, + Message, + Routes, + TextChannel, + ThreadMember, + User, + type APIMessage, + type Client, + type Snowflake, + type UserResolvable +} from 'discord.js'; +import got from 'got'; +import _ from 'lodash'; +import CommandErrorListener from '../../listeners/commands/commandError.js'; +import { BushInspectOptions } from '../common/typings/BushInspectOptions.js'; +import { CodeBlockLang } from '../common/typings/CodeBlockLang.js'; +import { CommandMessage } from '../extensions/discord-akairo/BushCommand.js'; +import { SlashMessage } from '../extensions/discord-akairo/SlashMessage.js'; +import { Global } from '../models/shared/Global.js'; +import { Shared } from '../models/shared/Shared.js'; +import { GlobalCache, SharedCache } from './BushCache.js'; +import { emojis, Pronoun, PronounCode, pronounMapping, regex } from './BushConstants.js'; +import { addOrRemoveFromArray, formatError, inspect } from './BushUtils.js'; + +/** + * Utilities that require access to the client. + */ +export class BushClientUtils { + /** + * 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' + ]; + + public constructor(private readonly client: 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 { + return await Promise.all(ids.map((id) => this.client.users.fetch(id))); + } + + /** + * 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 { + 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 this.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 { + const idReg = /\d{17,19}/; + const idMatch = text.match(idReg); + if (idMatch) { + try { + return await this.client.users.fetch(text as Snowflake); + } catch {} + } + const mentionReg = /<@!?(?\d{17,19})>/; + const mentionMatch = text.match(mentionReg); + if (mentionMatch) { + try { + return await this.client.users.fetch(mentionMatch.groups!.id as Snowflake); + } catch {} + } + const user = this.client.users.cache.find((u) => u.username === text); + if (user) return user; + return null; + } + + /** + * 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 { + let hasteOut = ''; + code = 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}` : ''}` + : `${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 this.client.console.warn(`codeblockError`, `Required Length: ${length}. Actual Length: ${code3.length}`, true); + void this.client.console.warn(`codeblockError`, code3, true); + throw new Error('code too long'); + } + return code3; + } + + /** + * 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 { ...this.client.config.credentials, dbPassword: this.client.config.db.password }) { + const credential = { ...this.client.config.credentials, dbPassword: this.client.config.db.password }[ + credentialName as keyof typeof this.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; + } + + /** + * 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 = inspect(input, inspectOptions ?? undefined); + if (inspectOptions) inspectOptions.inspectStrings = undefined; + input = 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 { + input = 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 = inspect(input, inspectOptions ?? undefined); + return this.redact(input); + } + + /** + * 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(key: K): GlobalCache[K]; + public getGlobal(key?: keyof GlobalCache) { + return key ? this.client.cache.global[key] : this.client.cache.global; + } + + /** + * Get the shared cache. + */ + public getShared(): SharedCache; + /** + * Get a key from the shared cache. + * @param key The key to get in the shared cache. + */ + public getShared(key: K): SharedCache[K]; + public getShared(key?: keyof SharedCache) { + return key ? this.client.cache.shared[key] : this.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( + action: 'add' | 'remove', + key: K, + value: Client['cache']['global'][K][0] + ): Promise { + const row = + (await Global.findByPk(this.client.config.environment)) ?? + (await Global.create({ environment: this.client.config.environment })); + const oldValue: any[] = row[key]; + const newValue = addOrRemoveFromArray(action, oldValue, value); + row[key] = newValue; + this.client.cache.global[key] = newValue; + return await row.save().catch((e) => this.handleError('insertOrRemoveFro