diff options
Diffstat (limited to 'src/lib')
20 files changed, 743 insertions, 629 deletions
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<T extends ATC>(type: T, message: CommandMessage | Sla 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); + 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<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 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<boolean> { - 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<boolean> { new ActionRowBuilder<ButtonBuilder>({ 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<boolean> { }) ]; - 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<boolean> { return !!dmSuccess; } -interface BaseCreateModLogEntryOptions { +interface BaseCreateModLogEntryOptions extends BaseOptions { /** * The type of modlog entry. */ @@ -355,6 +354,11 @@ interface BaseCreateModLogEntryOptions { */ export interface CreateModLogEntryOptions extends BaseCreateModLogEntryOptions { /** + * The client. + */ + client: Client; + + /** * The user that a modlog entry is created for. */ user: GuildMemberResolvable; @@ -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<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; @@ -126,72 +157,77 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re /** * Whether or not the client is ready. */ - public customReady = false; + public override customReady = false; /** * Stats for the client. */ - public stats: BushStats = { cpu: undefined, commandsUsed: 0n, slashCommandsUsed: 0n }; + public override stats: BushStats = { cpu: undefined, commandsUsed: 0n, slashCommandsUsed: 0n }; /** * The handler for the bot's listeners. */ - public listenerHandler: BushListenerHandler; + public override listenerHandler: BushListenerHandler; /** * The handler for the bot's command inhibitors. */ - public inhibitorHandler: BushInhibitorHandler; + public override inhibitorHandler: BushInhibitorHandler; /** * The handler for the bot's commands. */ - public commandHandler: BushCommandHandler; + public override commandHandler: BushCommandHandler; /** * The handler for the bot's tasks. */ - public taskHandler: BushTaskHandler; + public override taskHandler: BushTaskHandler; /** * The handler for the bot's context menu commands. */ - public contextMenuCommandHandler: ContextMenuCommandHandler; + public override contextMenuCommandHandler: ContextMenuCommandHandler; /** * The database connection for this instance of the bot (production, beta, or development). */ - public instanceDB: SequelizeType; + public override instanceDB: SequelizeType; /** * The database connection that is shared between all instances of the bot. */ - public sharedDB: SequelizeType; + public override sharedDB: SequelizeType; /** * A custom logging system for the bot. */ - public logger = BushLogger; + public override logger: BushLogger; /** * Cached global and guild database data. */ - public cache = new BushCache(); + public override cache = new BushCache(); /** * Sentry error reporting for the bot. */ - public sentry!: typeof Sentry; + public override sentry!: typeof Sentry; /** * Manages most aspects of the highlight command */ - public highlightManager = new HighlightManager(); + public override highlightManager = new HighlightManager(); /** * The perspective api */ - public perspective: any; + public override perspective: any; + + /** + * Client utilities. + */ + public override utils: BushClientUtils; /** * @param config The configuration for the bot. @@ -200,7 +236,7 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re /** * The configuration for the client. */ - public config: Config + public override config: Config ) { super({ ownerID: config.owners, @@ -220,7 +256,8 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re patch(this); this.token = config.token as If<Ready, string, string | null>; - 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<Ready extends boolean = boolean> extends AkairoClient<Re /** * A custom logging system for the bot. */ - public get console(): typeof BushLogger { + public override get console(): BushLogger { return this.logger; } @@ -474,7 +511,7 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re await this.highlightManager.syncCache(); await UpdateCacheTask.init(this); void this.console.success('startup', `Successfully created <<cache>>.`, 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<Ready extends boolean = boolean> extends AkairoClient<Re public override isSuperUser(user: UserResolvable): boolean { const userID = this.users.resolveId(user)!; - return client.cache.shared.superUsers.includes(userID) || this.config.owners.includes(userID); + return this.cache.shared.superUsers.includes(userID) || this.config.owners.includes(userID); } } diff --git a/src/lib/extensions/discord-akairo/BushCommand.ts b/src/lib/extensions/discord-akairo/BushCommand.ts index 5fb4e06..414da09 100644 --- a/src/lib/extensions/discord-akairo/BushCommand.ts +++ b/src/lib/extensions/discord-akairo/BushCommand.ts @@ -34,6 +34,8 @@ import { Message, User, type ApplicationCommandOptionChoiceData, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type ApplicationCommandOptionType, type PermissionResolvable, type PermissionsString, type Snowflake @@ -93,7 +95,7 @@ interface BaseBushArgumentOptions extends Omit<ArgumentOptions, 'type' | 'prompt /** * Allows you to get a discord resolved object * - * ex. get the resolved member object when the type is `USER` + * ex. get the resolved member object when the type is {@link ApplicationCommandOptionType.User User} */ slashResolve?: SlashResolveType; @@ -113,12 +115,12 @@ interface BaseBushArgumentOptions extends Omit<ArgumentOptions, 'type' | 'prompt channelTypes?: AkairoApplicationCommandChannelOptionData['channelTypes']; /** - * The minimum value for an `INTEGER` or `NUMBER` option + * The minimum value for an {@link ApplicationCommandOptionType.Integer Integer} or {@link ApplicationCommandOptionType.Number Number} option */ minValue?: number; /** - * The maximum value for an `INTEGER` or `NUMBER` option + * The maximum value for an {@link ApplicationCommandOptionType.Integer Integer} or {@link ApplicationCommandOptionType.Number Number} option */ maxValue?: number; } diff --git a/src/lib/extensions/discord-akairo/BushCommandHandler.ts b/src/lib/extensions/discord-akairo/BushCommandHandler.ts index f095356..da49af9 100644 --- a/src/lib/extensions/discord-akairo/BushCommandHandler.ts +++ b/src/lib/extensions/discord-akairo/BushCommandHandler.ts @@ -1,4 +1,4 @@ -import { type BushClient, type BushCommand, type CommandMessage, type SlashMessage } from '#lib'; +import { type BushCommand, type CommandMessage, type SlashMessage } from '#lib'; import { CommandHandler, type Category, type CommandHandlerEvents, type CommandHandlerOptions } from 'discord-akairo'; import { type Collection, type Message, type PermissionsString } from 'discord.js'; @@ -28,13 +28,8 @@ export interface BushCommandHandlerEvents extends CommandHandlerEvents { } export class BushCommandHandler extends CommandHandler { - public declare client: BushClient; public declare modules: Collection<string, BushCommand>; public declare categories: Collection<string, Category<string, BushCommand>>; - - 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<K extends keyof GuildModel>(setting: K): Promise<GuildModel[K]> { 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> { - 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<boolean> { 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<Cached extends boolean = boolean> extends Message<C public constructor(client_: Client, data: RawMessageData) { super(client_, data); - this.util = new CommandUtil(client.commandHandler, this); + this.util = new CommandUtil(this.client.commandHandler, this); } } diff --git a/src/lib/extensions/discord.js/ExtendedUser.ts b/src/lib/extensions/discord.js/ExtendedUser.ts index 556ab85..23de523 100644 --- a/src/lib/extensions/discord.js/ExtendedUser.ts +++ b/src/lib/extensions/discord.js/ExtendedUser.ts @@ -23,13 +23,13 @@ export class ExtendedUser extends User { * Indicates whether the user is an owner of the bot. */ public override isOwner(): boolean { - return client.isOwner(this); + return this.client.isOwner(this); } /** * Indicates whether the user is a superuser of the bot. */ public override isSuperUser(): boolean { - return client.isSuperUser(this); + return this.client.isSuperUser(this); } } diff --git a/src/lib/extensions/global.ts b/src/lib/extensions/global.ts index d9cfaec..a9020d7 100644 --- a/src/lib/extensions/global.ts +++ b/src/lib/extensions/global.ts @@ -1,11 +1,5 @@ /* eslint-disable no-var */ -import type { BushClient } from '#lib'; declare global { - /** - * The bushbot client. - */ - var client: BushClient; - // eslint-disable-next-line @typescript-eslint/no-unused-vars interface ReadonlyArray<T> { includes<S, R extends `${Extract<S, string>}`>( @@ -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 = <T>(et: { [K in keyof T]: PartialBy<GuildSetting, 'config return et as { [K in keyof T]: GuildSetting }; }; +const { default: config } = await import('../../../../config/options.js'); + export const guildSettingsObj = asGuildSetting({ prefix: { name: 'Prefix', description: 'The phrase required to trigger text commands in this server.', type: 'string', - replaceNullWith: () => 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<User[]> { + 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<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 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<User | null> { + 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 = /<@!?(?<id>\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<string> { + 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<HasteResults> { + 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<K extends keyof GlobalCache>(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<K extends keyof SharedCache>(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<K extends keyof Client['cache']['global']>( + action: 'add' | 'remove', + key: K, + value: Client['cache']['global'][K][0] + ): Promise<Global | void> { + 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('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 Client['cache']['shared'], 'badWords' | 'autoBanCode'>>( + action: 'add' | 'remove', + key: K, + value: 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; + this.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 Client['cache']['global']>( + key: K, + value: Client['cache']['global'][K] + ): Promise<Global | void> { + const row = + (await Global.findByPk(this.client.config.environment)) ?? + (await Global.create({ environment: this.client.config.environment })); + row[key] = value; + this.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 Client['cache']['shared'], 'badWords' | 'autoBanCode'>>( + key: K, + value: Client['cache']['shared'][K] + ): Promise<Shared | void> { + const row = (await Shared.findByPk(0)) ?? (await Shared.create()); + row[key] = value; + this.client.cache.shared[key] = value; + return await row.save().catch((e) => this.handleError('setShared', e)); + } + + /** + * 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 this.client.console.error(_.camelCase(context), `An error occurred:\n${formatError(error, false)}`, false); + await this.client.console.channelError({ + embeds: await CommandErrorListener.generateErrorEmbed(this.client, { type: 'unhandledRejection', error: error, context }) + }); + } + + /** + * 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 this.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 pronounMapping[apiRes.pronouns!]!; + } + + /** + * 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; + } + + /** + * 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 + ? '/' + : this.client.config.isDevelopment + ? 'dev ' + : message.util.parsed?.prefix ?? this.client.config.prefix; + } + + public async 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; + } + + 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 = this.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 this.client.rest + .get(Routes.channelMessage(channel_id, message_id)) + .catch(() => null)) as APIMessage | null; + if (!message) continue; + + res.push(message); + } + + return res; + } + + /** + * Gets a a configured channel as a TextChannel. + * @channel The channel to retrieve. + */ + public async getConfigChannel(channel: keyof Client['config']['channels']): Promise<TextChannel> { + return (await this.client.channels.fetch(this.client.config.channels[channel])) as unknown as TextChannel; + } +} + +interface HastebinRes { + key: string; +} + +export interface HasteResults { + url?: string; + error?: 'content too long' | 'substr' | 'unable to post'; +} + +export interface MessageLinkParts { + guild_id: Snowflake; + channel_id: Snowflake; + message_id: Snowflake; +} diff --git a/src/lib/utils/BushLogger.ts b/src/lib/utils/BushLogger.ts index 7d42574..1be58a4 100644 --- a/src/lib/utils/BushLogger.ts +++ b/src/lib/utils/BushLogger.ts @@ -1,11 +1,11 @@ import chalk from 'chalk'; // eslint-disable-next-line @typescript-eslint/no-unused-vars -import { EmbedBuilder, escapeMarkdown, PartialTextBasedChannelFields, type Message } from 'discord.js'; +import { Client, EmbedBuilder, escapeMarkdown, PartialTextBasedChannelFields, type Message } from 'discord.js'; import repl, { REPLServer, REPL_MODE_STRICT } from 'repl'; import { WriteStream } from 'tty'; import { type SendMessageType } from '../extensions/discord-akairo/BushClient.js'; import { colors } from './BushConstants.js'; -import { getConfigChannel, inspect } from './BushUtils.js'; +import { inspect } from './BushUtils.js'; let REPL: REPLServer; let replGone = false; @@ -131,7 +131,14 @@ function getTimeStamp(): string { /** * Custom logging utility for the bot. */ -export default { +export class BushLogger { + public constructor( + /** + * The client. + */ + public client: Client + ) {} + /** * Logs information. Highlight information by surrounding it in `<<>>`. * @param header The header displayed before the content, displayed in cyan. @@ -139,27 +146,27 @@ export default { * @param sendChannel Should this also be logged to discord? Defaults to false. * @param depth The depth the content will inspected. Defaults to 0. */ - get log() { + public 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. */ - async channelLog(message: SendMessageType): Promise<Message | null> { - const channel = await getConfigChannel('log'); + public async channelLog(message: SendMessageType): Promise<Message | null> { + const channel = await this.client.utils.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. */ - async channelError(message: SendMessageType): Promise<Message | null> { - const channel = await getConfigChannel('error'); + public async channelError(message: SendMessageType): Promise<Message | null> { + const channel = await this.client.utils.getConfigChannel('error'); if (!channel) { void this.error( 'BushLogger', @@ -171,27 +178,27 @@ export default { 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`. */ - debug(content: any, depth = 0): void { - if (!client.config.isDevelopment) return; + public debug(content: any, depth = 0): void { + if (!this.client.config.isDevelopment) return; 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. */ - debugRaw(...content: any): void { - if (!client.config.isDevelopment) return; + public debugRaw(...content: any): void { + if (!this.client.config.isDevelopment) return; console.log(`${chalk.bgMagenta(getTimeStamp())} ${chalk.magenta('[Debug]')}`, ...content); - }, + } /** * Logs verbose information. Highlight information by surrounding it in `<<>>`. @@ -200,8 +207,8 @@ export default { * @param sendChannel Should this also be logged to discord? Defaults to `false`. * @param depth The depth the content will inspected. Defaults to `0`. */ - async verbose(header: string, content: any, sendChannel = false, depth = 0): Promise<void> { - if (!client.config.logging.verbose) return; + public async verbose(header: string, content: any, sendChannel = false, depth = 0): Promise<void> { + if (!this.client.config.logging.verbose) return; const newContent = inspectContent(content, depth, true); console.log(`${chalk.bgGrey(getTimeStamp())} ${chalk.grey(`[${header}]`)} ${parseFormatting(newContent, 'blackBright')}`); if (!sendChannel) return; @@ -210,7 +217,7 @@ export default { .setColor(colors.gray) .setTimestamp(); await this.channelLog({ embeds: [embed] }); - }, + } /** * Logs very verbose information. Highlight information by surrounding it in `<<>>`. @@ -218,23 +225,23 @@ export default { * @param content The content to log, highlights displayed in bright black. * @param depth The depth the content will inspected. Defaults to `0`. */ - async superVerbose(header: string, content: any, depth = 0): Promise<void> { - if (!client.config.logging.verbose) return; + public async superVerbose(header: string, content: any, depth = 0): Promise<void> { + if (!this.client.config.logging.verbose) return; const newContent = inspectContent(content, depth, true); console.log( `${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. */ - async superVerboseRaw(header: string, ...content: any[]): Promise<void> { - if (!client.config.logging.verbose) return; + public async superVerboseRaw(header: string, ...content: any[]): Promise<void> { + if (!this.client.config.logging.verbose) return; console.log(`${chalk.bgHex('#a3a3a3')(getTimeStamp())} ${chalk.hex('#a3a3a3')(`[${header}]`)}`, ...content); - }, + } /** * Logs information. Highlight information by surrounding it in `<<>>`. @@ -243,8 +250,8 @@ export default { * @param sendChannel Should this also be logged to discord? Defaults to `false`. * @param depth The depth the content will inspected. Defaults to `0`. */ - async info(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { - if (!client.config.logging.info) return; + public async info(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { + if (!this.client.config.logging.info) return; const newContent = inspectContent(content, depth, true); console.log(`${chalk.bgCyan(getTimeStamp())} ${chalk.cyan(`[${header}]`)} ${parseFormatting(newContent, 'blueBright')}`); if (!sendChannel) return; @@ -253,7 +260,7 @@ export default { .setColor(colors.info) .setTimestamp(); await this.channelLog({ embeds: [embed] }); - }, + } /** * Logs warnings. Highlight information by surrounding it in `<<>>`. @@ -262,7 +269,7 @@ export default { * @param sendChannel Should this also be logged to discord? Defaults to `false`. * @param depth The depth the content will inspected. Defaults to `0`. */ - async warn(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { + public async warn(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { const newContent = inspectContent(content, depth, true); console.warn( `${chalk.bgYellow(getTimeStamp())} ${chalk.yellow(`[${header}]`)} ${parseFormatting(newContent, 'yellowBright')}` @@ -274,7 +281,7 @@ export default { .setColor(colors.warn) .setTimestamp(); await this.channelError({ embeds: [embed] }); - }, + } /** * Logs errors. Highlight information by surrounding it in `<<>>`. @@ -283,7 +290,7 @@ export default { * @param sendChannel Should this also be logged to discord? Defaults to `false`. * @param depth The depth the content will inspected. Defaults to `0`. */ - async error(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { + public async error(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { const newContent = inspectContent(content, depth, true); console.warn( `${chalk.bgRedBright(getTimeStamp())} ${chalk.redBright(`[${header}]`)} ${parseFormatting(newContent, 'redBright')}` @@ -295,7 +302,7 @@ export default { .setTimestamp(); await this.channelError({ embeds: [embed] }); return; - }, + } /** * Logs successes. Highlight information by surrounding it in `<<>>`. @@ -304,7 +311,7 @@ export default { * @param sendChannel Should this also be logged to discord? Defaults to `false`. * @param depth The depth the content will inspected. Defaults to `0`. */ - async success(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { + public async success(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { const newContent = inspectContent(content, depth, true); console.log( `${chalk.bgGreen(getTimeStamp())} ${chalk.greenBright(`[${header}]`)} ${parseFormatting(newContent, 'greenBright')}` @@ -316,6 +323,6 @@ export default { .setTimestamp(); await this.channelLog({ embeds: [embed] }).catch(() => {}); } -}; +} /** @typedef {PartialTextBasedChannelFields} vscodeDontDeleteMyImportTy */ diff --git a/src/lib/utils/BushUtils.ts b/src/lib/utils/BushUtils.ts index 8a84d80..a6463cf 100644 --- a/src/lib/utils/BushUtils.ts +++ b/src/lib/utils/BushUtils.ts @@ -1,22 +1,12 @@ 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'; @@ -25,59 +15,25 @@ 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 + type PermissionsString } 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 @@ -96,60 +52,6 @@ export async function shell(command: string): Promise<{ stdout: string; stderr: } /** - * 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 @@ -185,46 +87,6 @@ export async function mcUUID(username: string, dashed = false): Promise<string> } /** - * 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. @@ -246,44 +108,6 @@ function getDefaultInspectOptions(options?: BushInspectOptions): BushInspectOpti } /** - * 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. @@ -298,51 +122,6 @@ export function inspect(object: any, options?: BushInspectOptions): string { } /** - * 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. @@ -363,14 +142,6 @@ export async function slashRespond( } /** - * 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. @@ -392,93 +163,6 @@ export function oxford(array: string[], conjunction: string, ifEmpty?: string): } /** - * 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. @@ -643,58 +327,6 @@ export function hexToRgb(hex: string): string { 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. @@ -763,31 +395,6 @@ export function getSymbols(obj: Record<string, any>): symbol[] { } /** - * 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. @@ -843,15 +450,6 @@ export function clientSendAndPermCheck( 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 }; @@ -950,48 +548,6 @@ export function overflowEmbed(embed: Omit<APIEmbed, 'description'>, lines: strin 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. @@ -1011,10 +567,6 @@ export function formatError(error: Error | any, colors = false): string { return error.stack; } -interface HastebinRes { - key: string; -} - export interface UuidRes { uuid: string; username: string; @@ -1034,11 +586,6 @@ export interface UuidRes { 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; @@ -1050,9 +597,3 @@ export interface ParsedDurationRes { } export type TimestampStyle = 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R'; - -export interface MessageLinkParts { - guild_id: Snowflake; - channel_id: Snowflake; - message_id: Snowflake; -} |