From fc390ffc300334c396d9d06b0feaf8fbc6ed2814 Mon Sep 17 00:00:00 2001 From: IRONM00N <64110067+IRONM00N@users.noreply.github.com> Date: Sun, 26 Dec 2021 17:16:32 -0500 Subject: documentation, bug fixes etc --- src/lib/common/AutoMod.ts | 1 + src/lib/common/ButtonPaginator.ts | 3 +- src/lib/common/DeleteButton.ts | 3 +- src/lib/common/Moderation.ts | 166 ++++++++++-- src/lib/common/util/Arg.ts | 56 ++-- src/lib/extensions/discord-akairo/BushClient.ts | 195 ++++++++++---- .../extensions/discord-akairo/BushClientUtil.ts | 102 +++++++- src/lib/extensions/discord-akairo/BushCommand.ts | 188 +++++++------- src/lib/extensions/discord-akairo/BushInhibitor.ts | 7 + .../extensions/discord-akairo/BushSlashMessage.ts | 2 +- src/lib/extensions/discord.js/BushActivity.ts | 3 + .../discord.js/BushApplicationCommand.ts | 3 + .../discord.js/BushApplicationCommandManager.d.ts | 101 ++++++- .../BushApplicationCommandPermissionsManager.d.ts | 123 +++++++++ .../discord.js/BushBaseGuildEmojiManager.d.ts | 8 + .../discord.js/BushBaseGuildTextChannel.ts | 3 + .../discord.js/BushBaseGuildVoiceChannel.d.ts | 13 + .../extensions/discord.js/BushButtonInteraction.ts | 7 +- .../extensions/discord.js/BushCategoryChannel.ts | 7 +- src/lib/extensions/discord.js/BushChannel.d.ts | 14 +- .../extensions/discord.js/BushChannelManager.d.ts | 20 +- .../extensions/discord.js/BushClientEvents.d.ts | 29 ++- src/lib/extensions/discord.js/BushClientUser.d.ts | 78 +++++- .../discord.js/BushCommandInteraction.ts | 31 ++- src/lib/extensions/discord.js/BushDMChannel.ts | 3 + src/lib/extensions/discord.js/BushEmoji.ts | 3 + src/lib/extensions/discord.js/BushGuild.ts | 109 ++++++-- .../BushGuildApplicationCommandManager.d.ts | 89 ++++++- src/lib/extensions/discord.js/BushGuildBan.d.ts | 3 + src/lib/extensions/discord.js/BushGuildChannel.ts | 9 + src/lib/extensions/discord.js/BushGuildEmoji.ts | 3 + .../discord.js/BushGuildEmojiRoleManager.d.ts | 36 +++ .../extensions/discord.js/BushGuildManager.d.ts | 16 ++ src/lib/extensions/discord.js/BushGuildMember.ts | 289 +++++++++++++++------ .../discord.js/BushGuildMemberManager.d.ts | 131 ++++++++++ src/lib/extensions/discord.js/BushMessage.ts | 10 +- .../extensions/discord.js/BushMessageManager.d.ts | 84 +++++- .../extensions/discord.js/BushMessageReaction.ts | 3 + src/lib/extensions/discord.js/BushNewsChannel.ts | 3 + src/lib/extensions/discord.js/BushPresence.ts | 3 + src/lib/extensions/discord.js/BushReactionEmoji.ts | 5 + src/lib/extensions/discord.js/BushRole.ts | 3 + .../discord.js/BushSelectMenuInteraction.ts | 7 +- src/lib/extensions/discord.js/BushStageChannel.ts | 5 +- src/lib/extensions/discord.js/BushStageInstance.ts | 3 + src/lib/extensions/discord.js/BushStoreChannel.ts | 4 + src/lib/extensions/discord.js/BushTextChannel.ts | 3 + src/lib/extensions/discord.js/BushThreadChannel.ts | 3 + .../extensions/discord.js/BushThreadManager.d.ts | 57 ++++ src/lib/extensions/discord.js/BushThreadMember.ts | 3 + .../discord.js/BushThreadMemberManager.d.ts | 31 ++- src/lib/extensions/discord.js/BushUser.ts | 9 + src/lib/extensions/discord.js/BushUserManager.d.ts | 59 ++++- src/lib/extensions/discord.js/BushVoiceChannel.ts | 3 + src/lib/extensions/discord.js/BushVoiceState.ts | 9 +- src/lib/extensions/global.d.ts | 7 + src/lib/utils/BushCache.ts | 4 +- src/lib/utils/BushConstants.ts | 4 +- src/lib/utils/BushLogger.ts | 118 +++++++-- 59 files changed, 1908 insertions(+), 386 deletions(-) create mode 100644 src/lib/extensions/discord.js/BushBaseGuildVoiceChannel.d.ts (limited to 'src/lib') diff --git a/src/lib/common/AutoMod.ts b/src/lib/common/AutoMod.ts index 932d457..e5487c3 100644 --- a/src/lib/common/AutoMod.ts +++ b/src/lib/common/AutoMod.ts @@ -261,6 +261,7 @@ export class AutoMod { .addField('Message Content', `${await util.codeblock(this.message.content, 1024)}`) .setColor(color) .setTimestamp() + .setAuthor({name: this.message.author.tag, url: this.message.author.displayAvatarURL()}) ], components: highestOffence.severity >= 2 diff --git a/src/lib/common/ButtonPaginator.ts b/src/lib/common/ButtonPaginator.ts index 983eb56..d193b4d 100644 --- a/src/lib/common/ButtonPaginator.ts +++ b/src/lib/common/ButtonPaginator.ts @@ -1,4 +1,5 @@ import { DeleteButton, type BushMessage, type BushSlashMessage } from '#lib'; +import { CommandUtil } from 'discord-akairo'; import { Constants, MessageActionRow, @@ -120,7 +121,7 @@ export class ButtonPaginator { } protected async end() { - if (this.sentMessage && !this.sentMessage.deleted) + if (this.sentMessage && !CommandUtil.deletedMessages.has(this.sentMessage.id)) return await this.sentMessage .edit({ content: this.text, diff --git a/src/lib/common/DeleteButton.ts b/src/lib/common/DeleteButton.ts index 38ce6df..e2509a9 100644 --- a/src/lib/common/DeleteButton.ts +++ b/src/lib/common/DeleteButton.ts @@ -1,4 +1,5 @@ import { PaginateEmojis, type BushMessage, type BushSlashMessage } from '#lib'; +import { CommandUtil } from 'discord-akairo'; import { Constants, MessageActionRow, MessageButton, type MessageComponentInteraction, type MessageOptions } from 'discord.js'; export class DeleteButton { @@ -32,7 +33,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 (msg.deletable && !msg.deleted) await msg.delete(); + if (msg.deletable && !CommandUtil.deletedMessages.has(msg.id)) await msg.delete(); } }); diff --git a/src/lib/common/Moderation.ts b/src/lib/common/Moderation.ts index a7a037f..ab2943b 100644 --- a/src/lib/common/Moderation.ts +++ b/src/lib/common/Moderation.ts @@ -10,13 +10,18 @@ import { } from '#lib'; import { type Snowflake } from 'discord.js'; +/** + * A utility class with moderation-related methods. + */ export class Moderation { /** * Checks if a moderator can perform a moderation action on another user. - * @param moderator - The person trying to perform the action. - * @param victim - The person getting punished. - * @param type - The type of punishment - used to format the response. - * @param checkModerator - Whether or not to check if the victim is a moderator. + * @param moderator The person trying to perform the action. + * @param victim The person getting punished. + * @param type The type of punishment - used to format the response. + * @param checkModerator Whether or not to check if the victim is a moderator. + * @param force Override permissions checks. + * @returns `true` if the moderator can perform the action otherwise a reason why they can't. */ public static async permissionCheck( moderator: BushGuildMember, @@ -61,17 +66,14 @@ export class Moderation { return true; } + /** + * Creates a modlog entry for a punishment. + * @param options Options for creating a modlog entry. + * @param getCaseNumber Whether or not to get the case number of the entry. + * @returns An object with the modlog and the case number. + */ public static async createModLogEntry( - options: { - type: ModLogType; - user: BushGuildMemberResolvable; - moderator: BushGuildMemberResolvable; - reason: string | undefined | null; - duration?: number; - guild: BushGuildResolvable; - pseudo?: boolean; - evidence?: string; - }, + options: CreateModLogEntryOptions, getCaseNumber = false ): Promise<{ log: ModLog | null; caseNum: number | null }> { const user = (await util.resolveNonCachedUser(options.user))!.id; @@ -111,14 +113,12 @@ export class Moderation { return { log: saveResult, caseNum }; } - public static async createPunishmentEntry(options: { - type: 'mute' | 'ban' | 'role' | 'block'; - user: BushGuildMemberResolvable; - duration: number | undefined; - guild: BushGuildResolvable; - modlog: string; - extraInfo?: Snowflake; - }): Promise { + /** + * Creates a punishment entry. + * @param options Options for creating the punishment entry. + * @returns The database entry, or null if no entry is created. + */ + public static async createPunishmentEntry(options: CreatePunishmentEntryOptions): Promise { const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined; const user = (await util.resolveNonCachedUser(options.user))!.id; const guild = client.guilds.resolveId(options.guild)!; @@ -135,12 +135,12 @@ export class Moderation { }); } - public static async removePunishmentEntry(options: { - type: 'mute' | 'ban' | 'role' | 'block'; - user: BushGuildMemberResolvable; - guild: BushGuildResolvable; - extraInfo?: Snowflake; - }): Promise { + /** + * Destroys a punishment entry. + * @param options Options for destroying the punishment entry. + * @returns Whether or not the entry was destroyed. + */ + public static async removePunishmentEntry(options: RemovePunishmentEntryOptions): Promise { const user = await util.resolveNonCachedUser(options.user); const guild = client.guilds.resolveId(options.guild); const type = this.findTypeEnum(options.type); @@ -171,6 +171,11 @@ export class Moderation { return success; } + /** + * Returns the punishment type enum for the given type. + * @param type The type of the punishment. + * @returns The punishment type enum. + */ private static findTypeEnum(type: 'mute' | 'ban' | 'role' | 'block') { const typeMap = { ['mute']: ActivePunishmentType.MUTE, @@ -181,3 +186,108 @@ export class Moderation { return typeMap[type]; } } + +/** + * Options for creating a modlog entry. + */ +export interface CreateModLogEntryOptions { + /** + * The type of modlog entry. + */ + type: ModLogType; + + /** + * The user that a modlog entry is created for. + */ + user: BushGuildMemberResolvable; + + /** + * The moderator that created the modlog entry. + */ + moderator: BushGuildMemberResolvable; + + /** + * The reason for the punishment. + */ + reason: string | undefined | null; + + /** + * The duration of the punishment. + */ + duration?: number; + + /** + * The guild that the punishment is created for. + */ + guild: BushGuildResolvable; + + /** + * Whether the punishment is a pseudo punishment. + */ + pseudo?: boolean; + + /** + * The evidence for the punishment. + */ + evidence?: string; +} + +/** + * Options for creating a punishment entry. + */ +export interface CreatePunishmentEntryOptions { + /** + * The type of punishment. + */ + type: 'mute' | 'ban' | 'role' | 'block'; + + /** + * The user that the punishment is created for. + */ + user: BushGuildMemberResolvable; + + /** + * The length of time the punishment lasts for. + */ + duration: number | undefined; + + /** + * The guild that the punishment is created for. + */ + guild: BushGuildResolvable; + + /** + * The id of the modlog that is linked to the punishment entry. + */ + modlog: string; + + /** + * The role id if the punishment is a role punishment. + */ + extraInfo?: Snowflake; +} + +/** + * Options for removing a punishment entry. + */ +export interface RemovePunishmentEntryOptions { + /** + * The type of punishment. + */ + type: 'mute' | 'ban' | 'role' | 'block'; + + /** + * The user that the punishment is destroyed for. + */ + user: BushGuildMemberResolvable; + + /** + * The guild that the punishment was in. + */ + guild: BushGuildResolvable; + + /** + * The role id if the punishment is a role punishment. + */ + extraInfo?: Snowflake; +} diff --git a/src/lib/common/util/Arg.ts b/src/lib/common/util/Arg.ts index 9ce8b54..2577db9 100644 --- a/src/lib/common/util/Arg.ts +++ b/src/lib/common/util/Arg.ts @@ -1,7 +1,10 @@ import { BaseBushArgumentType, BushArgumentTypeCaster, BushSlashMessage, type BushArgumentType } from '#lib'; -import { Argument, type ArgumentTypeCaster, type Flag, type ParsedValuePredicate } from 'discord-akairo'; +import { Argument, type Flag, type ParsedValuePredicate } from 'discord-akairo'; import { type Message } from 'discord.js'; +/** + * A wrapper for the {@link Argument} class that adds custom typings. + */ export class Arg { /** * Casts a phrase to this argument's type. @@ -11,14 +14,9 @@ export class Arg { */ public static async cast(type: T, message: Message | BushSlashMessage, phrase: string): Promise>; public static async cast(type: T, message: Message | BushSlashMessage, phrase: string): Promise; - public static async cast(type: T, message: Message | BushSlashMessage, phrase: string): Promise; + public static async cast(type: AT | ATC, message: Message | BushSlashMessage, phrase: string): Promise; public static async cast(type: ATC | AT, message: Message | BushSlashMessage, phrase: string): Promise { - return Argument.cast( - type as ArgumentTypeCaster | keyof BushArgumentType, - client.commandHandler.resolver, - message as Message, - phrase - ); + return Argument.cast(type as any, client.commandHandler.resolver, message as Message, phrase); } /** @@ -28,7 +26,7 @@ export class Arg { */ public static compose(...types: T[]): ATCATCR; public static compose(...types: T[]): ATCBAT; - public static compose(...types: T[]): ATC; + public static compose(...types: (AT | ATC)[]): ATC; public static compose(...types: (AT | ATC)[]): ATC { return Argument.compose(...(types as any)); } @@ -40,7 +38,7 @@ export class Arg { */ public static composeWithFailure(...types: T[]): ATCATCR; public static composeWithFailure(...types: T[]): ATCBAT; - public static composeWithFailure(...types: T[]): ATC; + public static composeWithFailure(...types: (AT | ATC)[]): ATC; public static composeWithFailure(...types: (AT | ATC)[]): ATC { return Argument.composeWithFailure(...(types as any)); } @@ -60,7 +58,7 @@ export class Arg { */ public static product(...types: T[]): ATCATCR; public static product(...types: T[]): ATCBAT; - public static product(...types: T[]): ATC; + public static product(...types: (AT | ATC)[]): ATC; public static product(...types: (AT | ATC)[]): ATC { return Argument.product(...(types as any)); } @@ -74,7 +72,7 @@ export class Arg { */ public static range(type: T, min: number, max: number, inclusive?: boolean): ATCATCR; public static range(type: T, min: number, max: number, inclusive?: boolean): ATCBAT; - public static range(type: T, min: number, max: number, inclusive?: boolean): ATC; + public static range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC; public static range(type: AT | ATC, min: number, max: number, inclusive?: boolean): ATC { return Argument.range(type as any, min, max, inclusive); } @@ -87,7 +85,7 @@ export class Arg { */ public static tagged(type: T, tag?: any): ATCATCR; public static tagged(type: T, tag?: any): ATCBAT; - public static tagged(type: T, tag?: any): ATC; + public static tagged(type: AT | ATC, tag?: any): ATC; public static tagged(type: AT | ATC, tag?: any): ATC { return Argument.tagged(type as any, tag); } @@ -100,7 +98,7 @@ export class Arg { */ public static taggedUnion(...types: T[]): ATCATCR; public static taggedUnion(...types: T[]): ATCBAT; - public static taggedUnion(...types: T[]): ATC; + public static taggedUnion(...types: (AT | ATC)[]): ATC; public static taggedUnion(...types: (AT | ATC)[]): ATC { return Argument.taggedUnion(...(types as any)); } @@ -113,7 +111,7 @@ export class Arg { */ public static taggedWithInput(type: T, tag?: any): ATCATCR; public static taggedWithInput(type: T, tag?: any): ATCBAT; - public static taggedWithInput(type: T, tag?: any): ATC; + public static taggedWithInput(type: AT | ATC, tag?: any): ATC; public static taggedWithInput(type: AT | ATC, tag?: any): ATC { return Argument.taggedWithInput(type as any, tag); } @@ -125,7 +123,7 @@ export class Arg { */ public static union(...types: T[]): ATCATCR; public static union(...types: T[]): ATCBAT; - public static union(...types: T[]): ATC; + public static union(...types: (AT | ATC)[]): ATC; public static union(...types: (AT | ATC)[]): ATC { return Argument.union(...(types as any)); } @@ -138,7 +136,7 @@ export class Arg { */ public static validate(type: T, predicate: ParsedValuePredicate): ATCATCR; public static validate(type: T, predicate: ParsedValuePredicate): ATCBAT; - public static validate(type: T, predicate: ParsedValuePredicate): ATC; + public static validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC; public static validate(type: AT | ATC, predicate: ParsedValuePredicate): ATC { return Argument.validate(type as any, predicate); } @@ -150,39 +148,39 @@ export class Arg { */ public static withInput(type: T): ATC>; public static withInput(type: T): ATCBAT; - public static withInput(type: T): ATC; + public static withInput(type: AT | ATC): ATC; public static withInput(type: AT | ATC): ATC { return Argument.withInput(type as any); } } -type ArgumentTypeCasterReturn = R extends BushArgumentTypeCaster ? S : R; +type BushArgumentTypeCasterReturn = R extends BushArgumentTypeCaster ? S : R; /** ```ts - * = ArgumentTypeCaster + * = BushArgumentTypeCaster * ``` */ type ATC = BushArgumentTypeCaster; /** ```ts - * keyof BaseArgumentType + * keyof BaseBushArgumentType * ``` */ type KBAT = keyof BaseBushArgumentType; /** ```ts - * = ArgumentTypeCasterReturn + * = BushArgumentTypeCasterReturn * ``` */ -type ATCR = ArgumentTypeCasterReturn; +type ATCR = BushArgumentTypeCasterReturn; /** ```ts - * keyof BaseBushArgumentType | string + * BushArgumentType * ``` */ -type AT = BushArgumentTypeCaster | keyof BaseBushArgumentType | string; +type AT = BushArgumentType; /** ```ts - * BaseArgumentType + * BaseBushArgumentType * ``` */ type BAT = BaseBushArgumentType; /** ```ts - * = ArgumentTypeCaster> + * = BushArgumentTypeCaster> * ``` */ -type ATCATCR = BushArgumentTypeCaster>; +type ATCATCR = BushArgumentTypeCaster>; /** ```ts - * = ArgumentTypeCaster + * = BushArgumentTypeCaster * ``` */ type ATCBAT = BushArgumentTypeCaster; diff --git a/src/lib/extensions/discord-akairo/BushClient.ts b/src/lib/extensions/discord-akairo/BushClient.ts index a9e172a..d7c8b60 100644 --- a/src/lib/extensions/discord-akairo/BushClient.ts +++ b/src/lib/extensions/discord-akairo/BushClient.ts @@ -1,26 +1,25 @@ import type { BushApplicationCommand, BushBaseGuildEmojiManager, - BushChannel, BushChannelManager, BushClientEvents, BushClientUser, BushGuildManager, BushReactionEmoji, + BushStageChannel, BushUserManager, Config } from '#lib'; -import { patch, type PatchedElements } from '@notenoughupdates/events-intercept'; +import { patch, PatchedElements } from '@notenoughupdates/events-intercept'; import * as Sentry from '@sentry/node'; import { AkairoClient, ContextMenuCommandHandler, version as akairoVersion } from 'discord-akairo'; import { + Awaitable, Intents, Options, Structures, version as discordJsVersion, - type Awaitable, type Collection, - type DMChannel, type InteractionReplyOptions, type Message, type MessageEditOptions, @@ -31,6 +30,7 @@ import { type Snowflake, type WebhookEditMessageOptions } from 'discord.js'; +import EventEmitter from 'events'; import path from 'path'; import readline from 'readline'; import type { Sequelize as SequelizeType } from 'sequelize'; @@ -100,9 +100,28 @@ export type BushEmojiIdentifierResolvable = string | BushEmojiResolvable; export type BushThreadChannelResolvable = BushThreadChannel | Snowflake; export type BushApplicationCommandResolvable = BushApplicationCommand | Snowflake; export type BushGuildTextChannelResolvable = BushTextChannel | BushNewsChannel | Snowflake; -export type BushChannelResolvable = BushChannel | Snowflake; -export type BushTextBasedChannels = PartialDMChannel | BushDMChannel | BushTextChannel | BushNewsChannel | BushThreadChannel; -export type BushGuildTextBasedChannel = Exclude; +export type BushChannelResolvable = BushAnyChannel | Snowflake; +export type BushGuildChannelResolvable = Snowflake | BushGuildBasedChannel; +export type BushAnyChannel = + | BushCategoryChannel + | BushDMChannel + | PartialDMChannel + | BushNewsChannel + | BushStageChannel + // eslint-disable-next-line deprecation/deprecation + | BushStoreChannel + | BushTextChannel + | BushThreadChannel + | BushVoiceChannel; +export type BushTextBasedChannel = PartialDMChannel | BushThreadChannel | BushDMChannel | BushNewsChannel | BushTextChannel; +export type BushTextBasedChannelTypes = BushTextBasedChannel['type']; +export type BushVoiceBasedChannel = Extract; +export type BushGuildBasedChannel = Extract; +export type BushNonThreadGuildBasedChannel = Exclude; +export type BushGuildTextBasedChannel = Extract; +export type BushTextChannelResolvable = Snowflake | BushTextChannel; +export type BushGuildVoiceChannelResolvable = BushVoiceBasedChannel | Snowflake; + export interface BushFetchedThreads { threads: Collection; hasMore?: boolean; @@ -118,29 +137,86 @@ type If = T extends true ? A : T extends false ? const __dirname = path.dirname(fileURLToPath(import.meta.url)); +/** + * The main hub for interacting with the Discord API. + */ export class BushClient extends AkairoClient { public declare channels: BushChannelManager; public declare readonly emojis: BushBaseGuildEmojiManager; public declare guilds: BushGuildManager; public declare user: If; public declare users: BushUserManager; + public declare util: BushClientUtil; + public declare ownerID: Snowflake[]; + /** + * Whether or not the client is ready. + */ public customReady = false; - public stats: { cpu: number | undefined; commandsUsed: bigint } = { cpu: undefined, commandsUsed: 0n }; + + /** + * Stats for the client. + */ + public stats: BushStats = { cpu: undefined, commandsUsed: 0n }; + + /** + * The configuration for the client. + */ public config: Config; + + /** + * The handler for the bot's listeners. + */ public listenerHandler: BushListenerHandler; + + /** + * The handler for the bot's command inhibitors. + */ public inhibitorHandler: BushInhibitorHandler; + + /** + * The handler for the bot's commands. + */ public commandHandler: BushCommandHandler; + + /** + * The handler for the bot's tasks. + */ public taskHandler: BushTaskHandler; + + /** + * The handler for the bot's context menu commands. + */ public contextMenuCommandHandler: ContextMenuCommandHandler; - public declare util: BushClientUtil; - public declare ownerID: Snowflake[]; + + /** + * The database connection for the bot. + */ public db: SequelizeType; + + /** + * A custom logging system for the bot. + */ public logger = BushLogger; + + /** + * Constants for the bot. + */ public constants = BushConstants; + + /** + * Cached global and guild database data. + */ public cache = new BushCache(); + + /** + * Sentry error reporting for the bot. + */ public sentry!: typeof Sentry; + /** + * @param config The configuration for the bot. + */ public constructor(config: Config) { super({ ownerID: config.owners, @@ -163,25 +239,18 @@ export class BushClient extends AkairoClient; this.config = config; - // Create listener handler this.listenerHandler = new BushListenerHandler(this, { directory: path.join(__dirname, '..', '..', '..', 'listeners'), automateCategories: true }); - - // Create inhibitor handler this.inhibitorHandler = new BushInhibitorHandler(this, { directory: path.join(__dirname, '..', '..', '..', 'inhibitors'), automateCategories: true }); - - // Create task handler this.taskHandler = new BushTaskHandler(this, { directory: path.join(__dirname, '..', '..', '..', 'tasks'), automateCategories: true }); - - // Create command handler this.commandHandler = new BushCommandHandler(this, { directory: path.join(__dirname, '..', '..', '..', 'commands'), prefix: async ({ guild }: Message) => { @@ -215,12 +284,10 @@ export class BushClient extends AkairoClient extends AkairoClient BushGuildEmoji); Structures.extend('DMChannel', () => BushDMChannel); Structures.extend('TextChannel', () => BushTextChannel); @@ -265,18 +341,28 @@ export class BushClient extends AkairoClient BushSelectMenuInteraction); } - // Initialize everything - async #init() { - this.commandHandler.useListenerHandler(this.listenerHandler); + /** + * Initializes the bot. + */ + async init() { + if (!process.version.startsWith('v17.')) { + void (await this.console.error('version', `Please use node <>, not <<${process.version}>>.`, false)); + process.exit(2); + } + this.commandHandler.useInhibitorHandler(this.inhibitorHandler); + this.commandHandler.useListenerHandler(this.listenerHandler); + this.commandHandler.useTaskHandler(this.taskHandler); + this.commandHandler.useContextMenuCommandHandler(this.contextMenuCommandHandler); this.commandHandler.ignorePermissions = this.config.owners; this.commandHandler.ignoreCooldown = [...new Set([...this.config.owners, ...this.cache.global.superUsers])]; this.listenerHandler.setEmitters({ client: this, commandHandler: this.commandHandler, - listenerHandler: this.listenerHandler, inhibitorHandler: this.inhibitorHandler, + listenerHandler: this.listenerHandler, taskHandler: this.taskHandler, + contextMenuCommandHandler: this.contextMenuCommandHandler, process, stdin: rl, gateway: this.ws @@ -301,28 +387,31 @@ export class BushClient extends AkairoClient>.`, false); // loads all the handlers - const loaders = { + const handlers = { commands: this.commandHandler, - contextMenuCommand: this.contextMenuCommandHandler, + contextMenuCommands: this.contextMenuCommandHandler, listeners: this.listenerHandler, inhibitors: this.inhibitorHandler, tasks: this.taskHandler }; - for (const loader in loaders) { - try { - await loaders[loader as keyof typeof loaders].loadAll(); - void this.logger.success('startup', `Successfully loaded <<${loader}>>.`, false); - } catch (e) { - void this.logger.error('startup', `Unable to load loader <<${loader}>> with error:\n${e?.stack || e}`, false); - } - } - await this.dbPreInit(); - await UpdateCacheTask.init(this); - void this.console.success('startup', `Successfully created <>.`, false); - this.stats.commandsUsed = await UpdateStatsTask.init(); + const handlerPromises = Object.entries(handlers).map(([handlerName, handler]) => + handler + .loadAll() + .then(() => { + void this.logger.success('startup', `Successfully loaded <<${handlerName}>>.`, false); + }) + .catch((e) => { + void this.logger.error('startup', `Unable to load loader <<${handlerName}>> with error:\n${e?.stack || e}`, false); + if (process.argv.includes('dry')) process.exit(1); + }) + ); + await Promise.allSettled(handlerPromises); } - public async dbPreInit() { + /** + * Connects to the database, initializes models, and creates tables if they do not exist. + */ + private async dbPreInit() { try { await this.db.authenticate(); Global.initModel(this.db); @@ -348,10 +437,6 @@ export class BushClient extends AkairoClient>, not <<${process.version}>>.`, false)); - process.exit(2); - } this.intercept('ready', async (arg, done) => { await this.guilds.fetch(); const promises = this.guilds.cache.map((guild) => { @@ -368,7 +453,10 @@ export class BushClient extends AkairoClient>.`, false); + this.stats.commandsUsed = await UpdateStatsTask.init(); await this.login(this.token!); } catch (e) { await this.console.error('start', util.inspect(e, { colors: true, depth: 1 }), false); @@ -389,13 +477,14 @@ export class BushClient extends AkairoClient(event: K, listener: (...args: BushClientEvents[K]) => Awaitable): this; on(event: Exclude, listener: (...args: any[]) => Awaitable): this; @@ -411,3 +500,15 @@ export interface BushClient extends PatchedElements { removeAllListeners(event?: K): this; removeAllListeners(event?: Exclude): this; } + +export interface BushStats { + /** + * The average cpu usage of the bot from the past 60 seconds. + */ + cpu: number | undefined; + + /** + * The total number of times any command has been used. + */ + commandsUsed: bigint; +} diff --git a/src/lib/extensions/discord-akairo/BushClientUtil.ts b/src/lib/extensions/discord-akairo/BushClientUtil.ts index ab1f3ed..5ae2ac0 100644 --- a/src/lib/extensions/discord-akairo/BushClientUtil.ts +++ b/src/lib/extensions/discord-akairo/BushClientUtil.ts @@ -2,6 +2,7 @@ import { Arg, BushConstants, Global, + GlobalCache, type BushClient, type BushInspectOptions, type BushMessage, @@ -438,6 +439,12 @@ export class BushClientUtil extends ClientUtil { return array.join(', '); } + public getGlobal(): GlobalCache; + public getGlobal(key: K): GlobalCache[K]; + public getGlobal(key?: keyof GlobalCache) { + return key ? client.cache.global[key] : client.cache.global; + } + /** * Add or remove an element from an array stored in the Globals database. * @param action Either `add` or `remove` an element. @@ -610,11 +617,11 @@ export class BushClientUtil extends ClientUtil { /** * Wait an amount in seconds. - * @param s The number of seconds to wait + * @param seconds The number of seconds to wait * @returns A promise that resolves after the specified amount of seconds */ - public async sleep(s: number) { - return new Promise((resolve) => setTimeout(resolve, s * 1000)); + public async sleep(seconds: number) { + return new Promise((resolve) => setTimeout(resolve, seconds * 1000)); } /** @@ -629,8 +636,13 @@ export class BushClientUtil extends ClientUtil { }); } + /** + * 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 { - if (!user) return undefined; + if (user == null) return undefined; const id = user instanceof User || user instanceof GuildMember || user instanceof ThreadMember ? user.id @@ -643,6 +655,11 @@ export class BushClientUtil extends ClientUtil { else return await client.users.fetch(id).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 { const _user = await this.resolveNonCachedUser(user); if (!_user) throw new Error(`Cannot find user ${user}`); @@ -657,6 +674,11 @@ export class BushClientUtil extends ClientUtil { return client.constants.pronounMapping[apiRes.pronouns!]!; } + /** + * List the methods of an object. + * @param obj The object to get the methods of. + * @returns A string with each method on a new line. + */ public getMethods(obj: Record): string { // modified from https://stackoverflow.com/questions/31054910/get-functions-methods-of-a-class // answer by Bruno Grieder @@ -700,13 +722,17 @@ export class BushClientUtil extends ClientUtil { return props.join('\n'); } + /** + * 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: `Bearer ${token}`, Authorization: `Client-ID ${clientId}`, Accept: 'application/json' }, @@ -721,18 +747,38 @@ export class BushClientUtil extends ClientUtil { return resp.data.link; } + /** + * Checks if a user has a certain guild permission (doesn't check channel permissions). + * @param message The message to check the user from. + * @param permissions The permissions to check for. + * @returns The missing permissions or null if none are missing. + */ public userGuildPermCheck(message: BushMessage | BushSlashMessage, permissions: PermissionResolvable) { const missing = message.member?.permissions.missing(permissions) ?? []; return missing.length ? missing : null; } + /** + * Check if the client has certain permissions in the guild (doesn't check channel permissions). + * @param message The message to check the client user from. + * @param permissions The permissions to check for. + * @returns The missing permissions or null if none are missing. + */ public clientGuildPermCheck(message: BushMessage | BushSlashMessage, permissions: PermissionResolvable) { const missing = message.guild?.me?.permissions.missing(permissions) ?? []; return missing.length ? missing : null; } + /** + * Check if the client has permission to send messages in the channel as well as check if they have other permissions + * in the guild (or the channel if `checkChannel` is `true`). + * @param message The message to check the client user from. + * @param permissions The permissions to check for. + * @param checkChannel Whether to check the channel permissions instead of the guild permissions. + * @returns The missing permissions or null if none are missing. + */ public clientSendAndPermCheck( message: BushMessage | BushSlashMessage, permissions: PermissionResolvable = [], @@ -752,6 +798,11 @@ export class BushClientUtil extends ClientUtil { return missing.length ? missing : null; } + /** + * Gets the prefix based off of the message. + * @param message The message to get the prefix from. + * @returns The prefix. + */ public prefix(message: BushMessage | BushSlashMessage): string { return message.util.isSlash ? '/' @@ -760,14 +811,55 @@ export class BushClientUtil extends ClientUtil { : message.util.parsed?.prefix ?? client.config.prefix; } + /** + * Recursively apply provided options operations on object + * and all of the object properties that are either object or function. + * + * By default freezes object. + * + * @param obj - The object to which will be applied `freeze`, `seal` or `preventExtensions` + * @param options default `{ action: 'freeze' }` + * @param options.action + * ``` + * | action | Add | Modify | Delete | Reconfigure | + * | ----------------- | --- | ------ | ------ | ----------- | + * | preventExtensions | - | + | + | + | + * | seal | - | + | - | - | + * | freeze | - | - | - | - | + * ``` + * + * @returns Initial object with applied options action + */ public get deepFreeze() { return deepLock; } + /** + * Recursively apply provided options operations on object + * and all of the object properties that are either object or function. + * + * By default freezes object. + * + * @param obj - The object to which will be applied `freeze`, `seal` or `preventExtensions` + * @param options default `{ action: 'freeze' }` + * @param options.action + * ``` + * | action | Add | Modify | Delete | Reconfigure | + * | ----------------- | --- | ------ | ------ | ----------- | + * | preventExtensions | - | + | + | + | + * | seal | - | + | - | - | + * | freeze | - | - | - | - | + * ``` + * + * @returns Initial object with applied options action + */ public static get deepFreeze() { return deepLock; } + /** + * A wrapper for the Argument class that adds custom typings. + */ public get arg() { return Arg; } diff --git a/src/lib/extensions/discord-akairo/BushCommand.ts b/src/lib/extensions/discord-akairo/BushCommand.ts index ae3dcb2..6b54e20 100644 --- a/src/lib/extensions/discord-akairo/BushCommand.ts +++ b/src/lib/extensions/discord-akairo/BushCommand.ts @@ -174,7 +174,7 @@ export interface CustomBushArgumentOptions extends BaseBushArgumentOptions { export type BushMissingPermissionSupplier = (message: BushMessage | BushSlashMessage) => Promise | any; -export interface BaseBushCommandOptions extends Omit { +interface ExtendedCommandOptions { /** * Whether the command is hidden from the help command. */ @@ -190,11 +190,6 @@ export interface BaseBushCommandOptions extends Omit