diff options
80 files changed, 2133 insertions, 557 deletions
diff --git a/.vscode/settings.json b/.vscode/settings.json index bf53657..b6f5614 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,7 +27,7 @@ "editor.insertSpaces": false, "editor.wordWrap": "on", "editor.tabSize": 2, - "prettier.configPath": "package.json", + "prettier.configPath": ".prettierrc.json", "prettier.prettierPath": "node_modules/prettier", "prettier.withNodeModules": true, "prettier.useEditorConfig": false, @@ -6,5 +6,7 @@ import { Sentry } from './lib/common/Sentry.js'; import { BushClient } from './lib/index.js'; new Sentry(dirname(fileURLToPath(import.meta.url)) || process.cwd()); -BushClient.init(); -void new BushClient(config).start(); +BushClient.extendStructures(); +const client = new BushClient(config); +await client.init(); +if (!process.argv.includes('dry')) await client.start(); diff --git a/src/commands/config/blacklist.ts b/src/commands/config/blacklist.ts index da4ad18..a6e6a3d 100644 --- a/src/commands/config/blacklist.ts +++ b/src/commands/config/blacklist.ts @@ -1,6 +1,5 @@ -import { AllowedMentions, BushCommand, Global, type BushMessage, type BushSlashMessage } from '#lib'; -import { GuildTextBasedChannels } from 'discord-akairo'; -import { User } from 'discord.js'; +import { AllowedMentions, BushCommand, type BushMessage, type BushSlashMessage } from '#lib'; +import { GuildTextBasedChannel, User } from 'discord.js'; export default class BlacklistCommand extends BushCommand { public constructor() { @@ -52,10 +51,10 @@ export default class BlacklistCommand extends BushCommand { public override async exec( message: BushMessage | BushSlashMessage, - args: { action: 'blacklist' | 'unblacklist'; target: GuildTextBasedChannels | User | string; global: boolean } + args: { action?: 'blacklist' | 'unblacklist'; target: GuildTextBasedChannel | User | string; global: boolean } ) { let action: 'blacklist' | 'unblacklist' | 'toggle' = - args.action ?? (message?.util?.parsed?.alias as 'blacklist' | 'unblacklist') ?? 'toggle'; + args.action ?? (message?.util?.parsed?.alias as 'blacklist' | 'unblacklist' | undefined) ?? 'toggle'; const global = args.global && message.author.isOwner(); const target = typeof args.target === 'string' @@ -64,65 +63,43 @@ export default class BlacklistCommand extends BushCommand { if (!target) return await message.util.reply(`${util.emojis.error} Choose a valid channel or user.`); const targetID = target.id; - if (global) { - if ((action as 'blacklist' | 'unblacklist' | 'toggle') === 'toggle') { - const globalDB = - (await Global.findByPk(client.config.environment)) ?? (await Global.create({ environment: client.config.environment })); - const blacklistedUsers = globalDB.blacklistedUsers; - const blacklistedChannels = globalDB.blacklistedChannels; - action = blacklistedUsers.includes(targetID) || blacklistedChannels.includes(targetID) ? 'unblacklist' : 'blacklist'; - } - const success = await util - .insertOrRemoveFromGlobal( - action === 'blacklist' ? 'add' : 'remove', - target instanceof User ? 'blacklistedUsers' : 'blacklistedChannels', - targetID - ) - .catch(() => false); - if (!success) - return await message.util.reply({ - content: `${util.emojis.error} There was an error globally ${action}ing ${util.format.input( - target instanceof User ? target.tag : target.name - )}.`, - allowedMentions: AllowedMentions.none() - }); - else - return await message.util.reply({ - content: `${util.emojis.success} Successfully ${action}ed ${util.format.input( - target instanceof User ? target.tag : target.name - )} globally.`, - allowedMentions: AllowedMentions.none() - }); - // guild disable - } else { - if (!message.guild) return await message.util.reply(`${util.emojis.error} You have to be in a guild to disable commands.`); - const blacklistedChannels = (await message.guild.getSetting('blacklistedChannels')) ?? []; - const blacklistedUsers = (await message.guild.getSetting('blacklistedUsers')) ?? []; - if ((action as 'blacklist' | 'unblacklist' | 'toggle') === 'toggle') { - action = blacklistedChannels.includes(targetID) ?? blacklistedUsers.includes(targetID) ? 'unblacklist' : 'blacklist'; - } - const newValue = util.addOrRemoveFromArray( - action === 'blacklist' ? 'add' : 'remove', - target instanceof User ? blacklistedUsers : blacklistedChannels, - targetID - ); - const success = await message.guild - .setSetting(target instanceof User ? 'blacklistedUsers' : 'blacklistedChannels', newValue, message.member!) - .catch(() => false); - if (!success) - return await message.util.reply({ - content: `${util.emojis.error} There was an error ${action}ing ${util.format.input( - target instanceof User ? target.tag : target.name - )}.`, - allowedMentions: AllowedMentions.none() - }); - else - return await message.util.reply({ - content: `${util.emojis.success} Successfully ${action}ed ${util.format.input( - target instanceof User ? target.tag : target.name - )}.`, - allowedMentions: AllowedMentions.none() - }); + if (!message.guild && global) + return await message.util.reply(`${util.emojis.error} You have to be in a guild to disable commands.`); + const blacklistedUsers = global + ? util.getGlobal('blacklistedUsers') + : (await message.guild!.getSetting('blacklistedChannels')) ?? []; + const blacklistedChannels = global + ? util.getGlobal('blacklistedChannels') + : (await message.guild!.getSetting('blacklistedUsers')) ?? []; + if (action === 'toggle') { + action = blacklistedUsers.includes(targetID) || blacklistedChannels.includes(targetID) ? 'unblacklist' : 'blacklist'; } + const newValue = util.addOrRemoveFromArray( + action === 'blacklist' ? 'add' : 'remove', + target instanceof User ? blacklistedUsers : blacklistedChannels, + targetID + ); + + const key = target instanceof User ? 'blacklistedUsers' : 'blacklistedChannels'; + + const success = await (global + ? util.setGlobal(key, newValue) + : message.guild!.setSetting(key, newValue, message.member!) + ).catch(() => false); + + if (!success) + return await message.util.reply({ + content: `${util.emojis.error} There was an error${global ? ' globally' : ''} ${action}ing ${util.format.input( + target instanceof User ? target.tag : target.name + )}.`, + allowedMentions: AllowedMentions.none() + }); + else + return await message.util.reply({ + content: `${util.emojis.success} Successfully ${action}ed ${util.format.input( + target instanceof User ? target.tag : target.name + )}${global ? ' globally' : ''}.`, + allowedMentions: AllowedMentions.none() + }); } } diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts index 6af5895..b88147d 100644 --- a/src/commands/config/config.ts +++ b/src/commands/config/config.ts @@ -97,7 +97,7 @@ export default class SettingsCommand extends BushCommand { }); } - override *args(message: BushMessage): Generator<ArgumentOptions | Flag> { + public override *args(message: BushMessage): Generator<ArgumentOptions | Flag> { const optional = message.util.parsed!.alias === 'settings'; const setting = yield { id: 'setting', diff --git a/src/commands/config/disable.ts b/src/commands/config/disable.ts index a30652a..44c28d3 100644 --- a/src/commands/config/disable.ts +++ b/src/commands/config/disable.ts @@ -1,6 +1,8 @@ -import { AllowedMentions, BushCommand, Global, type BushMessage, type BushSlashMessage } from '#lib'; +import { AllowedMentions, BushCommand, type BushMessage, type BushSlashMessage } from '#lib'; export default class DisableCommand extends BushCommand { + private static blacklistedCommands = ['eval', 'disable']; + public constructor() { super('disable', { aliases: ['disable', 'enable'], @@ -45,20 +47,23 @@ export default class DisableCommand extends BushCommand { }); } - blacklistedCommands = ['eval', 'disable']; - public override async exec( message: BushMessage | BushSlashMessage, - args: { action: 'enable' | 'disable'; command: BushCommand | string; global: boolean } + args: { action?: 'enable' | 'disable'; command: BushCommand | string; global: boolean } ) { let action = (args.action ?? message?.util?.parsed?.alias ?? 'toggle') as 'disable' | 'enable' | 'toggle'; const global = args.global && message.author.isOwner(); - const commandID = (args.command as BushCommand).id; + const commandID = + args.command instanceof BushCommand + ? args.command.id + : (await util.arg.cast(util.arg.union('commandAlias', 'command'), message, args.command))?.id; + + if (!commandID) return await message.util.reply(`${util.emojis.error} Invalid command.`); + + if (DisableCommand.blacklistedCommands.includes(commandID)) + return message.util.send(`${util.emojis.error} the ${commandID} command cannot be disabled.`); - const disabledCommands = global - ? ((await Global.findByPk(client.config.environment)) ?? (await Global.create({ environment: client.config.environment }))) - .disabledCommands - : await message.guild!.getSetting('disabledCommands'); + const disabledCommands = global ? util.getGlobal('disabledCommands') : await message.guild!.getSetting('disabledCommands'); if (action === 'toggle') action = disabledCommands.includes(commandID) ? 'disable' : 'enable'; const newValue = util.addOrRemoveFromArray(action === 'disable' ? 'remove' : 'add', disabledCommands, commandID); diff --git a/src/commands/config/log.ts b/src/commands/config/log.ts index 6121ad7..52cb8f5 100644 --- a/src/commands/config/log.ts +++ b/src/commands/config/log.ts @@ -22,11 +22,12 @@ export default class LogCommand extends BushCommand { }, { id: 'channel', - description: 'The channel to have logs of the seleted type to be sent in.', + description: 'The channel to have logs of the selected type to be sent in.', type: 'channel', prompt: 'What channel would you like these logs to be sent in?', slashType: 'CHANNEL', - channelTypes: ['GUILD_TEXT', 'GUILD_NEWS', 'GUILD_NEWS_THREAD', 'GUILD_PUBLIC_THREAD', 'GUILD_PRIVATE_THREAD'] + channelTypes: ['GUILD_TEXT', 'GUILD_NEWS', 'GUILD_NEWS_THREAD', 'GUILD_PUBLIC_THREAD', 'GUILD_PRIVATE_THREAD'], + only: 'slash' } ], channel: 'guild', @@ -35,7 +36,7 @@ export default class LogCommand extends BushCommand { }); } - override *args(): IterableIterator<ArgumentOptions | Flag> { + public override *args(): IterableIterator<ArgumentOptions | Flag> { const log_type = yield { id: 'log_type', type: guildLogsArr, diff --git a/src/commands/info/snowflake.ts b/src/commands/info/snowflake.ts index bd0924f..f2ffaa8 100644 --- a/src/commands/info/snowflake.ts +++ b/src/commands/info/snowflake.ts @@ -4,7 +4,6 @@ import { SnowflakeUtil, VoiceChannel, type CategoryChannel, - type Channel, type DeconstructedSnowflake, type DMChannel, type Guild, @@ -46,9 +45,9 @@ export default class SnowflakeCommand extends BushCommand { // Channel if (client.channels.cache.has(snowflake)) { - const channel: Channel = client.channels.cache.get(snowflake)!; + const channel = client.channels.cache.get(snowflake)!; const channelInfo = [`**Type:** ${channel.type}`]; - if ((['DM', 'GROUP_DM'] as const).includes(channel.type)) { + if (['DM', 'GROUP_DM'].includes(channel.type)) { const _channel = channel as DMChannel; channelInfo.push(`**Recipient:** ${util.discord.escapeMarkdown(_channel.recipient.tag)} (${_channel.recipient.id})`); snowflakeEmbed.setTitle( diff --git a/src/commands/info/userInfo.ts b/src/commands/info/userInfo.ts index 2d7fcfb..c62be93 100644 --- a/src/commands/info/userInfo.ts +++ b/src/commands/info/userInfo.ts @@ -1,4 +1,11 @@ -import { BushCommand, type BushMessage, type BushSlashMessage, type BushUser } from '#lib'; +import { + BushCommand, + BushGuild, + BushGuildMember, + type BushMessage, + type BushSlashMessage, + type BushUser +} from '#lib'; import { MessageEmbed, type Snowflake } from 'discord.js'; // TODO: Add bot information @@ -37,11 +44,17 @@ export default class UserInfoCommand extends BushCommand { : await client.users.fetch(`${args.user}`).catch(() => undefined); if (user === undefined) return message.util.reply(`${util.emojis.error} Invalid user.`); const member = message.guild ? message.guild.members.cache.get(user.id) : undefined; + await user.fetch(true); // gets banner info and accent color + + const userEmbed = await UserInfoCommand.makeUserInfoEmbed(user, member, message.guild); + + return await message.util.reply({ embeds: [userEmbed] }); + } + + public static async makeUserInfoEmbed(user: BushUser, member?: BushGuildMember, guild?: BushGuild | null) { const emojis = []; const superUsers = client.cache.global.superUsers; - await user.fetch(true); // gets banner info and accent color - const userEmbed: MessageEmbed = new MessageEmbed() .setTitle(util.discord.escapeMarkdown(user.tag)) .setThumbnail(user.displayAvatarURL({ size: 2048, format: 'png', dynamic: true })) @@ -69,7 +82,7 @@ export default class UserInfoCommand extends BushCommand { emojis.push(client.consts.mappings.otherEmojis.NITRO); } - if (message.guild?.ownerId == user.id) emojis.push(client.consts.mappings.otherEmojis.OWNER); + if (guild?.ownerId == user.id) emojis.push(client.consts.mappings.otherEmojis.OWNER); else if (member?.permissions.has('ADMINISTRATOR')) emojis.push(client.consts.mappings.otherEmojis.ADMIN); if (member?.premiumSinceTimestamp) emojis.push(client.consts.mappings.otherEmojis.BOOSTER); @@ -92,16 +105,14 @@ export default class UserInfoCommand extends BushCommand { // Server User Info const serverUserInfo = []; if (joinedAt) - serverUserInfo.push( - `**${message.guild!.ownerId == user.id ? 'Created Server' : 'Joined'}:** ${joinedAt} (${joinedAtDelta} ago)` - ); + serverUserInfo.push(`**${guild!.ownerId == user.id ? 'Created Server' : 'Joined'}:** ${joinedAt} (${joinedAtDelta} ago)`); if (premiumSince) serverUserInfo.push(`**Boosting Since:** ${premiumSince} (${premiumSinceDelta} ago)`); if (member?.displayHexColor) serverUserInfo.push(`**Display Color:** ${member.displayHexColor}`); - if (user.id == '322862723090219008' && message.guild?.id == client.consts.mappings.guilds.bush) + if (user.id == '322862723090219008' && guild?.id == client.consts.mappings.guilds.bush) serverUserInfo.push(`**General Deletions:** 1⅓`); if ( (['384620942577369088', '496409778822709251'] as const).includes(user.id) && - message.guild?.id == client.consts.mappings.guilds.bush + guild?.id == client.consts.mappings.guilds.bush ) serverUserInfo.push(`**General Deletions:** ⅓`); if (member?.nickname) serverUserInfo.push(`**Nickname:** ${util.discord.escapeMarkdown(member?.nickname)}`); @@ -154,7 +165,7 @@ export default class UserInfoCommand extends BushCommand { // Important Perms const perms = []; - if (member?.permissions.has('ADMINISTRATOR') || message.guild?.ownerId == user.id) { + if (member?.permissions.has('ADMINISTRATOR') || guild?.ownerId == user.id) { perms.push('`Administrator`'); } else if (member?.permissions.toArray(false).length) { member.permissions.toArray(false).forEach((permission) => { @@ -166,7 +177,6 @@ export default class UserInfoCommand extends BushCommand { if (perms.length) userEmbed.addField('» Important Perms', perms.join(' ')); if (emojis) userEmbed.setDescription(`\u200B${emojis.join(' ')}`); // zero width space - - return await message.util.reply({ embeds: [userEmbed] }); + return userEmbed } } diff --git a/src/commands/moderation/role.ts b/src/commands/moderation/role.ts index 275db38..689ef1e 100644 --- a/src/commands/moderation/role.ts +++ b/src/commands/moderation/role.ts @@ -56,7 +56,7 @@ export default class RoleCommand extends BushCommand { }); } - override *args(message: BushMessage): Generator<ArgumentOptions | Flag> { + public override *args(message: BushMessage): Generator<ArgumentOptions | Flag> { const action = (['rr'] as const).includes(message.util.parsed?.alias ?? '') ? 'remove' : (['ar', 'ra'] as const).includes(message.util.parsed?.alias ?? '') diff --git a/src/commands/utilities/suicide.ts b/src/commands/utilities/suicide.ts index 05f8d47..641f7ec 100644 --- a/src/commands/utilities/suicide.ts +++ b/src/commands/utilities/suicide.ts @@ -1,5 +1,5 @@ import { AllowedMentions, BushCommand, type BushMessage, type BushSlashMessage } from '#lib'; -import { MessageEmbed } from 'discord.js'; +import { Message, MessageEmbed } from 'discord.js'; export default class TemplateCommand extends BushCommand { public constructor() { @@ -46,8 +46,8 @@ export default class TemplateCommand extends BushCommand { return ( // If the original message was a reply -> imitate it - !message.util.isSlashMessage(message) && message.reference?.messageId && message.guild && message.channel - ? await message.channel.messages.fetch(message.reference!.messageId!).then(async (message1) => { + !message.util.isSlashMessage(message) && (message as Message).reference?.messageId && message.guild && message.channel + ? await message.channel.messages.fetch((message as Message).reference!.messageId!).then(async (message1) => { await message1.reply({ embeds: [suicideEmbed], allowedMentions: AllowedMentions.users(), diff --git a/src/context-menu-commands/user/userInfo.ts b/src/context-menu-commands/user/userInfo.ts index 2ab265a..1e35093 100644 --- a/src/context-menu-commands/user/userInfo.ts +++ b/src/context-menu-commands/user/userInfo.ts @@ -1 +1,25 @@ -// todo: make context interaction for user command +import { BushGuild, BushGuildMember, BushUser } from '#lib'; +import { ContextMenuCommand } from 'discord-akairo'; +import { type ContextMenuInteraction } from 'discord.js'; +import UserInfoCommand from '../../commands/info/userInfo.js'; + +export default class UserInfoContextMenuCommand extends ContextMenuCommand { + public constructor() { + super('userInfo', { + name: 'User Info', + type: 'USER', + category: 'user' + }); + } + + public override async exec(interaction: ContextMenuInteraction) { + await interaction.deferReply({ ephemeral: true }); + + const user = (await interaction.user.fetch()) as BushUser; + const member = interaction.member as BushGuildMember; + const guild = interaction.guild as BushGuild; + const userEmbed = await UserInfoCommand.makeUserInfoEmbed(user, member, guild); + + return await interaction.editReply({ embeds: [userEmbed] }); + } +} diff --git a/src/inhibitors/blacklist/userGlobalBlacklist.ts b/src/inhibitors/blacklist/userGlobalBlacklist.ts index 0f8cea7..ad906f8 100644 --- a/src/inhibitors/blacklist/userGlobalBlacklist.ts +++ b/src/inhibitors/blacklist/userGlobalBlacklist.ts @@ -12,8 +12,7 @@ export default class UserGlobalBlacklistInhibitor extends BushInhibitor { public override exec(message: BushMessage | BushSlashMessage): boolean { if (!message.author) return false; - if (client.isOwner(message.author) || client.isSuperUser(message.author) || client.user!.id === message.author.id) - return false; + if (client.isOwner(message.author) || client.user!.id === message.author.id) return false; if (client.cache.global.blacklistedUsers.includes(message.author.id)) { void client.console.verbose( 'userGlobalBlacklist', 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<ActivePunishment | null> { + /** + * Creates a punishment entry. + * @param options Options for creating the punishment entry. + * @returns The database entry, or null if no entry is created. + */ + public static async createPunishmentEntry(options: CreatePunishmentEntryOptions): Promise<ActivePunishment | null> { const expires = options.duration ? new Date(+new Date() + options.duration ?? 0) : undefined; const user = (await util.resolveNonCachedUser(options.user))!.id; const guild = client.guilds.resolveId(options.guild)!; @@ -135,12 +135,12 @@ export class Moderation { }); } - public static async removePunishmentEntry(options: { - type: 'mute' | 'ban' | 'role' | 'block'; - user: BushGuildMemberResolvable; - guild: BushGuildResolvable; - extraInfo?: Snowflake; - }): Promise<boolean> { + /** + * Destroys a punishment entry. + * @param options Options for destroying the punishment entry. + * @returns Whether or not the entry was destroyed. + */ + public static async removePunishmentEntry(options: RemovePunishmentEntryOptions): Promise<boolean> { const user = await util.resolveNonCachedUser(options.user); const guild = client.guilds.resolveId(options.guild); const type = this.findTypeEnum(options.type); @@ -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<T extends ATC>(type: T, message: Message | BushSlashMessage, phrase: string): Promise<ATCR<T>>; public static async cast<T extends KBAT>(type: T, message: Message | BushSlashMessage, phrase: string): Promise<BAT[T]>; - public static async cast<T extends AT | ATC>(type: T, message: Message | BushSlashMessage, phrase: string): Promise<any>; + public static async cast(type: AT | ATC, message: Message | BushSlashMessage, phrase: string): Promise<any>; public static async cast(type: ATC | AT, message: Message | BushSlashMessage, phrase: string): Promise<any> { - 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<T extends ATC>(...types: T[]): ATCATCR<T>; public static compose<T extends KBAT>(...types: T[]): ATCBAT<T>; - public static compose<T extends AT | ATC>(...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<T extends ATC>(...types: T[]): ATCATCR<T>; public static composeWithFailure<T extends KBAT>(...types: T[]): ATCBAT<T>; - public static composeWithFailure<T extends AT | ATC>(...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<T extends ATC>(...types: T[]): ATCATCR<T>; public static product<T extends KBAT>(...types: T[]): ATCBAT<T>; - public static product<T extends AT | ATC>(...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<T extends ATC>(type: T, min: number, max: number, inclusive?: boolean): ATCATCR<T>; public static range<T extends KBAT>(type: T, min: number, max: number, inclusive?: boolean): ATCBAT<T>; - public static range<T extends AT | ATC>(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<T extends ATC>(type: T, tag?: any): ATCATCR<T>; public static tagged<T extends KBAT>(type: T, tag?: any): ATCBAT<T>; - public static tagged<T extends AT | ATC>(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<T extends ATC>(...types: T[]): ATCATCR<T>; public static taggedUnion<T extends KBAT>(...types: T[]): ATCBAT<T>; - public static taggedUnion<T extends AT | ATC>(...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<T extends ATC>(type: T, tag?: any): ATCATCR<T>; public static taggedWithInput<T extends KBAT>(type: T, tag?: any): ATCBAT<T>; - public static taggedWithInput<T extends AT | ATC>(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<T extends ATC>(...types: T[]): ATCATCR<T>; public static union<T extends KBAT>(...types: T[]): ATCBAT<T>; - public static union<T extends AT | ATC>(...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<T extends ATC>(type: T, predicate: ParsedValuePredicate): ATCATCR<T>; public static validate<T extends KBAT>(type: T, predicate: ParsedValuePredicate): ATCBAT<T>; - public static validate<T extends AT | ATC>(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<T extends ATC>(type: T): ATC<ATCR<T>>; public static withInput<T extends KBAT>(type: T): ATCBAT<T>; - public static withInput<T extends AT | ATC>(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> = R extends BushArgumentTypeCaster<infer S> ? S : R; +type BushArgumentTypeCasterReturn<R> = R extends BushArgumentTypeCaster<infer S> ? S : R; /** ```ts - * <R = unknown> = ArgumentTypeCaster<R> + * <R = unknown> = BushArgumentTypeCaster<R> * ``` */ type ATC<R = unknown> = BushArgumentTypeCaster<R>; /** ```ts - * keyof BaseArgumentType + * keyof BaseBushArgumentType * ``` */ type KBAT = keyof BaseBushArgumentType; /** ```ts - * <R> = ArgumentTypeCasterReturn<R> + * <R> = BushArgumentTypeCasterReturn<R> * ``` */ -type ATCR<R> = ArgumentTypeCasterReturn<R>; +type ATCR<R> = BushArgumentTypeCasterReturn<R>; /** ```ts - * keyof BaseBushArgumentType | string + * BushArgumentType * ``` */ -type AT = BushArgumentTypeCaster | keyof BaseBushArgumentType | string; +type AT = BushArgumentType; /** ```ts - * BaseArgumentType + * BaseBushArgumentType * ``` */ type BAT = BaseBushArgumentType; /** ```ts - * <T extends ArgumentTypeCaster> = ArgumentTypeCaster<ArgumentTypeCasterReturn<T>> + * <T extends BushArgumentTypeCaster> = BushArgumentTypeCaster<BushArgumentTypeCasterReturn<T>> * ``` */ -type ATCATCR<T extends BushArgumentTypeCaster> = BushArgumentTypeCaster<ArgumentTypeCasterReturn<T>>; +type ATCATCR<T extends BushArgumentTypeCaster> = BushArgumentTypeCaster<BushArgumentTypeCasterReturn<T>>; /** ```ts - * <T extends keyof BaseArgumentType> = ArgumentTypeCaster<BaseArgumentType[T]> + * <T extends keyof BaseBushArgumentType> = BushArgumentTypeCaster<BaseBushArgumentType[T]> * ``` */ type ATCBAT<T extends keyof BaseBushArgumentType> = BushArgumentTypeCaster<BaseBushArgumentType[T]>; 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<BushTextBasedChannels, PartialDMChannel | BushDMChannel | DMChannel>; +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<BushAnyChannel, { bitrate: number }>; +export type BushGuildBasedChannel = Extract<BushAnyChannel, { guild: BushGuild }>; +export type BushNonThreadGuildBasedChannel = Exclude<BushGuildBasedChannel, BushThreadChannel>; +export type BushGuildTextBasedChannel = Extract<BushGuildBasedChannel, BushTextBasedChannel>; +export type BushTextChannelResolvable = Snowflake | BushTextChannel; +export type BushGuildVoiceChannelResolvable = BushVoiceBasedChannel | Snowflake; + export interface BushFetchedThreads { threads: Collection<Snowflake, BushThreadChannel>; hasMore?: boolean; @@ -118,29 +137,86 @@ type If<T extends boolean, A, B = null> = 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<Ready extends boolean = boolean> extends AkairoClient<Ready> { public declare channels: BushChannelManager; public declare readonly emojis: BushBaseGuildEmojiManager; public declare guilds: BushGuildManager; public declare user: If<Ready, BushClientUser>; 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<Ready extends boolean = boolean> extends AkairoClient<Re this.token = config.token as If<Ready, string, string | null>; 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<Ready extends boolean = boolean> extends AkairoClient<Re useSlashPermissions: true, aliasReplacement: /-/g }); - this.contextMenuCommandHandler = new ContextMenuCommandHandler(this, { directory: path.join(__dirname, '..', '..', '..', 'context-menu-commands'), automateCategories: true }); - this.util = new BushClientUtil(this); this.db = new Sequelize({ database: this.config.isDevelopment ? 'bushbot-dev' : this.config.isBeta ? 'bushbot-beta' : 'bushbot', @@ -234,15 +301,24 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re }); } - get console(): typeof BushLogger { + /** + * A custom logging system for the bot. + */ + public get console(): typeof BushLogger { return this.logger; } - get consts(): typeof BushConstants { + /** + * Constants for the bot. + */ + public get consts(): typeof BushConstants { return this.constants; } - public static init(): void { + /** + * Extends discord.js structures before the client is instantiated. + */ + public static extendStructures(): void { Structures.extend('GuildEmoji', () => BushGuildEmoji); Structures.extend('DMChannel', () => BushDMChannel); Structures.extend('TextChannel', () => BushTextChannel); @@ -265,18 +341,28 @@ export class BushClient<Ready extends boolean = boolean> extends AkairoClient<Re Structures.extend('SelectMenuInteraction', () => 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 <<v17.x.x>>, 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<Ready extends boolean = boolean> extends AkairoClient<Re void this.logger.success('startup', `Successfully connected to <<Sentry>>.`, 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 <<cache>>.`, 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<Ready extends boolean = boolean> extends AkairoClient<Re * Starts the bot */ public async start() { - if (!process.version.startsWith('v17.')) { - void (await this.console.error('version', `Please use node <<v17.x.x>>, 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<Ready extends boolean = boolean> extends AkairoClient<Re global.util = this.util; try { - await this.#init(); + await this.dbPreInit(); + await UpdateCacheTask.init(this); + void this.console.success('startup', `Successfully created <<cache>>.`, 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<Ready extends boolean = boolean> extends AkairoClient<Re public override isOwner(user: BushUserResolvable): boolean { return this.config.owners.includes(this.users.resolveId(user!)!); } + public override isSuperUser(user: BushUserResolvable): boolean { const userID = this.users.resolveId(user)!; return !!client.cache?.global?.superUsers?.includes(userID) || this.config.owners.includes(userID); } } -export interface BushClient extends PatchedElements { +export interface BushClient extends EventEmitter, PatchedElements { on<K extends keyof BushClientEvents>(event: K, listener: (...args: BushClientEvents[K]) => Awaitable<void>): this; on<S extends string | symbol>(event: Exclude<S, keyof BushClientEvents>, listener: (...args: any[]) => Awaitable<void>): this; @@ -411,3 +500,15 @@ export interface BushClient extends PatchedElements { removeAllListeners<K extends keyof BushClientEvents>(event?: K): this; removeAllListeners<S extends string | symbol>(event?: Exclude<S, keyof BushClientEvents>): 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<K extends keyof GlobalCache>(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<BushUser | undefined> { - 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<Pronoun | undefined> { 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, any>): 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> | any; -export interface BaseBushCommandOptions extends Omit<CommandOptions, 'userPermissions' | 'clientPermissions' | 'args'> { +interface ExtendedCommandOptions { /** * Whether the command is hidden from the help command. */ @@ -191,11 +191,6 @@ export interface BaseBushCommandOptions extends Omit<CommandOptions, 'userPermis restrictedGuilds?: Snowflake[]; /** - * The description of the command. - */ - description: string; - - /** * Show how to use the command. */ usage: string[]; @@ -206,13 +201,6 @@ export interface BaseBushCommandOptions extends Omit<CommandOptions, 'userPermis examples: string[]; /** - * The arguments for the command. - */ - args?: BushArgumentOptions[] & CustomBushArgumentOptions[]; - - category: string; - - /** * A fake command, completely hidden from the help command. */ pseudo?: boolean; @@ -223,6 +211,27 @@ export interface BaseBushCommandOptions extends Omit<CommandOptions, 'userPermis bypassChannelBlacklist?: boolean; /** + * Use instead of {@link BaseBushCommandOptions.args} when using argument generators or custom slashOptions + */ + helpArgs?: BushArgumentOptions[]; +} + +export interface BaseBushCommandOptions + extends Omit<CommandOptions, 'userPermissions' | 'clientPermissions' | 'args'>, + ExtendedCommandOptions { + /** + * The description of the command. + */ + description: string; + + /** + * The arguments for the command. + */ + args?: BushArgumentOptions[] & CustomBushArgumentOptions[]; + + category: string; + + /** * Permissions required by the client to run this command. */ clientPermissions: PermissionResolvable | PermissionResolvable[] | BushMissingPermissionSupplier; @@ -241,11 +250,6 @@ export interface BaseBushCommandOptions extends Omit<CommandOptions, 'userPermis * Restrict this argument to super users. */ superUserOnly?: boolean; - - /** - * Use instead of {@link BaseBushCommandOptions.args} when using argument generators or custom slashOptions - */ - helpArgs?: BushArgumentOptions[]; } export type BushCommandOptions = Omit<BaseBushCommandOptions, 'helpArgs'> | Omit<BaseBushCommandOptions, 'args'>; @@ -329,90 +333,67 @@ export class BushCommand extends Command { }); } - const newOptions: CommandOptions = {}; - if ('aliases' in options_) newOptions.aliases = options_.aliases; - if ('args' in options_ && typeof options_.args === 'object') { - const newTextArgs: ArgumentOptions[] = []; - const newSlashArgs: SlashOption[] = []; - for (const arg of options_.args) { - if (arg.only !== 'slash' && !options_.slashOnly) { - const newArg: ArgumentOptions = {}; - if ('default' in arg) newArg.default = arg.default; - if ('description' in arg) newArg.description = arg.description; - if ('flag' in arg) newArg.flag = arg.flag; - if ('id' in arg) newArg.id = arg.id; - if ('index' in arg) newArg.index = arg.index; - if ('limit' in arg) newArg.limit = arg.limit; - if ('match' in arg) newArg.match = arg.match; - if ('modifyOtherwise' in arg) newArg.modifyOtherwise = arg.modifyOtherwise; - if ('multipleFlags' in arg) newArg.multipleFlags = arg.multipleFlags; - if ('otherwise' in arg) newArg.otherwise = arg.otherwise; - if ('prompt' in arg || 'retry' in arg || 'optional' in arg) { - newArg.prompt = {}; - if ('prompt' in arg) newArg.prompt.start = arg.prompt; - if ('retry' in arg) newArg.prompt.retry = arg.retry; - if ('optional' in arg) newArg.prompt.optional = arg.optional; + const newOptions: Partial<CommandOptions & ExtendedCommandOptions> = {}; + for (const _key in options_) { + const key = _key as keyof typeof options_; // you got to love typescript + if (key === 'args' && 'args' in options_ && typeof options_.args === 'object') { + const newTextArgs: ArgumentOptions[] = []; + const newSlashArgs: SlashOption[] = []; + for (const arg of options_.args) { + if (arg.only !== 'slash' && !options_.slashOnly) { + const newArg: ArgumentOptions = {}; + if ('default' in arg) newArg.default = arg.default; + if ('description' in arg) newArg.description = arg.description; + if ('flag' in arg) newArg.flag = arg.flag; + if ('id' in arg) newArg.id = arg.id; + if ('index' in arg) newArg.index = arg.index; + if ('limit' in arg) newArg.limit = arg.limit; + if ('match' in arg) newArg.match = arg.match; + if ('modifyOtherwise' in arg) newArg.modifyOtherwise = arg.modifyOtherwise; + if ('multipleFlags' in arg) newArg.multipleFlags = arg.multipleFlags; + if ('otherwise' in arg) newArg.otherwise = arg.otherwise; + if ('prompt' in arg || 'retry' in arg || 'optional' in arg) { + newArg.prompt = {}; + if ('prompt' in arg) newArg.prompt.start = arg.prompt; + if ('retry' in arg) newArg.prompt.retry = arg.retry; + if ('optional' in arg) newArg.prompt.optional = arg.optional; + } + if ('type' in arg) newArg.type = arg.type as ArgumentType | ArgumentTypeCaster; + if ('unordered' in arg) newArg.unordered = arg.unordered; + newTextArgs.push(newArg); + } + if ( + arg.only !== 'text' && + !('slashOptions' in options_) && + (options_.slash || options_.slashOnly) && + arg.slashType !== false + ) { + const newArg: { + [key in SlashOptionKeys]?: any; + } = { + name: arg.id, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + description: arg.prompt || arg.description || 'No description provided.', + type: arg.slashType + }; + if ('slashResolve' in arg) newArg.resolve = arg.slashResolve; + if ('autocomplete' in arg) newArg.autocomplete = arg.autocomplete; + if ('channelTypes' in arg) newArg.channelTypes = arg.channelTypes; + if ('choices' in arg) newArg.choices = arg.choices; + if ('minValue' in arg) newArg.minValue = arg.minValue; + if ('maxValue' in arg) newArg.maxValue = arg.maxValue; + newArg.required = 'optional' in arg ? !arg.optional : true; + newSlashArgs.push(newArg as SlashOption); } - if ('type' in arg) newArg.type = arg.type as ArgumentType | ArgumentTypeCaster; - if ('unordered' in arg) newArg.unordered = arg.unordered; - newTextArgs.push(newArg); - } - if ( - arg.only !== 'text' && - !('slashOptions' in options_) && - (options_.slash || options_.slashOnly) && - arg.slashType !== false - ) { - const newArg: { - [key in SlashOptionKeys]?: any; - } = { - name: arg.id, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - description: arg.prompt || arg.description || 'No description provided.', - type: arg.slashType - }; - if ('slashResolve' in arg) newArg.resolve = arg.slashResolve; - if ('autocomplete' in arg) newArg.autocomplete = arg.autocomplete; - if ('channelTypes' in arg) newArg.channelTypes = arg.channelTypes; - if ('choices' in arg) newArg.choices = arg.choices; - if ('minValue' in arg) newArg.minValue = arg.minValue; - if ('maxValue' in arg) newArg.maxValue = arg.maxValue; - newArg.required = 'optional' in arg ? !arg.optional : true; - newSlashArgs.push(newArg as SlashOption); } + if (newTextArgs.length > 0) newOptions.args = newTextArgs; + if (newSlashArgs.length > 0) newOptions.slashOptions = options_.slashOptions ?? newSlashArgs; + } else if (key === 'clientPermissions' || key === 'userPermissions') { + newOptions[key] = options_[key] as PermissionResolvable | PermissionResolvable[] | MissingPermissionSupplier; + } else { + newOptions[key] = options_[key]; } - if (newTextArgs.length > 0) newOptions.args = newTextArgs; - if (newSlashArgs.length > 0) newOptions.slashOptions = options_.slashOptions ?? newSlashArgs; } - type perm = PermissionResolvable | PermissionResolvable[] | MissingPermissionSupplier; - - if ('argumentDefaults' in options_) newOptions.argumentDefaults = options_.argumentDefaults; - if ('before' in options_) newOptions.before = options_.before; - if ('channel' in options_) newOptions.channel = options_.channel; - if ('clientPermissions' in options_) newOptions.clientPermissions = options_.clientPermissions as perm; - if ('condition' in options_) newOptions.condition = options_.condition; - if ('cooldown' in options_) newOptions.cooldown = options_.cooldown; - if ('description' in options_) newOptions.description = options_.description; - if ('editable' in options_) newOptions.editable = options_.editable; - if ('flags' in options_) newOptions.flags = options_.flags; - if ('ignoreCooldown' in options_) newOptions.ignoreCooldown = options_.ignoreCooldown; - if ('ignorePermissions' in options_) newOptions.ignorePermissions = options_.ignorePermissions; - if ('lock' in options_) newOptions.lock = options_.lock; - if ('onlyNsfw' in options_) newOptions.onlyNsfw = options_.onlyNsfw; - if ('optionFlags' in options_) newOptions.optionFlags = options_.optionFlags; - if ('ownerOnly' in options_) newOptions.ownerOnly = options_.ownerOnly; - if ('prefix' in options_) newOptions.prefix = options_.prefix; - if ('quoted' in options_) newOptions.quoted = options_.quoted; - if ('ratelimit' in options_) newOptions.ratelimit = options_.ratelimit; - if ('regex' in options_) newOptions.regex = options_.regex; - if ('separator' in options_) newOptions.separator = options_.separator; - if ('slash' in options_) newOptions.slash = options_.slash; - if ('slashEphemeral' in options_) newOptions.slashEphemeral = options_.slashEphemeral; - if ('slashGuilds' in options_) newOptions.slashGuilds = options_.slashGuilds; - if ('slashOptions' in options_) newOptions.slashOptions = options_.slashOptions; - if ('superUserOnly' in options_) newOptions.superUserOnly = options_.superUserOnly; - if ('typing' in options_) newOptions.typing = options_.typing; - if ('userPermissions' in options_) newOptions.userPermissions = options_.userPermissions as perm; super(id, newOptions); @@ -447,9 +428,14 @@ export class BushCommand extends Command { } } -export interface BushCommand { - exec(message: BushMessage, args: any): any; - exec(message: BushMessage | BushSlashMessage, args: any): any; +export interface BushCommand extends Command { + /** + * Executes the command. + * @param message - Message that triggered the command. + * @param args - Evaluated arguments. + */ + exec<R, A>(message: BushMessage, args: A): R; + exec<R, A>(message: BushMessage | BushSlashMessage, args: A): R; } type SlashOptionKeys = diff --git a/src/lib/extensions/discord-akairo/BushInhibitor.ts b/src/lib/extensions/discord-akairo/BushInhibitor.ts index c3f9994..7f13594 100644 --- a/src/lib/extensions/discord-akairo/BushInhibitor.ts +++ b/src/lib/extensions/discord-akairo/BushInhibitor.ts @@ -6,6 +6,13 @@ export class BushInhibitor extends Inhibitor { } export interface BushInhibitor { + /** + * 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. + * @param message - Message being handled. + * @param command - Command to check. + */ exec(message: BushMessage, command: BushCommand): any; exec(message: BushMessage | BushSlashMessage, command: BushCommand): any; } diff --git a/src/lib/extensions/discord-akairo/BushSlashMessage.ts b/src/lib/extensions/discord-akairo/BushSlashMessage.ts index c0a4a60..448963b 100644 --- a/src/lib/extensions/discord-akairo/BushSlashMessage.ts +++ b/src/lib/extensions/discord-akairo/BushSlashMessage.ts @@ -12,6 +12,6 @@ export class BushSlashMessage extends AkairoMessage { } } -export interface BushSlashMessage { +export interface BushSlashMessage extends AkairoMessage { get guild(): BushGuild | null; } diff --git a/src/lib/extensions/discord.js/BushActivity.ts b/src/lib/extensions/discord.js/BushActivity.ts index e438918..3f19232 100644 --- a/src/lib/extensions/discord.js/BushActivity.ts +++ b/src/lib/extensions/discord.js/BushActivity.ts @@ -2,6 +2,9 @@ import type { BushEmoji, BushPresence } from '#lib'; import { Activity } from 'discord.js'; import type { RawActivityData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents an activity that is part of a user's presence. + */ export class BushActivity extends Activity { public declare emoji: BushEmoji | null; diff --git a/src/lib/extensions/discord.js/BushApplicationCommand.ts b/src/lib/extensions/discord.js/BushApplicationCommand.ts index 35d598d..8298830 100644 --- a/src/lib/extensions/discord.js/BushApplicationCommand.ts +++ b/src/lib/extensions/discord.js/BushApplicationCommand.ts @@ -3,6 +3,9 @@ import type { BushClient, BushGuild } from '#lib'; import { ApplicationCommand, type Snowflake } from 'discord.js'; import type { RawApplicationCommandData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents an application command. + */ export class BushApplicationCommand<PermissionsFetchType = {}> extends ApplicationCommand<PermissionsFetchType> { public declare readonly client: BushClient; public declare guild: BushGuild | null; diff --git a/src/lib/extensions/discord.js/BushApplicationCommandManager.d.ts b/src/lib/extensions/discord.js/BushApplicationCommandManager.d.ts index 7947acd..2aa366d 100644 --- a/src/lib/extensions/discord.js/BushApplicationCommandManager.d.ts +++ b/src/lib/extensions/discord.js/BushApplicationCommandManager.d.ts @@ -14,12 +14,19 @@ import { type Snowflake } from 'discord.js'; +/** + * Manages API methods for application commands and stores their cache. + */ export class BushApplicationCommandManager< ApplicationCommandScope = BushApplicationCommand<{ guild: BushGuildResolvable }>, PermissionsOptionsExtras = { guild: BushGuildResolvable }, PermissionsGuildType = null > extends CachedManager<Snowflake, ApplicationCommandScope, BushApplicationCommandResolvable> { public constructor(client: BushClient, iterable?: Iterable<unknown>); + + /** + * The manager for permissions of arbitrary commands on arbitrary guilds + */ public permissions: BushApplicationCommandPermissionsManager< { command?: BushApplicationCommandResolvable } & PermissionsOptionsExtras, { command: BushApplicationCommandResolvable } & PermissionsOptionsExtras, @@ -27,22 +34,112 @@ export class BushApplicationCommandManager< PermissionsGuildType, null >; + + /** + * The APIRouter path to the commands + * @param id The application command's id + * @param guildId The guild's id to use in the path, + * ignored when using a {@link GuildApplicationCommandManager} + */ private commandPath({ id, guildId }: { id?: Snowflake; guildId?: Snowflake }): unknown; - public create(command: ApplicationCommandData): Promise<ApplicationCommandScope>; - public create(command: ApplicationCommandData, guildId: Snowflake): Promise<BushApplicationCommand>; + + /** + * Creates an application command. + * @param command The command + * @param guildId The guild's id to create this command in, ignored when using a {@link GuildApplicationCommandManager} + * @example + * // Create a new command + * client.application.commands.create({ + * name: 'test', + * description: 'A test command', + * }) + * .then(console.log) + * .catch(console.error); + */ + public create(command: BushApplicationCommandResolvable, guildId?: Snowflake): Promise<ApplicationCommandScope>; + + /** + * Deletes an application command. + * @param command The command to delete + * @param guildId The guild's id where the command is registered, + * ignored when using a {@link GuildApplicationCommandManager} + * @example + * // Delete a command + * guild.commands.delete('123456789012345678') + * .then(console.log) + * .catch(console.error); + */ public delete(command: BushApplicationCommandResolvable, guildId?: Snowflake): Promise<ApplicationCommandScope | null>; + + /** + * Edits an application command. + * @param command The command to edit + * @param data The data to update the command with + * @param guildId The guild's id where the command registered, + * ignored when using a {@link GuildApplicationCommandManager} + * @example + * // Edit an existing command + * client.application.commands.edit('123456789012345678', { + * description: 'New description', + * }) + * .then(console.log) + * .catch(console.error); + */ public edit(command: BushApplicationCommandResolvable, data: ApplicationCommandData): Promise<ApplicationCommandScope>; public edit( command: BushApplicationCommandResolvable, data: ApplicationCommandData, guildId: Snowflake ): Promise<BushApplicationCommand>; + + /** + * Obtains one or multiple application commands from Discord, or the cache if it's already available. + * @param id The application command's id + * @param options Additional options for this fetch + * @example + * // Fetch a single command + * client.application.commands.fetch('123456789012345678') + * .then(command => console.log(`Fetched command ${command.name}`)) + * .catch(console.error); + * @example + * // Fetch all commands + * guild.commands.fetch() + * .then(commands => console.log(`Fetched ${commands.size} commands`)) + * .catch(console.error); + */ public fetch(id: Snowflake, options: FetchApplicationCommandOptions & { guildId: Snowflake }): Promise<BushApplicationCommand>; public fetch(options: FetchApplicationCommandOptions): Promise<Collection<string, ApplicationCommandScope>>; public fetch(id: Snowflake, options?: FetchApplicationCommandOptions): Promise<ApplicationCommandScope>; public fetch(id?: Snowflake, options?: FetchApplicationCommandOptions): Promise<Collection<Snowflake, ApplicationCommandScope>>; + + /** + * Sets all the commands for this application or guild. + * @param commands The commands + * @param guildId The guild's id to create the commands in, + * ignored when using a {@link GuildApplicationCommandManager} + * @example + * // Set all commands to just this one + * client.application.commands.set([ + * { + * name: 'test', + * description: 'A test command', + * }, + * ]) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove all commands + * guild.commands.set([]) + * .then(console.log) + * .catch(console.error); + */ public set(commands: ApplicationCommandData[]): Promise<Collection<Snowflake, ApplicationCommandScope>>; public set(commands: ApplicationCommandData[], guildId: Snowflake): Promise<Collection<Snowflake, BushApplicationCommand>>; + + /** + * Transforms an {@link ApplicationCommandData} object into something that can be used with the API. + * @param command The command to transform + */ private static transformCommand( command: ApplicationCommandData ): Omit<APIApplicationCommand, 'id' | 'application_id' | 'guild_id'>; diff --git a/src/lib/extensions/discord.js/BushApplicationCommandPermissionsManager.d.ts b/src/lib/extensions/discord.js/BushApplicationCommandPermissionsManager.d.ts index 5f2e4da..ff32be4 100644 --- a/src/lib/extensions/discord.js/BushApplicationCommandPermissionsManager.d.ts +++ b/src/lib/extensions/discord.js/BushApplicationCommandPermissionsManager.d.ts @@ -13,6 +13,9 @@ import { } from 'discord.js'; import type { ApplicationCommandPermissionTypes } from 'discord.js/typings/enums'; +/** + * Manages API methods for permissions of Application Commands. + */ export class BushApplicationCommandPermissionsManager< BaseOptions, FetchSingleOptions, @@ -21,18 +24,95 @@ export class BushApplicationCommandPermissionsManager< CommandIdType > extends BaseManager { public constructor(manager: ApplicationCommandManager | GuildApplicationCommandManager | ApplicationCommand); + + /** + * The manager or command that this manager belongs to + */ private manager: ApplicationCommandManager | GuildApplicationCommandManager | ApplicationCommand; + /** + * The client that instantiated this Manager + */ public client: BushClient; + + /** + * The id of the command this manager acts on + */ public commandId: CommandIdType; + + /** + * The guild that this manager acts on + */ public guild: GuildType; + + /** + * The id of the guild that this manager acts on + */ public guildId: Snowflake | null; + + /** + * Add permissions to a command. + * @param options Options used to add permissions + * @example + * // Block a role from the command permissions + * guild.commands.permissions.add({ command: '123456789012345678', permissions: [ + * { + * id: '876543211234567890', + * type: 'ROLE', + * permission: false + * }, + * ]}) + * .then(console.log) + * .catch(console.error); + */ public add( options: FetchSingleOptions & { permissions: ApplicationCommandPermissionData[] } ): Promise<ApplicationCommandPermissions[]>; + + /** + * Check whether a permission exists for a user or role + * @param options Options used to check permissions + * @example + * // Check whether a user has permission to use a command + * guild.commands.permissions.has({ command: '123456789012345678', permissionId: '876543210123456789' }) + * .then(console.log) + * .catch(console.error); + */ public has(options: FetchSingleOptions & { permissionId: BushUserResolvable | BushRoleResolvable }): Promise<boolean>; + + /** + * Fetches the permissions for one or multiple commands. + * @param options Options used to fetch permissions + * @example + * // Fetch permissions for one command + * guild.commands.permissions.fetch({ command: '123456789012345678' }) + * .then(perms => console.log(`Fetched permissions for ${perms.length} users`)) + * .catch(console.error); + * @example + * // Fetch permissions for all commands in a guild + * client.application.commands.permissions.fetch({ guild: '123456789012345678' }) + * .then(perms => console.log(`Fetched permissions for ${perms.size} commands`)) + * .catch(console.error); + */ public fetch(options: FetchSingleOptions): Promise<ApplicationCommandPermissions[]>; public fetch(options: BaseOptions): Promise<Collection<Snowflake, ApplicationCommandPermissions[]>>; + + /** + * Remove permissions from a command. + * @param options Options used to remove permissions + * @example + * // Remove a user permission from this command + * guild.commands.permissions.remove({ command: '123456789012345678', users: '876543210123456789' }) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove multiple roles from this command + * guild.commands.permissions.remove({ + * command: '123456789012345678', roles: ['876543210123456789', '765432101234567890'] + * }) + * .then(console.log) + * .catch(console.error); + */ public remove( options: | (FetchSingleOptions & { @@ -44,6 +124,37 @@ export class BushApplicationCommandPermissionsManager< roles: BushRoleResolvable | BushRoleResolvable[]; }) ): Promise<ApplicationCommandPermissions[]>; + + /** + * Sets the permissions for one or more commands. + * @param options Options used to set permissions + * @example + * // Set the permissions for one command + * client.application.commands.permissions.set({ guild: '892455839386304532', command: '123456789012345678', + * permissions: [ + * { + * id: '876543210987654321', + * type: 'USER', + * permission: false, + * }, + * ]}) + * .then(console.log) + * .catch(console.error); + * @example + * // Set the permissions for all commands + * guild.commands.permissions.set({ fullPermissions: [ + * { + * id: '123456789012345678', + * permissions: [{ + * id: '876543210987654321', + * type: 'USER', + * permission: false, + * }], + * }, + * ]}) + * .then(console.log) + * .catch(console.error); + */ public set( options: FetchSingleOptions & { permissions: ApplicationCommandPermissionData[] } ): Promise<ApplicationCommandPermissions[]>; @@ -52,7 +163,19 @@ export class BushApplicationCommandPermissionsManager< fullPermissions: GuildApplicationCommandPermissionData[]; } ): Promise<Collection<Snowflake, ApplicationCommandPermissions[]>>; + + /** + * The APIRouter path to the commands + * @param guildId The guild's id to use in the path, + * @param commandId The application command's id + */ private permissionsPath(guildId: Snowflake, commandId?: Snowflake): unknown; + + /** + * Transforms an {@link ApplicationCommandPermissionData} object into something that can be used with the API. + * @param permissions The permissions to transform + * @param received Whether these permissions have been received from Discord + */ private static transformPermissions( permissions: ApplicationCommandPermissionData, received: true diff --git a/src/lib/extensions/discord.js/BushBaseGuildEmojiManager.d.ts b/src/lib/extensions/discord.js/BushBaseGuildEmojiManager.d.ts index 7c913a9..347ff65 100644 --- a/src/lib/extensions/discord.js/BushBaseGuildEmojiManager.d.ts +++ b/src/lib/extensions/discord.js/BushBaseGuildEmojiManager.d.ts @@ -2,7 +2,15 @@ import type { BushClient, BushEmojiIdentifierResolvable, BushEmojiResolvable, Bu import { CachedManager, type Snowflake } from 'discord.js'; import { type RawGuildEmojiData } from 'discord.js/typings/rawDataTypes'; +/** + * Holds methods to resolve GuildEmojis and stores their cache. + */ export class BushBaseGuildEmojiManager extends CachedManager<Snowflake, BushGuildEmoji, BushEmojiResolvable> { public constructor(client: BushClient, iterable?: Iterable<RawGuildEmojiData>); + + /** + * Resolves an EmojiResolvable to an emoji identifier. + * @param emoji The emoji resolvable to resolve + */ public resolveIdentifier(emoji: BushEmojiIdentifierResolvable): string | null; } diff --git a/src/lib/extensions/discord.js/BushBaseGuildTextChannel.ts b/src/lib/extensions/discord.js/BushBaseGuildTextChannel.ts index 742ea76..9b260dc 100644 --- a/src/lib/extensions/discord.js/BushBaseGuildTextChannel.ts +++ b/src/lib/extensions/discord.js/BushBaseGuildTextChannel.ts @@ -8,6 +8,9 @@ import { } from 'discord.js'; import type { RawGuildChannelData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a text-based guild channel on Discord. + */ export class BushBaseGuildTextChannel extends BaseGuildTextChannel { public declare messages: BushMessageManager; public declare threads: BushThreadManager<AllowedThreadTypeForTextChannel | AllowedThreadTypeForNewsChannel>; diff --git a/src/lib/extensions/discord.js/BushBaseGuildVoiceChannel.d.ts b/src/lib/extensions/discord.js/BushBaseGuildVoiceChannel.d.ts new file mode 100644 index 0000000..21be206 --- /dev/null +++ b/src/lib/extensions/discord.js/BushBaseGuildVoiceChannel.d.ts @@ -0,0 +1,13 @@ +import { BaseGuildVoiceChannel, Collection, Snowflake } from 'discord.js'; +import { BushCategoryChannel } from './BushCategoryChannel'; +import { BushGuild } from './BushGuild'; +import { BushGuildMember } from './BushGuildMember'; + +/** + * Represents a voice-based guild channel on Discord. + */ +export class BushBaseGuildVoiceChannel extends BaseGuildVoiceChannel { + public readonly members: Collection<Snowflake, BushGuildMember>; + public guild: BushGuild; + public readonly parent: BushCategoryChannel | null; +} diff --git a/src/lib/extensions/discord.js/BushButtonInteraction.ts b/src/lib/extensions/discord.js/BushButtonInteraction.ts index 18d6b35..2bc1c25 100644 --- a/src/lib/extensions/discord.js/BushButtonInteraction.ts +++ b/src/lib/extensions/discord.js/BushButtonInteraction.ts @@ -1,15 +1,18 @@ -import type { BushClient, BushGuild, BushGuildMember, BushGuildTextBasedChannel, BushTextBasedChannels, BushUser } from '#lib'; +import type { BushClient, BushGuild, BushGuildMember, BushGuildTextBasedChannel, BushTextBasedChannel, BushUser } from '#lib'; import type { APIInteractionGuildMember } from 'discord-api-types/v9'; import { ButtonInteraction, type CacheType, type CacheTypeReducer } from 'discord.js'; import type { RawMessageButtonInteractionData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a button interaction. + */ export class BushButtonInteraction<Cached extends CacheType = CacheType> extends ButtonInteraction<Cached> { public declare readonly channel: CacheTypeReducer< Cached, BushGuildTextBasedChannel | null, BushGuildTextBasedChannel | null, BushGuildTextBasedChannel | null, - BushTextBasedChannels | null + BushTextBasedChannel | null >; public declare readonly guild: CacheTypeReducer<Cached, BushGuild, null>; public declare member: CacheTypeReducer<Cached, BushGuildMember, APIInteractionGuildMember>; diff --git a/src/lib/extensions/discord.js/BushCategoryChannel.ts b/src/lib/extensions/discord.js/BushCategoryChannel.ts index c30a761..b711a54 100644 --- a/src/lib/extensions/discord.js/BushCategoryChannel.ts +++ b/src/lib/extensions/discord.js/BushCategoryChannel.ts @@ -1,10 +1,13 @@ -import { type BushClient, type BushGuild, type BushGuildChannel, type BushGuildMember } from '#lib'; +import { BushNonThreadGuildBasedChannel, type BushClient, type BushGuild, type BushGuildMember } from '#lib'; import { CategoryChannel, type Collection, type Snowflake } from 'discord.js'; import { type RawGuildChannelData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a guild category channel on Discord. + */ export class BushCategoryChannel extends CategoryChannel { public declare readonly client: BushClient; - public declare readonly children: Collection<Snowflake, BushGuildChannel>; + public declare readonly children: Collection<Snowflake, Exclude<BushNonThreadGuildBasedChannel, BushCategoryChannel>>; public declare guild: BushGuild; public declare readonly members: Collection<Snowflake, BushGuildMember>; public declare readonly parent: CategoryChannel | null; diff --git a/src/lib/extensions/discord.js/BushChannel.d.ts b/src/lib/extensions/discord.js/BushChannel.d.ts index 9a78b9a..42443ba 100644 --- a/src/lib/extensions/discord.js/BushChannel.d.ts +++ b/src/lib/extensions/discord.js/BushChannel.d.ts @@ -1,8 +1,12 @@ -import type { BushClient, BushStageChannel, BushTextBasedChannels, BushThreadChannel, BushVoiceChannel } from '#lib'; +import type { BushClient, BushTextBasedChannel, BushThreadChannel } from '#lib'; import { Channel, type ChannelMention, type Snowflake } from 'discord.js'; import type { ChannelTypes } from 'discord.js/typings/enums'; import type { RawChannelData } from 'discord.js/typings/rawDataTypes'; +import { BushBaseGuildVoiceChannel } from './BushBaseGuildVoiceChannel'; +/** + * Represents any channel on Discord. + */ export class BushChannel extends Channel { public constructor(client: BushClient, data?: RawChannelData, immediatePatch?: boolean); public readonly createdAt: Date; @@ -11,10 +15,10 @@ export class BushChannel extends Channel { public id: Snowflake; public readonly partial: false; public type: keyof typeof ChannelTypes; - public delete(): Promise<BushChannel>; - public fetch(force?: boolean): Promise<BushChannel>; - public isText(): this is BushTextBasedChannels; - public isVoice(): this is BushVoiceChannel | BushStageChannel; + public delete(): Promise<this>; + public fetch(force?: boolean): Promise<this>; + public isText(): this is BushTextBasedChannel; + public isVoice(): this is BushBaseGuildVoiceChannel; public isThread(): this is BushThreadChannel; public toString(): ChannelMention; } diff --git a/src/lib/extensions/discord.js/BushChannelManager.d.ts b/src/lib/extensions/discord.js/BushChannelManager.d.ts index 843b956..514cdd3 100644 --- a/src/lib/extensions/discord.js/BushChannelManager.d.ts +++ b/src/lib/extensions/discord.js/BushChannelManager.d.ts @@ -1,8 +1,22 @@ -import type { BushChannel, BushChannelResolvable } from '#lib'; +import type { BushAnyChannel, BushChannelResolvable } from '#lib'; import { CachedManager, type Client, type FetchChannelOptions, type Snowflake } from 'discord.js'; import type { RawChannelData } from 'discord.js/typings/rawDataTypes'; -export class BushChannelManager extends CachedManager<Snowflake, BushChannel, BushChannelResolvable> { +/** + * A manager of channels belonging to a client + */ +export class BushChannelManager extends CachedManager<Snowflake, BushAnyChannel, BushChannelResolvable> { public constructor(client: Client, iterable: Iterable<RawChannelData>); - public fetch(id: Snowflake, options?: FetchChannelOptions): Promise<BushChannel | null>; + + /** + * Obtains a channel from Discord, or the channel cache if it's already available. + * @param id The channel's id + * @param options Additional options for this fetch + * @example + * // Fetch a channel by its id + * client.channels.fetch('222109930545610754') + * .then(channel => console.log(channel.name)) + * .catch(console.error); + */ + public fetch(id: Snowflake, options?: FetchChannelOptions): Promise<BushAnyChannel | null>; } diff --git a/src/lib/extensions/discord.js/BushClientEvents.d.ts b/src/lib/extensions/discord.js/BushClientEvents.d.ts index 91096bd..10d70f9 100644 --- a/src/lib/extensions/discord.js/BushClientEvents.d.ts +++ b/src/lib/extensions/discord.js/BushClientEvents.d.ts @@ -4,16 +4,16 @@ import type { BushDMChannel, BushGuild, BushGuildBan, - BushGuildChannel, BushGuildEmoji, BushGuildMember, BushMessage, BushMessageReaction, BushNewsChannel, + BushNonThreadGuildBasedChannel, BushPresence, BushRole, BushStageInstance, - BushTextBasedChannels, + BushTextBasedChannel, BushTextChannel, BushThreadChannel, BushThreadMember, @@ -29,6 +29,7 @@ import type { import type { AkairoClientEvents } from 'discord-akairo'; import type { Collection, + GuildScheduledEvent, Interaction, InvalidRequestWarningData, Invite, @@ -45,12 +46,12 @@ export interface BushClientEvents extends AkairoClientEvents { oldCommand: BushApplicationCommand | null, newCommand: BushApplicationCommand ]; - channelCreate: [channel: BushGuildChannel]; - channelDelete: [channel: BushDMChannel | BushGuildChannel]; - channelPinsUpdate: [channel: BushTextBasedChannels, date: Date]; + channelCreate: [channel: BushNonThreadGuildBasedChannel]; + channelDelete: [channel: BushDMChannel | BushNonThreadGuildBasedChannel]; + channelPinsUpdate: [channel: BushTextBasedChannel, date: Date]; channelUpdate: [ - oldChannel: BushDMChannel | BushGuildChannel, - newChannel: BushDMChannel | BushGuildChannel + oldChannel: BushDMChannel | BushNonThreadGuildBasedChannel, + newChannel: BushDMChannel | BushNonThreadGuildBasedChannel ]; debug: [message: string]; warn: [message: string]; @@ -145,6 +146,20 @@ export interface BushClientEvents extends AkairoClientEvents { stickerCreate: [sticker: Sticker]; stickerDelete: [sticker: Sticker]; stickerUpdate: [oldSticker: Sticker, newSticker: Sticker]; + guildScheduledEventCreate: [guildScheduledEvent: GuildScheduledEvent]; + guildScheduledEventUpdate: [ + oldGuildScheduledEvent: GuildScheduledEvent, + newGuildScheduledEvent: GuildScheduledEvent + ]; + guildScheduledEventDelete: [guildScheduledEvent: GuildScheduledEvent]; + guildScheduledEventUserAdd: [ + guildScheduledEvent: GuildScheduledEvent, + user: BushUser + ]; + guildScheduledEventUserRemove: [ + guildScheduledEvent: GuildScheduledEvent, + user: BushUser + ]; /* Custom */ bushBan: [ victim: BushGuildMember | BushUser, diff --git a/src/lib/extensions/discord.js/BushClientUser.d.ts b/src/lib/extensions/discord.js/BushClientUser.d.ts index c307553..503413b 100644 --- a/src/lib/extensions/discord.js/BushClientUser.d.ts +++ b/src/lib/extensions/discord.js/BushClientUser.d.ts @@ -3,22 +3,96 @@ import type { Base64Resolvable, BufferResolvable, ClientPresence, + ClientUser, ClientUserEditData, PresenceData, PresenceStatusData } from 'discord.js'; import { BushUser } from './BushUser'; -export class BushClientUser extends BushUser { +/** + * Represents the logged in client's Discord user. + */ +export class BushClientUser extends BushUser implements ClientUser { + /** + * If the bot's {@link ClientApplication.owner Owner} has MFA enabled on their account + */ public mfaEnabled: boolean; + + /** + * Represents the client user's presence + */ public readonly presence: ClientPresence; + + /** + * Whether or not this account has been verified + */ public verified: boolean; + + /** + * Edits the logged in client. + * @param data The new data + */ public edit(data: ClientUserEditData): Promise<this>; + + /** + * Sets the activity the client user is playing. + * @param name Activity being played, or options for setting the activity + * @param options Options for setting the activity + * @example + * // Set the client user's activity + * client.user.setActivity('discord.js', { type: 'WATCHING' }); + */ public setActivity(options?: ActivityOptions): ClientPresence; public setActivity(name: string, options?: ActivityOptions): ClientPresence; + + /** + * Sets/removes the AFK flag for the client user. + * @param afk Whether or not the user is AFK + * @param shardId Shard Id(s) to have the AFK flag set on + */ public setAFK(afk: boolean, shardId?: number | number[]): ClientPresence; - public setAvatar(avatar: BufferResolvable | Base64Resolvable): Promise<this>; + + /** + * Sets the avatar of the logged in client. + * @param avatar The new avatar + * @example + * // Set avatar + * client.user.setAvatar('./avatar.png') + * .then(user => console.log(`New avatar set!`)) + * .catch(console.error); + */ + public setAvatar(avatar: BufferResolvable | Base64Resolvable | null): Promise<this>; + + /** + * Sets the full presence of the client user. + * @param data Data for the presence + * @example + * // Set the client user's presence + * client.user.setPresence({ activities: [{ name: 'with discord.js' }], status: 'idle' }); + */ public setPresence(data: PresenceData): ClientPresence; + + /** + * Sets the status of the client user. + * @param status Status to change to + * @param shardId Shard id(s) to have the activity set on + * @example + * // Set the client user's status + * client.user.setStatus('idle'); + */ public setStatus(status: PresenceStatusData, shardId?: number | number[]): ClientPresence; + + /** + * Sets the username of the logged in client. + * <info>Changing usernames in Discord is heavily rate limited, with only 2 requests + * every hour. Use this sparingly!</info> + * @param username The new username + * @example + * // Set username + * client.user.setUsername('discordjs') + * .then(user => console.log(`My new username is ${user.username}`)) + * .catch(console.error); + */ public setUsername(username: string): Promise<this>; } diff --git a/src/lib/extensions/discord.js/BushCommandInteraction.ts b/src/lib/extensions/discord.js/BushCommandInteraction.ts index 174fd9c..e6643f0 100644 --- a/src/lib/extensions/discord.js/BushCommandInteraction.ts +++ b/src/lib/extensions/discord.js/BushCommandInteraction.ts @@ -1,19 +1,26 @@ -import type { - BushApplicationCommand, - BushGuild, - BushGuildChannel, - BushGuildEmoji, - BushGuildMember, - BushRole, - BushUser -} from '#lib'; +import type { BushApplicationCommand, BushGuild, BushGuildEmoji, BushGuildMember, BushRole, BushUser } from '#lib'; import type { APIInteractionGuildMember } from 'discord-api-types/v9'; import { CommandInteraction, type CacheType, type CacheTypeReducer, type Invite, type Snowflake } from 'discord.js'; import type { RawCommandInteractionData } from 'discord.js/typings/rawDataTypes'; -import { BushGuildTextBasedChannel, BushTextBasedChannels, type BushClient } from '../discord-akairo/BushClient'; +import { + BushGuildTextBasedChannel, + BushNonThreadGuildBasedChannel, + BushTextBasedChannel, + type BushClient +} from '../discord-akairo/BushClient'; -export type BushGuildResolvable = BushGuild | BushGuildChannel | BushGuildMember | BushGuildEmoji | Invite | BushRole | Snowflake; +export type BushGuildResolvable = + | BushGuild + | BushNonThreadGuildBasedChannel + | BushGuildMember + | BushGuildEmoji + | Invite + | BushRole + | Snowflake; +/** + * Represents a command interaction. + */ export class BushCommandInteraction<Cached extends CacheType = CacheType> extends CommandInteraction<Cached> { public declare readonly client: BushClient; public declare readonly command: BushApplicationCommand | BushApplicationCommand<{ guild: BushGuildResolvable }> | null; @@ -22,7 +29,7 @@ export class BushCommandInteraction<Cached extends CacheType = CacheType> extend BushGuildTextBasedChannel | null, BushGuildTextBasedChannel | null, BushGuildTextBasedChannel | null, - BushTextBasedChannels | null + BushTextBasedChannel | null >; public declare readonly guild: CacheTypeReducer<Cached, BushGuild, null>; public declare member: CacheTypeReducer<Cached, BushGuildMember, APIInteractionGuildMember>; diff --git a/src/lib/extensions/discord.js/BushDMChannel.ts b/src/lib/extensions/discord.js/BushDMChannel.ts index ca08d7e..9df9275 100644 --- a/src/lib/extensions/discord.js/BushDMChannel.ts +++ b/src/lib/extensions/discord.js/BushDMChannel.ts @@ -2,6 +2,9 @@ import type { BushClient, BushMessageManager, BushUser } from '#lib'; import { DMChannel } from 'discord.js'; import type { RawDMChannelData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a direct message channel between two users. + */ export class BushDMChannel extends DMChannel { public declare readonly client: BushClient; public declare messages: BushMessageManager; diff --git a/src/lib/extensions/discord.js/BushEmoji.ts b/src/lib/extensions/discord.js/BushEmoji.ts index 5cdf5ac..9e42e5d 100644 --- a/src/lib/extensions/discord.js/BushEmoji.ts +++ b/src/lib/extensions/discord.js/BushEmoji.ts @@ -2,6 +2,9 @@ import type { BushClient } from '#lib'; import { Emoji } from 'discord.js'; import type { RawEmojiData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents an emoji, see {@link GuildEmoji} and {@link ReactionEmoji}. + */ export class BushEmoji extends Emoji { public declare readonly client: BushClient; diff --git a/src/lib/extensions/discord.js/BushGuild.ts b/src/lib/extensions/discord.js/BushGuild.ts index 9c272ff..e12053e 100644 --- a/src/lib/extensions/discord.js/BushGuild.ts +++ b/src/lib/extensions/discord.js/BushGuild.ts @@ -16,6 +16,11 @@ import { Moderation } from '../../common/Moderation.js'; import { Guild as GuildDB } from '../../models/Guild.js'; import { ModLogType } from '../../models/ModLog.js'; +/** + * Represents a guild (or a server) on Discord. + * <info>It's recommended to see if a guild is available before performing operations or reading data from it. You can + * check this with {@link Guild.available}.</info> + */ export class BushGuild extends Guild { public declare readonly client: BushClient; public declare readonly me: BushGuildMember | null; @@ -97,6 +102,11 @@ export class BushGuild extends Guild { return await row.save(); } + /** + * Get a the log channel configured for a certain log type. + * @param logType The type of log channel to get. + * @returns Either the log channel or undefined if not configured. + */ public async getLogChannel(logType: GuildLogType): Promise<BushTextChannel | undefined> { const channelId = (await this.getSetting('logChannels'))[logType]; if (!channelId) return undefined; @@ -107,14 +117,12 @@ export class BushGuild extends Guild { ); } - public async bushBan(options: { - user: BushUserResolvable | UserResolvable; - reason?: string | null; - moderator?: BushUserResolvable; - duration?: number; - deleteDays?: number; - evidence?: string; - }): Promise<'success' | 'missing permissions' | 'error banning' | 'error creating modlog entry' | 'error creating ban entry'> { + /** + * Bans a user, dms them, creates a mod log entry, and creates a punishment entry. + * @param options Options for banning the user. + * @returns A string status message of the ban. + */ + public async bushBan(options: BushBanOptions): Promise<BanResponse> { // checks if (!this.me!.permissions.has('BAN_MEMBERS')) return 'missing permissions'; @@ -165,18 +173,12 @@ export class BushGuild extends Guild { return ret; } - public async bushUnban(options: { - user: BushUserResolvable | BushUser; - reason?: string | null; - moderator?: BushUserResolvable; - }): Promise< - | 'success' - | 'missing permissions' - | 'user not banned' - | 'error unbanning' - | 'error creating modlog entry' - | 'error removing ban entry' - > { + /** + * Unbans a user, dms them, creates a mod log entry, and destroys the punishment entry. + * @param options Options for unbanning the user. + * @returns A status message of the unban. + */ + public async bushUnban(options: BushUnbanOptions): Promise<UnbanResponse> { let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; const user = (await util.resolveNonCachedUser(options.user))!; @@ -260,3 +262,70 @@ export class BushGuild extends Guild { void this.sendLogChannel('error', { embeds: [{ title: title, description: message, color: util.colors.error }] }); } } + +/** + * Options for unbanning a user + */ +interface BushUnbanOptions { + /** + * The user to unban + */ + user: BushUserResolvable | BushUser; + + /** + * The reason for unbanning the user + */ + reason?: string | null; + + /** + * The moderator who unbanned the user + */ + moderator?: BushUserResolvable; +} + +/** + * Options for banning a user + */ +interface BushBanOptions { + /** + * The user to ban + */ + user: BushUserResolvable | UserResolvable; + + /** + * The reason to ban the user + */ + reason?: string | null; + + /** + * The moderator who banned the user + */ + moderator?: BushUserResolvable; + + /** + * The duration of the ban + */ + duration?: number; + + /** + * The number of days to delete the user's messages for + */ + deleteDays?: number; + + /** + * The evidence for the ban + */ + evidence?: string; +} + +type PunishmentResponse = 'success' | 'missing permissions' | 'error creating modlog entry'; + +/** + * Response returned when banning a user + */ +type BanResponse = PunishmentResponse | 'error banning' | 'error creating ban entry'; + +/** + * Response returned when unbanning a user + */ +type UnbanResponse = PunishmentResponse | 'user not banned' | 'error unbanning' | 'error removing ban entry'; diff --git a/src/lib/extensions/discord.js/BushGuildApplicationCommandManager.d.ts b/src/lib/extensions/discord.js/BushGuildApplicationCommandManager.d.ts index f8e80ae..4d76b07 100644 --- a/src/lib/extensions/discord.js/BushGuildApplicationCommandManager.d.ts +++ b/src/lib/extensions/discord.js/BushGuildApplicationCommandManager.d.ts @@ -9,15 +9,102 @@ import { import type { ApplicationCommandData, BaseFetchOptions, Collection, Snowflake } from 'discord.js'; import type { RawApplicationCommandData } from 'discord.js/typings/rawDataTypes'; +/** + * An extension for guild-specific application commands. + */ export class BushGuildApplicationCommandManager extends BushApplicationCommandManager<BushApplicationCommand, {}, BushGuild> { public constructor(guild: BushGuild, iterable?: Iterable<RawApplicationCommandData>); public declare readonly client: BushClient; + + /** + * The guild that this manager belongs to + */ public guild: BushGuild; - public create(command: ApplicationCommandData): Promise<BushApplicationCommand>; + + /** + * Creates an application command. + * @param command The command + * @param guildId The guild's id to create this command in, + * ignored when using a {@link GuildApplicationCommandManager} + * @example + * // Create a new command + * client.application.commands.create({ + * name: 'test', + * description: 'A test command', + * }) + * .then(console.log) + * .catch(console.error); + */ + public create(command: BushApplicationCommandResolvable): Promise<BushApplicationCommand>; + + /** + * Deletes an application command. + * @param command The command to delete + * @param guildId The guild's id where the command is registered, + * ignored when using a {@link GuildApplicationCommandManager} + * @example + * // Delete a command + * guild.commands.delete('123456789012345678') + * .then(console.log) + * .catch(console.error); + */ public delete(command: BushApplicationCommandResolvable): Promise<BushApplicationCommand | null>; + + /** + * Edits an application command. + * @param command The command to edit + * @param data The data to update the command with + * @param guildId The guild's id where the command registered, + * ignored when using a {@link GuildApplicationCommandManager} + * @example + * // Edit an existing command + * client.application.commands.edit('123456789012345678', { + * description: 'New description', + * }) + * .then(console.log) + * .catch(console.error); + */ public edit(command: BushApplicationCommandResolvable, data: ApplicationCommandData): Promise<BushApplicationCommand>; + + /** + * Obtains one or multiple application commands from Discord, or the cache if it's already available. + * @param id The application command's id + * @param options Additional options for this fetch + * @example + * // Fetch a single command + * client.application.commands.fetch('123456789012345678') + * .then(command => console.log(`Fetched command ${command.name}`)) + * .catch(console.error); + * @example + * // Fetch all commands + * guild.commands.fetch() + * .then(commands => console.log(`Fetched ${commands.size} commands`)) + * .catch(console.error); + */ public fetch(id: Snowflake, options?: BaseFetchOptions): Promise<BushApplicationCommand>; public fetch(options: BaseFetchOptions): Promise<Collection<Snowflake, BushApplicationCommand>>; public fetch(id?: undefined, options?: BaseFetchOptions): Promise<Collection<Snowflake, BushApplicationCommand>>; + + /** + * Sets all the commands for this application or guild. + * @param commands The commands + * @param guildId The guild's id to create the commands in, + * ignored when using a {@link GuildApplicationCommandManager} + * @example + * // Set all commands to just this one + * client.application.commands.set([ + * { + * name: 'test', + * description: 'A test command', + * }, + * ]) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove all commands + * guild.commands.set([]) + * .then(console.log) + * .catch(console.error); + */ public set(commands: ApplicationCommandData[]): Promise<Collection<Snowflake, BushApplicationCommand>>; } diff --git a/src/lib/extensions/discord.js/BushGuildBan.d.ts b/src/lib/extensions/discord.js/BushGuildBan.d.ts index 4287792..11875f3 100644 --- a/src/lib/extensions/discord.js/BushGuildBan.d.ts +++ b/src/lib/extensions/discord.js/BushGuildBan.d.ts @@ -2,6 +2,9 @@ import type { BushClient, BushGuild, BushUser } from '#lib'; import { GuildBan } from 'discord.js'; import type { RawGuildBanData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a ban in a guild on Discord. + */ export class BushGuildBan extends GuildBan { public constructor(client: BushClient, data: RawGuildBanData, guild: BushGuild); public guild: BushGuild; diff --git a/src/lib/extensions/discord.js/BushGuildChannel.ts b/src/lib/extensions/discord.js/BushGuildChannel.ts index 3324dc8..6880daf 100644 --- a/src/lib/extensions/discord.js/BushGuildChannel.ts +++ b/src/lib/extensions/discord.js/BushGuildChannel.ts @@ -2,6 +2,15 @@ import type { BushClient, BushGuild } from '#lib'; import { GuildChannel } from 'discord.js'; import type { RawGuildChannelData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a guild channel from any of the following: + * - {@link BushTextChannel} + * - {@link BushVoiceChannel} + * - {@link BushCategoryChannel} + * - {@link BushNewsChannel} + * - {@link BushStoreChannel} + * - {@link BushStageChannel} + */ export class BushGuildChannel extends GuildChannel { public declare readonly client: BushClient; public declare guild: BushGuild; diff --git a/src/lib/extensions/discord.js/BushGuildEmoji.ts b/src/lib/extensions/discord.js/BushGuildEmoji.ts index 180f78c..2c9d36b 100644 --- a/src/lib/extensions/discord.js/BushGuildEmoji.ts +++ b/src/lib/extensions/discord.js/BushGuildEmoji.ts @@ -2,6 +2,9 @@ import type { BushClient, BushGuild, BushGuildEmojiRoleManager, BushUser } from import { GuildEmoji } from 'discord.js'; import type { RawGuildEmojiData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a custom emoji. + */ export class BushGuildEmoji extends GuildEmoji { public declare readonly client: BushClient; public declare guild: BushGuild; diff --git a/src/lib/extensions/discord.js/BushGuildEmojiRoleManager.d.ts b/src/lib/extensions/discord.js/BushGuildEmojiRoleManager.d.ts index 3aae4f0..9253cad 100644 --- a/src/lib/extensions/discord.js/BushGuildEmojiRoleManager.d.ts +++ b/src/lib/extensions/discord.js/BushGuildEmojiRoleManager.d.ts @@ -1,15 +1,51 @@ import type { BushClient, BushGuild, BushGuildEmoji, BushRole, BushRoleResolvable } from '#lib'; import { DataManager, type Collection, type Snowflake } from 'discord.js'; +/** + * Manages API methods for roles belonging to emojis and stores their cache. + */ export class BushGuildEmojiRoleManager extends DataManager<Snowflake, BushRole, BushRoleResolvable> { public constructor(emoji: BushGuildEmoji); public declare readonly client: BushClient; + + /** + * The emoji belonging to this manager + */ public emoji: BushGuildEmoji; + + /** + * The guild belonging to this manager + */ public guild: BushGuild; + + /** + * Adds a role (or multiple roles) to the list of roles that can use this emoji. + * @param roleOrRoles The role or roles to add + */ public add( roleOrRoles: BushRoleResolvable | readonly BushRoleResolvable[] | Collection<Snowflake, BushRole> ): Promise<BushGuildEmoji>; + + /** + * Sets the role(s) that can use this emoji. + * @param roles The roles or role ids to apply + * @example + * // Set the emoji's roles to a single role + * guildEmoji.roles.set(['391156570408615936']) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove all roles from an emoji + * guildEmoji.roles.set([]) + * .then(console.log) + * .catch(console.error); + */ public set(roles: readonly BushRoleResolvable[] | Collection<Snowflake, BushRole>): Promise<BushGuildEmoji>; + + /** + * Removes a role (or multiple roles) from the list of roles that can use this emoji. + * @param roleOrRoles The role or roles to remove + */ public remove( roleOrRoles: BushRoleResolvable | readonly BushRoleResolvable[] | Collection<Snowflake, BushRole> ): Promise<BushGuildEmoji>; diff --git a/src/lib/extensions/discord.js/BushGuildManager.d.ts b/src/lib/extensions/discord.js/BushGuildManager.d.ts index 4dd0750..95719a3 100644 --- a/src/lib/extensions/discord.js/BushGuildManager.d.ts +++ b/src/lib/extensions/discord.js/BushGuildManager.d.ts @@ -10,9 +10,25 @@ import { } from 'discord.js'; import { type RawGuildData } from 'discord.js/typings/rawDataTypes'; +/** + * Manages API methods for Guilds and stores their cache. + */ export class BushGuildManager extends CachedManager<Snowflake, BushGuild, BushGuildResolvable> { public constructor(client: BushClient, iterable?: Iterable<RawGuildData>); + + /** + * Creates a guild. + * <warn>This is only available to bots in fewer than 10 guilds.</warn> + * @param name The name of the guild + * @param options Options for creating the guild + * @returns The guild that was created + */ public create(name: string, options?: GuildCreateOptions): Promise<BushGuild>; + + /** + * Obtains one or multiple guilds from Discord, or the guild cache if it's already available. + * @param options The guild's id or options + */ public fetch(options: Snowflake | FetchGuildOptions): Promise<BushGuild>; public fetch(options?: FetchGuildsOptions): Promise<Collection<Snowflake, OAuth2Guild>>; } diff --git a/src/lib/extensions/discord.js/BushGuildMember.ts b/src/lib/extensions/discord.js/BushGuildMember.ts index fe9c4c9..f6d5259 100644 --- a/src/lib/extensions/discord.js/BushGuildMember.ts +++ b/src/lib/extensions/discord.js/BushGuildMember.ts @@ -1,79 +1,11 @@ -import { Moderation, ModLogType, type BushClient, type BushGuild, type BushRole, type BushUser } from '#lib'; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { BushClientEvents, Moderation, ModLogType, type BushClient, type BushGuild, type BushRole, type BushUser } from '#lib'; import { GuildMember, MessageEmbed, type Partialize, type Role } from 'discord.js'; import type { RawGuildMemberData } from 'discord.js/typings/rawDataTypes'; -interface BushPunishmentOptions { - reason?: string | null; - moderator?: BushGuildMember; - evidence?: string; -} - -interface BushTimedPunishmentOptions extends BushPunishmentOptions { - duration?: number; -} - -interface AddRoleOptions extends BushTimedPunishmentOptions { - role: BushRole | Role; - addToModlog: boolean; -} - -interface RemoveRoleOptions extends BushTimedPunishmentOptions { - role: BushRole | Role; - addToModlog: boolean; -} - -type PunishmentResponse = 'success' | 'error creating modlog entry' | 'failed to dm'; - -type WarnResponse = PunishmentResponse; - -type AddRoleResponse = - | PunishmentResponse - | 'user hierarchy' - | 'role managed' - | 'client hierarchy' - | 'error creating role entry' - | 'error adding role'; - -type RemoveRoleResponse = - | PunishmentResponse - | 'user hierarchy' - | 'role managed' - | 'client hierarchy' - | 'error removing role entry' - | 'error removing role'; - -type MuteResponse = - | PunishmentResponse - | 'missing permissions' - | 'no mute role' - | 'invalid mute role' - | 'mute role not manageable' - | 'error giving mute role' - | 'error creating mute entry'; - -type UnmuteResponse = - | PunishmentResponse - | 'missing permissions' - | 'no mute role' - | 'invalid mute role' - | 'mute role not manageable' - | 'error removing mute role' - | 'error removing mute entry'; - -type KickResponse = PunishmentResponse | 'missing permissions' | 'error kicking'; - -interface BushBanOptions extends BushTimedPunishmentOptions { - deleteDays?: number; -} - -type BanResponse = PunishmentResponse | 'missing permissions' | 'error creating ban entry' | 'error banning'; - -export type PartialBushGuildMember = Partialize< - BushGuildMember, - 'joinedAt' | 'joinedTimestamp', - 'warn' | 'addRole' | 'removeRole' | 'mute' | 'unmute' | 'bushKick' | 'bushBan' | 'isOwner' | 'isSuperUser' ->; - +/** + * Represents a member of a guild on Discord. + */ export class BushGuildMember extends GuildMember { public declare readonly client: BushClient; public declare guild: BushGuild; @@ -83,6 +15,14 @@ export class BushGuildMember extends GuildMember { super(client, data, guild); } + /** + * Send a punishment dm to the user. + * @param punishment The punishment that the user has received. + * @param reason The reason the user to be punished. + * @param duration The duration of the punishment. + * @param sendFooter Whether or not to send the guild's punishment footer with the dm. + * @returns Whether or not the dm was sent successfully. + */ public async punishDM(punishment: string, reason?: string | null, duration?: number, sendFooter = true): Promise<boolean> { const ending = await this.guild.getSetting('punishmentEnding'); const dmEmbed = @@ -98,6 +38,12 @@ export class BushGuildMember extends GuildMember { return !!dmSuccess; } + /** + * Warn the user, create a modlog entry, and send a dm to the user. + * @param options Options for warning the user. + * @returns An object with the result of the warning, and the case number of the warn. + * @emits {@link BushClientEvents.bushWarn} + */ public async warn(options: BushPunishmentOptions): Promise<{ result: WarnResponse | null; caseNum: number | null }> { let caseID: string | undefined = undefined; let dmSuccessEvent: boolean | undefined = undefined; @@ -131,6 +77,12 @@ export class BushGuildMember extends GuildMember { return ret as { result: WarnResponse | null; caseNum: number | null }; } + /** + * Add a role to the user, if it is a punishment create a modlog entry, and create a punishment entry if it is temporary or a punishment. + * @param options Options for adding a role to the user. + * @returns A status message for adding the add. + * @emits {@link BushClientEvents.bushPunishRole} + */ public async addRole(options: AddRoleOptions): Promise<AddRoleResponse> { const ifShouldAddRole = this.#checkIfShouldAddRole(options.role, options.moderator); if (ifShouldAddRole !== true) return ifShouldAddRole; @@ -186,6 +138,12 @@ export class BushGuildMember extends GuildMember { return ret; } + /** + * Remove a role from the user, if it is a punishment create a modlog entry, and destroy a punishment entry if it was temporary or a punishment. + * @param options Options for removing a role from the user. + * @returns A status message for removing the role. + * @emits {@link BushClientEvents.bushPunishRoleRemove} + */ public async removeRole(options: RemoveRoleOptions): Promise<RemoveRoleResponse> { const ifShouldAddRole = this.#checkIfShouldAddRole(options.role, options.moderator); if (ifShouldAddRole !== true) return ifShouldAddRole; @@ -237,11 +195,17 @@ export class BushGuildMember extends GuildMember { return ret; } + /** + * Check whether or not a role should be added/removed from the user based on hierarchy. + * @param role The role to check if can be modified. + * @param moderator The moderator that is trying to add/remove the role. + * @returns `true` if the role should be added/removed or a string for the reason why it shouldn't. + */ #checkIfShouldAddRole( role: BushRole | Role, moderator?: BushGuildMember ): true | 'user hierarchy' | 'role managed' | 'client hierarchy' { - if (moderator && moderator.roles.highest.position <= role.position /* && this.guild.ownerId !== this.id */) { + if (moderator && moderator.roles.highest.position <= role.position && this.guild.ownerId !== this.user.id) { client.console.debug(`${this.roles.highest.position} <= ${role.position}`); return 'user hierarchy'; } else if (role.managed) { @@ -252,6 +216,12 @@ export class BushGuildMember extends GuildMember { return true; } + /** + * Mute the user, create a modlog entry, creates a punishment entry, and dms the user. + * @param options Options for muting the user. + * @returns A status message for muting the user. + * @emits {@link BushClientEvents.bushMute} + */ public async mute(options: BushTimedPunishmentOptions): Promise<MuteResponse> { // checks if (!this.guild.me!.permissions.has('MANAGE_ROLES')) return 'missing permissions'; @@ -325,6 +295,12 @@ export class BushGuildMember extends GuildMember { return ret; } + /** + * Unmute the user, create a modlog entry, remove the punishment entry, and dm the user. + * @param options Options for unmuting the user. + * @returns A status message for unmuting the user. + * @emits {@link BushClientEvents.bushUnmute} + */ public async unmute(options: BushPunishmentOptions): Promise<UnmuteResponse> { //checks if (!this.guild.me!.permissions.has('MANAGE_ROLES')) return 'missing permissions'; @@ -393,6 +369,12 @@ export class BushGuildMember extends GuildMember { return ret; } + /** + * Kick the user, create a modlog entry, and dm the user. + * @param options Options for kicking the user. + * @returns A status message for kicking the user. + * @emits {@link BushClientEvents.bushKick} + */ public async bushKick(options: BushPunishmentOptions): Promise<KickResponse> { // checks if (!this.guild.me?.permissions.has('KICK_MEMBERS') || !this.kickable) return 'missing permissions'; @@ -437,6 +419,12 @@ export class BushGuildMember extends GuildMember { return ret; } + /** + * Ban the user, create a modlog entry, create a punishment entry, and dm the user. + * @param options Options for banning the user. + * @returns A status message for banning the user. + * @emits {@link BushClientEvents.bushBan} + */ public async bushBan(options: BushBanOptions): Promise<BanResponse> { // checks if (!this.guild.me!.permissions.has('BAN_MEMBERS') || !this.bannable) return 'missing permissions'; @@ -497,11 +485,160 @@ export class BushGuildMember extends GuildMember { return ret; } - public get isOwner(): boolean { + /** + * Whether or not the user is an owner of the bot. + */ + public isOwner(): boolean { return client.isOwner(this); } - public get isSuperUser(): boolean { + /** + * Whether or not the user is a super user of the bot. + */ + public isSuperUser(): boolean { return client.isSuperUser(this); } } + +/** + * Options for punishing a user. + */ +interface BushPunishmentOptions { + /** + * The reason for the punishment. + */ + reason?: string | null; + + /** + * The moderator who punished the user. + */ + moderator?: BushGuildMember; + + /** + * Evidence for the punishment. + */ + evidence?: string; +} + +/** + * Punishment options for punishments that can be temporary. + */ +interface BushTimedPunishmentOptions extends BushPunishmentOptions { + /** + * The duration of the punishment. + */ + duration?: number; +} + +/** + * Options for a role add punishment. + */ +interface AddRoleOptions extends BushTimedPunishmentOptions { + /** + * The role to add to the user. + */ + role: BushRole | Role; + + /** + * Whether to create a modlog entry for this punishment. + */ + addToModlog: boolean; +} + +/** + * Options for a role remove punishment. + */ +interface RemoveRoleOptions extends BushTimedPunishmentOptions { + /** + * The role to remove from the user. + */ + role: BushRole | Role; + + /** + * Whether to create a modlog entry for this punishment. + */ + addToModlog: boolean; +} + +/** + * Options for banning a user. + */ +interface BushBanOptions extends BushTimedPunishmentOptions { + /** + * The number of days to delete the user's messages for. + */ + deleteDays?: number; +} + +type PunishmentResponse = 'success' | 'error creating modlog entry' | 'failed to dm'; + +/** + * Response returned when warning a user. + */ +type WarnResponse = PunishmentResponse; + +/** + * Response returned when adding a role to a user. + */ +type AddRoleResponse = + | PunishmentResponse + | 'user hierarchy' + | 'role managed' + | 'client hierarchy' + | 'error creating role entry' + | 'error adding role'; + +/** + * Response returned when removing a role from a user. + */ +type RemoveRoleResponse = + | PunishmentResponse + | 'user hierarchy' + | 'role managed' + | 'client hierarchy' + | 'error removing role entry' + | 'error removing role'; + +/** + * Response returned when muting a user. + */ +type MuteResponse = + | PunishmentResponse + | 'missing permissions' + | 'no mute role' + | 'invalid mute role' + | 'mute role not manageable' + | 'error giving mute role' + | 'error creating mute entry'; + +/** + * Response returned when unmuting a user. + */ +type UnmuteResponse = + | PunishmentResponse + | 'missing permissions' + | 'no mute role' + | 'invalid mute role' + | 'mute role not manageable' + | 'error removing mute role' + | 'error removing mute entry'; + +/** + * Response returned when kicking a user. + */ +type KickResponse = PunishmentResponse | 'missing permissions' | 'error kicking'; + +/** + * Response returned when banning a user. + */ +type BanResponse = PunishmentResponse | 'missing permissions' | 'error creating ban entry' | 'error banning'; + +export type PartialBushGuildMember = Partialize< + BushGuildMember, + 'joinedAt' | 'joinedTimestamp', + 'warn' | 'addRole' | 'removeRole' | 'mute' | 'unmute' | 'bushKick' | 'bushBan' | 'isOwner' | 'isSuperUser' +>; + +/** + * @typedef {BushClientEvents} VSCodePleaseDontRemove + */
\ No newline at end of file diff --git a/src/lib/extensions/discord.js/BushGuildMemberManager.d.ts b/src/lib/extensions/discord.js/BushGuildMemberManager.d.ts index 0866fce..a0e65e7 100644 --- a/src/lib/extensions/discord.js/BushGuildMemberManager.d.ts +++ b/src/lib/extensions/discord.js/BushGuildMemberManager.d.ts @@ -14,25 +14,156 @@ import { } from 'discord.js'; import type { RawGuildMemberData } from 'discord.js/typings/rawDataTypes'; +/** + * Manages API methods for GuildMembers and stores their cache. + */ export class BushGuildMemberManager extends CachedManager<Snowflake, BushGuildMember, BushGuildMemberResolvable> { public constructor(guild: BushGuild, iterable?: Iterable<RawGuildMemberData>); public declare readonly client: BushClient; + + /** + * The guild this manager belongs to + */ public guild: BushGuild; + + /** + * Adds a user to the guild using OAuth2. Requires the `CREATE_INSTANT_INVITE` permission. + * @param user The user to add to the guild + * @param options Options for adding the user to the guild + */ public add( user: BushUserResolvable, options: AddGuildMemberOptions & { fetchWhenExisting: false } ): Promise<BushGuildMember | null>; public add(user: BushUserResolvable, options: AddGuildMemberOptions): Promise<BushGuildMember>; + + /** + * Bans a user from the guild. + * @param user The user to ban + * @param options Options for the ban + * @returns Result object will be resolved as specifically as possible. + * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot + * be resolved, the user id will be the result. + * Internally calls the GuildBanManager#create method. + * @example + * // Ban a user by id (or with a user/guild member object) + * guild.members.ban('84484653687267328') + * .then(kickInfo => console.log(`Banned ${kickInfo.user?.tag ?? kickInfo.tag ?? kickInfo}`)) + * .catch(console.error); + */ public ban(user: BushUserResolvable, options?: BanOptions): Promise<BushGuildMember | BushUser | Snowflake>; + + /** + * Edits a member of the guild. + * <info>The user must be a member of the guild</info> + * @param user The member to edit + * @param data The data to edit the member with + * @param reason Reason for editing this user + */ public edit(user: BushUserResolvable, data: GuildMemberEditData, reason?: string): Promise<void>; + + /** + * Fetches member(s) from Discord, even if they're offline. + * @param options If a UserResolvable, the user to fetch. + * If undefined, fetches all members. + * If a query, it limits the results to users with similar usernames. + * @example + * // Fetch all members from a guild + * guild.members.fetch() + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch a single member + * guild.members.fetch('66564597481480192') + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch a single member without checking cache + * guild.members.fetch({ user, force: true }) + * .then(console.log) + * .catch(console.error) + * @example + * // Fetch a single member without caching + * guild.members.fetch({ user, cache: false }) + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch by an array of users including their presences + * guild.members.fetch({ user: ['66564597481480192', '191615925336670208'], withPresences: true }) + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch by query + * guild.members.fetch({ query: 'hydra', limit: 1 }) + * .then(console.log) + * .catch(console.error); + */ public fetch( options: BushUserResolvable | FetchMemberOptions | (FetchMembersOptions & { user: BushUserResolvable }) ): Promise<BushGuildMember>; public fetch(options?: FetchMembersOptions): Promise<Collection<Snowflake, BushGuildMember>>; + + /** + * Kicks a user from the guild. + * <info>The user must be a member of the guild</info> + * @param user The member to kick + * @param reason Reason for kicking + * @returns Result object will be resolved as specifically as possible. + * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot + * be resolved, the user's id will be the result. + * @example + * // Kick a user by id (or with a user/guild member object) + * guild.members.kick('84484653687267328') + * .then(banInfo => console.log(`Kicked ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`)) + * .catch(console.error); + */ public kick(user: BushUserResolvable, reason?: string): Promise<BushGuildMember | BushUser | Snowflake>; + + /** + * Lists up to 1000 members of the guild. + * @param options Options for listing members + */ public list(options?: GuildListMembersOptions): Promise<Collection<Snowflake, BushGuildMember>>; + + /** + * Prunes members from the guild based on how long they have been inactive. + * @param options Options for pruning + * @returns The number of members that were/will be kicked + * @example + * // See how many members will be pruned + * guild.members.prune({ dry: true }) + * .then(pruned => console.log(`This will prune ${pruned} people!`)) + * .catch(console.error); + * @example + * // Actually prune the members + * guild.members.prune({ days: 1, reason: 'too many people!' }) + * .then(pruned => console.log(`I just pruned ${pruned} people!`)) + * .catch(console.error); + * @example + * // Include members with a specified role + * guild.members.prune({ days: 7, roles: ['657259391652855808'] }) + * .then(pruned => console.log(`I just pruned ${pruned} people!`)) + * .catch(console.error); + */ public prune(options: GuildPruneMembersOptions & { dry?: false; count: false }): Promise<null>; public prune(options?: GuildPruneMembersOptions): Promise<number>; + + /** + * Searches for members in the guild based on a query. + * @param options Options for searching members + */ public search(options: GuildSearchMembersOptions): Promise<Collection<Snowflake, BushGuildMember>>; + + /** + * Unbans a user from the guild. Internally calls the {@link GuildBanManager.remove} method. + * @param user The user to unban + * @param reason Reason for unbanning user + * @returns The user that was unbanned + * @example + * // Unban a user by id (or with a user/guild member object) + * guild.members.unban('84484653687267328') + * .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`)) + * .catch(console.error); + */ public unban(user: BushUserResolvable, reason?: string): Promise<BushUser>; } diff --git a/src/lib/extensions/discord.js/BushMessage.ts b/src/lib/extensions/discord.js/BushMessage.ts index 9f6d422..b442196 100644 --- a/src/lib/extensions/discord.js/BushMessage.ts +++ b/src/lib/extensions/discord.js/BushMessage.ts @@ -4,7 +4,7 @@ import type { BushGuild, BushGuildMember, BushGuildTextBasedChannel, - BushTextBasedChannels, + BushTextBasedChannel, BushUser } from '#lib'; import { Message, type If, type Partialize } from 'discord.js'; @@ -15,19 +15,23 @@ export type PartialBushMessage = Partialize< 'type' | 'system' | 'pinned' | 'tts', 'content' | 'cleanContent' | 'author' >; + +/** + * Represents a message on Discord. + */ export class BushMessage<Cached extends boolean = boolean> extends Message<Cached> { public declare readonly client: BushClient; public declare util: BushCommandUtil<BushMessage<true>>; public declare readonly guild: If<Cached, BushGuild>; public declare readonly member: BushGuildMember | null; public declare author: BushUser; - public declare readonly channel: If<Cached, BushGuildTextBasedChannel, BushTextBasedChannels>; + public declare readonly channel: If<Cached, BushGuildTextBasedChannel, BushTextBasedChannel>; public constructor(client: BushClient, data: RawMessageData) { super(client, data); } } -export interface BushMessage { +export interface BushMessage<Cached extends boolean = boolean> extends Message<Cached> { fetch(force?: boolean): Promise<BushMessage>; } diff --git a/src/lib/extensions/discord.js/BushMessageManager.d.ts b/src/lib/extensions/discord.js/BushMessageManager.d.ts index f6bc8e7..84918c0 100644 --- a/src/lib/extensions/discord.js/BushMessageManager.d.ts +++ b/src/lib/extensions/discord.js/BushMessageManager.d.ts @@ -1,4 +1,4 @@ -import { BushMessageResolvable, type BushMessage, type BushTextBasedChannels } from '#lib'; +import { BushMessageResolvable, BushTextBasedChannel, type BushMessage } from '#lib'; import { CachedManager, type BaseFetchOptions, @@ -11,17 +11,95 @@ import { } from 'discord.js'; import type { RawMessageData } from 'discord.js/typings/rawDataTypes'; +/** + * Manages API methods for Messages and holds their cache. + */ export class BushMessageManager extends CachedManager<Snowflake, BushMessage, BushMessageResolvable> { - public constructor(channel: BushTextBasedChannels, iterable?: Iterable<RawMessageData>); - public channel: BushTextBasedChannels; + public constructor(channel: BushTextBasedChannel, iterable?: Iterable<RawMessageData>); + + /** + * The channel that the messages belong to + */ + public channel: BushTextBasedChannel; + + /** + * The cache of Messages + */ public cache: Collection<Snowflake, BushMessage>; + + /** + * Publishes a message in an announcement channel to all channels following it, even if it's not cached. + * @param message The message to publish + */ public crosspost(message: BushMessageResolvable): Promise<BushMessage>; + + /** + * Deletes a message, even if it's not cached. + * @param message The message to delete + */ public delete(message: BushMessageResolvable): Promise<void>; + + /** + * Edits a message, even if it's not cached. + * @param message The message to edit + * @param options The options to edit the message + */ public edit(message: BushMessageResolvable, options: MessagePayload | MessageEditOptions): Promise<BushMessage>; + + /** + * Gets a message, or messages, from this channel. + * <info>The returned Collection does not contain reaction users of the messages if they were not cached. + * Those need to be fetched separately in such a case.</info> + * @param message The id of the message to fetch, or query parameters. + * @param options Additional options for this fetch + * @example + * // Get message + * channel.messages.fetch('99539446449315840') + * .then(message => console.log(message.content)) + * .catch(console.error); + * @example + * // Get messages + * channel.messages.fetch({ limit: 10 }) + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + * @example + * // Get messages and filter by user id + * channel.messages.fetch() + * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) + * .catch(console.error); + */ public fetch(message: Snowflake, options?: BaseFetchOptions): Promise<BushMessage>; public fetch(options?: ChannelLogsQueryOptions, cacheOptions?: BaseFetchOptions): Promise<Collection<Snowflake, BushMessage>>; + + /** + * Fetches the pinned messages of this channel and returns a collection of them. + * <info>The returned Collection does not contain any reaction data of the messages. + * Those need to be fetched separately.</info> + * @param cache Whether to cache the message(s) + * @example + * // Get pinned messages + * channel.messages.fetchPinned() + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + */ public fetchPinned(cache?: boolean): Promise<Collection<Snowflake, BushMessage>>; + + /** + * Adds a reaction to a message, even if it's not cached. + * @param message The message to react to + * @param emoji The emoji to react with + */ public react(message: BushMessageResolvable, emoji: EmojiIdentifierResolvable): Promise<void>; + + /** + * Pins a message to the channel's pinned messages, even if it's not cached. + * @param message The message to pin + */ public pin(message: BushMessageResolvable): Promise<void>; + + /** + * Unpins a message from the channel's pinned messages, even if it's not cached. + * @param message The message to unpin + */ public unpin(message: BushMessageResolvable): Promise<void>; } diff --git a/src/lib/extensions/discord.js/BushMessageReaction.ts b/src/lib/extensions/discord.js/BushMessageReaction.ts index 51b439a..47d4119 100644 --- a/src/lib/extensions/discord.js/BushMessageReaction.ts +++ b/src/lib/extensions/discord.js/BushMessageReaction.ts @@ -4,6 +4,9 @@ import type { RawMessageReactionData } from 'discord.js/typings/rawDataTypes'; export type PartialBushMessageReaction = Partialize<BushMessageReaction, 'count'>; +/** + * Represents a reaction to a message. + */ export class BushMessageReaction extends MessageReaction { public declare readonly client: BushClient; public declare readonly emoji: BushGuildEmoji | BushReactionEmoji; diff --git a/src/lib/extensions/discord.js/BushNewsChannel.ts b/src/lib/extensions/discord.js/BushNewsChannel.ts index 716c57d..7df7f37 100644 --- a/src/lib/extensions/discord.js/BushNewsChannel.ts +++ b/src/lib/extensions/discord.js/BushNewsChannel.ts @@ -1,6 +1,9 @@ import type { BushGuild, BushGuildMember, BushMessageManager, BushThreadManager } from '#lib'; import { NewsChannel, type AllowedThreadTypeForNewsChannel, type Collection, type Snowflake } from 'discord.js'; +/** + * Represents a guild news channel on Discord. + */ export class BushNewsChannel extends NewsChannel { public declare threads: BushThreadManager<AllowedThreadTypeForNewsChannel>; public declare guild: BushGuild; diff --git a/src/lib/extensions/discord.js/BushPresence.ts b/src/lib/extensions/discord.js/BushPresence.ts index 60408f0..f0a3ba6 100644 --- a/src/lib/extensions/discord.js/BushPresence.ts +++ b/src/lib/extensions/discord.js/BushPresence.ts @@ -2,6 +2,9 @@ import type { BushClient, BushGuild, BushGuildMember, BushUser } from '#lib'; import { Presence } from 'discord.js'; import type { RawPresenceData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a user's presence. + */ export class BushPresence extends Presence { public declare guild: BushGuild | null; public declare readonly member: BushGuildMember | null; diff --git a/src/lib/extensions/discord.js/BushReactionEmoji.ts b/src/lib/extensions/discord.js/BushReactionEmoji.ts index b85916c..b2a7eb0 100644 --- a/src/lib/extensions/discord.js/BushReactionEmoji.ts +++ b/src/lib/extensions/discord.js/BushReactionEmoji.ts @@ -2,6 +2,11 @@ import type { BushMessageReaction } from '#lib'; import { ReactionEmoji } from 'discord.js'; import type { RawReactionEmojiData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a limited emoji set used for both custom and unicode emojis. Custom emojis + * will use this class opposed to the Emoji class when the client doesn't know enough + * information about them. + */ export class BushReactionEmoji extends ReactionEmoji { public declare reaction: BushMessageReaction; diff --git a/src/lib/extensions/discord.js/BushRole.ts b/src/lib/extensions/discord.js/BushRole.ts index bf09e2d..acf795d 100644 --- a/src/lib/extensions/discord.js/BushRole.ts +++ b/src/lib/extensions/discord.js/BushRole.ts @@ -2,6 +2,9 @@ import type { BushClient, BushGuild, BushGuildMember } from '#lib'; import { Role, type Collection, type Snowflake } from 'discord.js'; import type { RawRoleData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a role on Discord. + */ export class BushRole extends Role { public declare guild: BushGuild; public declare readonly members: Collection<Snowflake, BushGuildMember>; diff --git a/src/lib/extensions/discord.js/BushSelectMenuInteraction.ts b/src/lib/extensions/discord.js/BushSelectMenuInteraction.ts index d33ddd3..903b43f 100644 --- a/src/lib/extensions/discord.js/BushSelectMenuInteraction.ts +++ b/src/lib/extensions/discord.js/BushSelectMenuInteraction.ts @@ -1,15 +1,18 @@ -import type { BushClient, BushGuild, BushGuildMember, BushGuildTextBasedChannel, BushTextBasedChannels, BushUser } from '#lib'; +import type { BushClient, BushGuild, BushGuildMember, BushGuildTextBasedChannel, BushTextBasedChannel, BushUser } from '#lib'; import type { APIInteractionGuildMember } from 'discord-api-types/v9'; import { SelectMenuInteraction, type CacheType, type CacheTypeReducer } from 'discord.js'; import type { RawMessageSelectMenuInteractionData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a select menu interaction. + */ export class BushSelectMenuInteraction<Cached extends CacheType = CacheType> extends SelectMenuInteraction<Cached> { public declare readonly channel: CacheTypeReducer< Cached, BushGuildTextBasedChannel | null, BushGuildTextBasedChannel | null, BushGuildTextBasedChannel | null, - BushTextBasedChannels | null + BushTextBasedChannel | null >; public declare readonly guild: CacheTypeReducer<Cached, BushGuild, null>; public declare member: CacheTypeReducer<Cached, BushGuildMember, APIInteractionGuildMember>; diff --git a/src/lib/extensions/discord.js/BushStageChannel.ts b/src/lib/extensions/discord.js/BushStageChannel.ts index 1391757..5f6c581 100644 --- a/src/lib/extensions/discord.js/BushStageChannel.ts +++ b/src/lib/extensions/discord.js/BushStageChannel.ts @@ -2,11 +2,14 @@ import type { BushCategoryChannel, BushGuild, BushGuildMember, BushStageInstance import { StageChannel, type Collection, type Snowflake } from 'discord.js'; import type { RawGuildChannelData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a guild stage channel on Discord. + */ export class BushStageChannel extends StageChannel { - public declare readonly instance: BushStageInstance | null; public declare readonly members: Collection<Snowflake, BushGuildMember>; public declare guild: BushGuild; public declare readonly parent: BushCategoryChannel | null; + public declare readonly stageInstance: BushStageInstance | null; public constructor(guild: BushGuild, data?: RawGuildChannelData) { super(guild, data); diff --git a/src/lib/extensions/discord.js/BushStageInstance.ts b/src/lib/extensions/discord.js/BushStageInstance.ts index 3b46bf6..80fa5cf 100644 --- a/src/lib/extensions/discord.js/BushStageInstance.ts +++ b/src/lib/extensions/discord.js/BushStageInstance.ts @@ -2,6 +2,9 @@ import type { BushClient, BushGuild, BushStageChannel } from '#lib'; import { StageInstance } from 'discord.js'; import type { RawStageInstanceData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a stage instance. + */ export class BushStageInstance extends StageInstance { public declare readonly channel: BushStageChannel | null; public declare readonly guild: BushGuild | null; diff --git a/src/lib/extensions/discord.js/BushStoreChannel.ts b/src/lib/extensions/discord.js/BushStoreChannel.ts index 918c27b..cb75076 100644 --- a/src/lib/extensions/discord.js/BushStoreChannel.ts +++ b/src/lib/extensions/discord.js/BushStoreChannel.ts @@ -2,6 +2,10 @@ import type { BushCategoryChannel, BushClient, BushGuild, BushGuildMember } from import { StoreChannel, type Collection, type Snowflake } from 'discord.js'; import type { RawGuildChannelData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a guild store channel on Discord. + * @deprecated Store channels are deprecated and will be removed from Discord in March 2022. See [Self-serve Game Selling Deprecation](https://support-dev.discord.com/hc/en-us/articles/4414590563479) for more information + */ // eslint-disable-next-line deprecation/deprecation export class BushStoreChannel extends StoreChannel { public declare guild: BushGuild; diff --git a/src/lib/extensions/discord.js/BushTextChannel.ts b/src/lib/extensions/discord.js/BushTextChannel.ts index 57f0833..45e1200 100644 --- a/src/lib/extensions/discord.js/BushTextChannel.ts +++ b/src/lib/extensions/discord.js/BushTextChannel.ts @@ -2,6 +2,9 @@ import type { BushGuild, BushMessageManager, BushThreadManager } from '#lib'; import { TextChannel, type AllowedThreadTypeForTextChannel } from 'discord.js'; import type { RawGuildChannelData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a guild text channel on Discord. + */ export class BushTextChannel extends TextChannel { public declare guild: BushGuild; public declare messages: BushMessageManager; diff --git a/src/lib/extensions/discord.js/BushThreadChannel.ts b/src/lib/extensions/discord.js/BushThreadChannel.ts index 5f68213..4310e83 100644 --- a/src/lib/extensions/discord.js/BushThreadChannel.ts +++ b/src/lib/extensions/discord.js/BushThreadChannel.ts @@ -10,6 +10,9 @@ import type { import { ThreadChannel, type Collection, type Snowflake } from 'discord.js'; import type { RawThreadChannelData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a thread channel on Discord. + */ export class BushThreadChannel extends ThreadChannel { public declare guild: BushGuild; public declare messages: BushMessageManager; diff --git a/src/lib/extensions/discord.js/BushThreadManager.d.ts b/src/lib/extensions/discord.js/BushThreadManager.d.ts index c824ae7..1366d68 100644 --- a/src/lib/extensions/discord.js/BushThreadManager.d.ts +++ b/src/lib/extensions/discord.js/BushThreadManager.d.ts @@ -14,12 +14,69 @@ import { } from 'discord.js'; import type { RawThreadChannelData } from 'discord.js/typings/rawDataTypes'; +/** + * Manages API methods for {@link BushThreadChannel} objects and stores their cache. + */ export class BushThreadManager<AllowedThreadType> extends CachedManager<Snowflake, BushThreadChannel, ThreadChannelResolvable> { public constructor(channel: TextChannel | NewsChannel, iterable?: Iterable<RawThreadChannelData>); + + /** + * The channel this Manager belongs to + */ public channel: TextChannel | NewsChannel; + + /** + * Creates a new thread in the channel. + * @param options Options to create a new thread + * @example + * // Create a new public thread + * channel.threads + * .create({ + * name: 'food-talk', + * autoArchiveDuration: 60, + * reason: 'Needed a separate thread for food', + * }) + * .then(threadChannel => console.log(threadChannel)) + * .catch(console.error); + * @example + * // Create a new private thread + * channel.threads + * .create({ + * name: 'mod-talk', + * autoArchiveDuration: 60, + * type: 'GUILD_PRIVATE_THREAD', + * reason: 'Needed a separate thread for moderation', + * }) + * .then(threadChannel => console.log(threadChannel)) + * .catch(console.error); + */ public create(options: ThreadCreateOptions<AllowedThreadType>): Promise<ThreadChannel>; + + /** + * Obtains a thread from Discord, or the channel cache if it's already available. + * @param options The options to fetch threads. If it is a + * ThreadChannelResolvable then the specified thread will be fetched. Fetches all active threads if `undefined` + * @param cacheOptions Additional options for this fetch. <warn>The `force` field gets ignored + * if `options` is not a {@link ThreadChannelResolvable}</warn> + * @example + * // Fetch a thread by its id + * channel.threads.fetch('831955138126104859') + * .then(channel => console.log(channel.name)) + * .catch(console.error); + */ public fetch(options: ThreadChannelResolvable, cacheOptions?: BaseFetchOptions): Promise<ThreadChannel | null>; public fetch(options?: FetchThreadsOptions, cacheOptions?: { cache?: boolean }): Promise<FetchedThreads>; + + /** + * Obtains a set of archived threads from Discord, requires `READ_MESSAGE_HISTORY` in the parent channel. + * @param options The options to fetch archived threads + * @param cache Whether to cache the new thread objects if they aren't already + */ public fetchArchived(options?: FetchArchivedThreadOptions, cache?: boolean): Promise<FetchedThreads>; + + /** + * Obtains the accessible active threads from Discord, requires `READ_MESSAGE_HISTORY` in the parent channel. + * @param cache Whether to cache the new thread objects if they aren't already + */ public fetchActive(cache?: boolean): Promise<FetchedThreads>; } diff --git a/src/lib/extensions/discord.js/BushThreadMember.ts b/src/lib/extensions/discord.js/BushThreadMember.ts index fa7588e..a316046 100644 --- a/src/lib/extensions/discord.js/BushThreadMember.ts +++ b/src/lib/extensions/discord.js/BushThreadMember.ts @@ -2,6 +2,9 @@ import type { BushGuildMember, BushThreadChannel, BushUser } from '#lib'; import { ThreadMember } from 'discord.js'; import type { RawThreadMemberData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a Member for a Thread. + */ export class BushThreadMember extends ThreadMember { public declare readonly guildMember: BushGuildMember | null; public declare readonly user: BushUser | null; diff --git a/src/lib/extensions/discord.js/BushThreadMemberManager.d.ts b/src/lib/extensions/discord.js/BushThreadMemberManager.d.ts index 7560c0e..dedf102 100644 --- a/src/lib/extensions/discord.js/BushThreadMemberManager.d.ts +++ b/src/lib/extensions/discord.js/BushThreadMemberManager.d.ts @@ -2,13 +2,42 @@ import type { BushClient, BushThreadChannel, BushThreadMember, BushThreadMemberR import { CachedManager, type BaseFetchOptions, type Collection, type Snowflake, type UserResolvable } from 'discord.js'; import type { RawThreadMemberData } from 'discord.js/typings/rawDataTypes'; +/** + * Manages API methods for GuildMembers and stores their cache. + */ export class BushThreadMemberManager extends CachedManager<Snowflake, BushThreadMember, BushThreadMemberResolvable> { public constructor(thread: BushThreadChannel, iterable?: Iterable<RawThreadMemberData>); public declare readonly client: BushClient; + + /** + * The thread this manager belongs to + */ public thread: BushThreadChannel; + + /** + * Adds a member to the thread. + * @param member The member to add + * @param reason The reason for adding this member + */ public add(member: UserResolvable | '@me', reason?: string): Promise<Snowflake>; + + /** + * Fetches member(s) for the thread from Discord, requires access to the `GUILD_MEMBERS` gateway intent. + * @param member The member to fetch. If `undefined`, all members in the thread are fetched, and will be + * cached based on `options.cache`. If boolean, this serves the purpose of `options.cache`. + * @param options Additional options for this fetch + */ public fetch(member?: UserResolvable, options?: BaseFetchOptions): Promise<BushThreadMember>; - /** @deprecated Use `fetch(member, options)` instead. */ + + /** + * @deprecated Use `fetch(member, options)` instead. + */ public fetch(cache?: boolean): Promise<Collection<Snowflake, BushThreadMember>>; + + /** + * Remove a user from the thread. + * @param id The id of the member to remove + * @param reason The reason for removing this member from the thread + */ public remove(id: Snowflake | '@me', reason?: string): Promise<Snowflake>; } diff --git a/src/lib/extensions/discord.js/BushUser.ts b/src/lib/extensions/discord.js/BushUser.ts index 9b2c92a..5ab288e 100644 --- a/src/lib/extensions/discord.js/BushUser.ts +++ b/src/lib/extensions/discord.js/BushUser.ts @@ -4,6 +4,9 @@ import type { RawUserData } from 'discord.js/typings/rawDataTypes'; export type PartialBushUser = Partialize<BushUser, 'username' | 'tag' | 'discriminator' | 'isOwner' | 'isSuperUser'>; +/** + * Represents a user on Discord. + */ export class BushUser extends User { public declare readonly client: BushClient; public declare readonly dmChannel: BushDMChannel | null; @@ -12,10 +15,16 @@ export class BushUser extends User { super(client, data); } + /** + * Indicates whether the user is an owner of the bot. + */ public isOwner(): boolean { return client.isOwner(this); } + /** + * Indicates whether the user is a superuser of the bot. + */ public isSuperUser(): boolean { return client.isSuperUser(this); } diff --git a/src/lib/extensions/discord.js/BushUserManager.d.ts b/src/lib/extensions/discord.js/BushUserManager.d.ts index 595332a..5d814da 100644 --- a/src/lib/extensions/discord.js/BushUserManager.d.ts +++ b/src/lib/extensions/discord.js/BushUserManager.d.ts @@ -1,8 +1,59 @@ -import type { BushClient, BushUser, BushUserResolvable } from '#lib'; -import { CachedManager, type BaseFetchOptions, type Snowflake } from 'discord.js'; +import type { BushClient, BushDMChannel, BushUser, BushUserResolvable } from '#lib'; +import { + CachedManager, + Message, + MessageOptions, + MessagePayload, + UserFlags, + type BaseFetchOptions, + type Snowflake +} from 'discord.js'; import type { RawUserData } from 'discord.js/typings/rawDataTypes'; +/** + * Manages API methods for users and stores their cache. + */ export class BushUserManager extends CachedManager<Snowflake, BushUser, BushUserResolvable> { - public constructor(client: BushClient, iterable?: Iterable<RawUserData>); - public fetch(id: Snowflake, options?: BaseFetchOptions): Promise<BushUser>; + private constructor(client: BushClient, iterable?: Iterable<RawUserData>); + + /** + * The DM between the client's user and a user + * @param userId The user id + * @private + */ + public dmChannel(userId: Snowflake): BushDMChannel | null; + + /** + * Creates a {@link DMChannel} between the client and a user. + * @param user The UserResolvable to identify + * @param options Additional options for this fetch + */ + public createDM(user: BushUserResolvable, options?: BaseFetchOptions): Promise<BushDMChannel>; + + /** + * Deletes a {@link DMChannel} (if one exists) between the client and a user. Resolves with the channel if successful. + * @param user The UserResolvable to identify + */ + public deleteDM(user: BushUserResolvable): Promise<BushDMChannel>; + + /** + * Obtains a user from Discord, or the user cache if it's already available. + * @param user The user to fetch + * @param options Additional options for this fetch + */ + public fetch(user: BushUserResolvable, options?: BaseFetchOptions): Promise<BushUser>; + + /** + * Fetches a user's flags. + * @param user The UserResolvable to identify + * @param options Additional options for this fetch + */ + public fetchFlags(user: BushUserResolvable, options?: BaseFetchOptions): Promise<UserFlags>; + + /** + * Sends a message to a user. + * @param user The UserResolvable to identify + * @param options The options to provide + */ + public send(user: BushUserResolvable, options: string | MessagePayload | MessageOptions): Promise<Message>; } diff --git a/src/lib/extensions/discord.js/BushVoiceChannel.ts b/src/lib/extensions/discord.js/BushVoiceChannel.ts index d5cdafc..9f246e5 100644 --- a/src/lib/extensions/discord.js/BushVoiceChannel.ts +++ b/src/lib/extensions/discord.js/BushVoiceChannel.ts @@ -2,6 +2,9 @@ import type { BushClient, BushGuild, BushGuildMember } from '#lib'; import { VoiceChannel, type Collection, type Snowflake } from 'discord.js'; import type { RawGuildChannelData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents a guild voice channel on Discord. + */ export class BushVoiceChannel extends VoiceChannel { public declare readonly client: BushClient; public declare readonly members: Collection<Snowflake, BushGuildMember>; diff --git a/src/lib/extensions/discord.js/BushVoiceState.ts b/src/lib/extensions/discord.js/BushVoiceState.ts index a479143..3f19201 100644 --- a/src/lib/extensions/discord.js/BushVoiceState.ts +++ b/src/lib/extensions/discord.js/BushVoiceState.ts @@ -1,10 +1,13 @@ -import type { BushGuild, BushGuildMember, BushStageChannel, BushVoiceChannel } from '#lib'; +import type { BushClient, BushGuild, BushGuildMember, BushVoiceBasedChannel } from '#lib'; import { VoiceState } from 'discord.js'; import type { RawVoiceStateData } from 'discord.js/typings/rawDataTypes'; +/** + * Represents the voice state for a Guild Member. + */ export class BushVoiceState extends VoiceState { - // public declare readonly client: BushClient; - public declare readonly channel: BushVoiceChannel | BushStageChannel | null; + public declare readonly client: BushClient; + public declare readonly channel: BushVoiceBasedChannel | null; public declare guild: BushGuild; public declare readonly member: BushGuildMember | null; diff --git a/src/lib/extensions/global.d.ts b/src/lib/extensions/global.d.ts index 1df86bb..a6f2b5a 100644 --- a/src/lib/extensions/global.d.ts +++ b/src/lib/extensions/global.d.ts @@ -1,7 +1,14 @@ /* eslint-disable no-var */ import type { BushClient, BushClientUtil } from '#lib'; declare global { + /** + * The bushbot client. + */ var client: BushClient; + + /** + * The bushbot client util. + */ var util: BushClientUtil; // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/lib/utils/BushCache.ts b/src/lib/utils/BushCache.ts index bfbd7e3..476f516 100644 --- a/src/lib/utils/BushCache.ts +++ b/src/lib/utils/BushCache.ts @@ -3,7 +3,7 @@ import { Guild } from '../models/Guild.js'; export class BushCache { public global = new GlobalCache(); - public guilds = new Collection<Snowflake, Guild>(); + public guilds = new GuildCache(); } export class GlobalCache { @@ -13,3 +13,5 @@ export class GlobalCache { public blacklistedGuilds: Snowflake[] = []; public blacklistedUsers: Snowflake[] = []; } + +export class GuildCache extends Collection<Snowflake, Guild> {} diff --git a/src/lib/utils/BushConstants.ts b/src/lib/utils/BushConstants.ts index 434a0a7..297a641 100644 --- a/src/lib/utils/BushConstants.ts +++ b/src/lib/utils/BushConstants.ts @@ -168,7 +168,9 @@ export class BushConstants { CREATE_PRIVATE_THREADS: { name: 'Create Private Threads', important: false }, USE_EXTERNAL_STICKERS: { name: 'Use External Stickers', important: false }, SEND_MESSAGES_IN_THREADS: { name: 'Send Messages In Threads', important: false }, - START_EMBEDDED_ACTIVITIES: { name: 'Start Activities', important: false } + START_EMBEDDED_ACTIVITIES: { name: 'Start Activities', important: false }, + MODERATE_MEMBERS: { name: 'Moderate Members', important: true }, + MANAGE_EVENTS: { name: 'Manage Events', important: true }, }, // prettier-ignore diff --git a/src/lib/utils/BushLogger.ts b/src/lib/utils/BushLogger.ts index 5bae96d..43c9203 100644 --- a/src/lib/utils/BushLogger.ts +++ b/src/lib/utils/BushLogger.ts @@ -1,10 +1,21 @@ +import * as assert from 'assert'; import chalk from 'chalk'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { MessageEmbed, Util, type Message, type PartialTextBasedChannelFields } from 'discord.js'; import { inspect } from 'util'; import { type BushSendMessageType } from '../extensions/discord-akairo/BushClient'; +/** + * A custom logging utility for the bot. + */ export class BushLogger { + /** + * Parses the content surrounding by `<<>>` and emphasizes it with the given color or by making it bold. + * @param content The content to parse. + * @param color The color to emphasize the content with. + * @param discordFormat Whether or not to format the content for discord. + * @returns The formatted content. + */ static #parseFormatting( content: any, color: 'blueBright' | 'blackBright' | 'redBright' | 'yellowBright' | 'greenBright' | '', @@ -23,6 +34,13 @@ export class BushLogger { return tempParsedArray.join(''); } + /** + * Inspects the content and returns a string. + * @param content The content to inspect. + * @param depth The depth the content will inspected. Defaults to `2`. + * @param colors Whether or not to use colors in the output. Defaults to `true`. + * @returns The inspected content. + */ static #inspectContent(content: any, depth = 2, colors = true): string { if (typeof content !== 'string') { return inspect(content, { depth, colors }); @@ -30,6 +48,11 @@ export class BushLogger { return content; } + /** + * Strips ANSI color codes from a string. + * @param text The string to strip color codes from. + * @returns A string without ANSI color codes. + */ static #stripColor(text: string): string { return text.replace( // eslint-disable-next-line no-control-regex @@ -38,6 +61,10 @@ export class BushLogger { ); } + /** + * Generates a formatted timestamp for logging. + * @returns The formatted timestamp. + */ static #getTimeStamp(): string { const now = new Date(); const hours = now.getHours(); @@ -63,26 +90,39 @@ export class BushLogger { } /** - * Sends a message to the log channel - * @param message The parameter to pass to {@link PartialTextBasedChannelFields.send} + * Sends a message to the log channel. + * @param message The parameter to pass to {@link PartialTextBasedChannelFields.send}. + * @returns The message sent. */ - public static async channelLog(message: BushSendMessageType) { + public static async channelLog(message: BushSendMessageType): Promise<Message | null> { const channel = await util.getConfigChannel('log'); - await channel.send(message).catch(() => {}); + return await channel.send(message).catch(() => null); } /** - * Sends a message to the error channel + * Sends a message to the error channel. + * @param message The parameter to pass to {@link PartialTextBasedChannelFields.send}. + * @returns The message sent. */ - public static async channelError(message: BushSendMessageType): Promise<Message> { + public static async channelError(message: BushSendMessageType): Promise<Message | null> { const channel = await util.getConfigChannel('error'); + if (!channel) { + void this.error( + 'BushLogger', + `Could not find error channel, was originally going to send: \n${util.inspect(message, { + colors: true + })}\n${new Error().stack?.substring(8)}`, + false + ); + 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. + * @param depth The depth the content will inspected. Defaults to `0`. */ public static debug(content: any, depth = 0): void { if (!client.config.isDevelopment) return; @@ -103,10 +143,10 @@ export class BushLogger { * Logs verbose information. Highlight information by surrounding it in `<<>>`. * @param header The header printed before the content, displayed in grey. * @param content The content to log, highlights displayed in bright black. - * @param sendChannel Should this also be logged to discord? Defaults to false. - * @param depth The depth the content will inspected. Defaults to 0. + * @param sendChannel Should this also be logged to discord? Defaults to `false`. + * @param depth The depth the content will inspected. Defaults to `0`. */ - public static async verbose(header: string, content: any, sendChannel = false, depth = 0) { + public static async verbose(header: string, content: any, sendChannel = false, depth = 0): Promise<void> { if (!client.config.logging.verbose) return; const newContent = this.#inspectContent(content, depth, true); console.info( @@ -121,13 +161,30 @@ export class BushLogger { } /** + * Logs very verbose information. Highlight information by surrounding it in `<<>>`. + * @param header The header printed before the content, displayed in grey. + * @param content The content to log, highlights displayed in bright black. + * @param depth The depth the content will inspected. Defaults to `0`. + */ + public static async superVerbose(header: string, content: any, depth = 0): Promise<void> { + if (!client.config.logging.verbose) return; + const newContent = this.#inspectContent(content, depth, true); + console.info( + `${chalk.bgHex('#8423b8')(this.#getTimeStamp())} ${chalk.hex('#8423b8')(`[${header}]`)} ${this.#parseFormatting( + newContent, + 'blackBright' + )}` + ); + } + + /** * Logs information. Highlight information by surrounding it in `<<>>`. * @param header The header displayed before the content, displayed in cyan. * @param content The content to log, highlights displayed in bright blue. - * @param sendChannel Should this also be logged to discord? Defaults to false. - * @param depth The depth the content will inspected. Defaults to 0. + * @param sendChannel Should this also be logged to discord? Defaults to `false`. + * @param depth The depth the content will inspected. Defaults to `0`. */ - public static async info(header: string, content: any, sendChannel = true, depth = 0) { + public static async info(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { if (!client.config.logging.info) return; const newContent = this.#inspectContent(content, depth, true); console.info( @@ -145,10 +202,10 @@ export class BushLogger { * Logs warnings. Highlight information by surrounding it in `<<>>`. * @param header The header displayed before the content, displayed in yellow. * @param content The content to log, highlights displayed in bright yellow. - * @param sendChannel Should this also be logged to discord? Defaults to false. - * @param depth The depth the content will inspected. Defaults to 0. + * @param sendChannel Should this also be logged to discord? Defaults to `false`. + * @param depth The depth the content will inspected. Defaults to `0`. */ - public static async warn(header: string, content: any, sendChannel = true, depth = 0) { + public static async warn(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { const newContent = this.#inspectContent(content, depth, true); console.warn( `${chalk.bgYellow(this.#getTimeStamp())} ${chalk.yellow(`[${header}]`)} ${this.#parseFormatting( @@ -169,10 +226,10 @@ export class BushLogger { * Logs errors. Highlight information by surrounding it in `<<>>`. * @param header The header displayed before the content, displayed in bright red. * @param content The content to log, highlights displayed in bright red. - * @param sendChannel Should this also be logged to discord? Defaults to false. - * @param depth The depth the content will inspected. Defaults to 0. + * @param sendChannel Should this also be logged to discord? Defaults to `false`. + * @param depth The depth the content will inspected. Defaults to `0`. */ - public static async error(header: string, content: any, sendChannel = true, depth = 0) { + public static async error(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { const newContent = this.#inspectContent(content, depth, true); console.error( `${chalk.bgRedBright(this.#getTimeStamp())} ${chalk.redBright(`[${header}]`)} ${this.#parseFormatting( @@ -193,10 +250,10 @@ export class BushLogger { * Logs successes. Highlight information by surrounding it in `<<>>`. * @param header The header displayed before the content, displayed in green. * @param content The content to log, highlights displayed in bright green. - * @param sendChannel Should this also be logged to discord? Defaults to false. - * @param depth The depth the content will inspected. Defaults to 0. + * @param sendChannel Should this also be logged to discord? Defaults to `false`. + * @param depth The depth the content will inspected. Defaults to `0`. */ - public static async success(header: string, content: any, sendChannel = true, depth = 0) { + public static async success(header: string, content: any, sendChannel = true, depth = 0): Promise<void> { const newContent = this.#inspectContent(content, depth, true); console.log( `${chalk.bgGreen(this.#getTimeStamp())} ${chalk.greenBright(`[${header}]`)} ${this.#parseFormatting( @@ -211,6 +268,23 @@ export class BushLogger { .setTimestamp(); await this.channelLog({ embeds: [embed] }).catch(() => {}); } + + /** + * Asserts a condition. If the condition is false, an error is thrown. + * @param type The type of assertion. + * @param actual The value to test. + * @param expected The expected value. + * @param message The error to throw if the assertion fails. + */ + public static assert<T>(type: AssertTypeEqual, actual: unknown, expected: T, message: Error): asserts actual is T; + public static assert<T>(type: AssertTypeNotEqual, actual: unknown, expected: T, message: Error): void; + public static assert(type: AssertType, actual: unknown, expected: unknown, message: Error): void { + assert[type](actual, expected, message); + } } +export type AssertTypeEqual = 'deepEqual' | 'deepStrictEqual' | 'equal' | 'strictEqual'; +export type AssertTypeNotEqual = 'notDeepEqual' | 'notDeepStrictEqual' | 'notEqual' | 'notStrictEqual'; +export type AssertType = AssertTypeEqual | AssertTypeNotEqual; + /** @typedef {PartialTextBasedChannelFields} vscodeDontDeleteMyImportTy */ diff --git a/src/listeners/client/dcjsDebug.ts b/src/listeners/client/dcjsDebug.ts new file mode 100644 index 0000000..52406c1 --- /dev/null +++ b/src/listeners/client/dcjsDebug.ts @@ -0,0 +1,15 @@ +import { BushListener, type BushClientEvents } from '#lib'; + +export default class DiscordJsDebugListener extends BushListener { + public constructor() { + super('discordJsDebug', { + emitter: 'client', + event: 'debug', + category: 'client' + }); + } + + public override async exec(...[message]: BushClientEvents['debug']): Promise<void> { + void client.console.superVerbose('dc.js-debug', message); + } +} diff --git a/src/listeners/client/dcjsError.ts b/src/listeners/client/dcjsError.ts new file mode 100644 index 0000000..4908720 --- /dev/null +++ b/src/listeners/client/dcjsError.ts @@ -0,0 +1,15 @@ +import { BushListener, type BushClientEvents } from '#lib'; + +export default class DiscordJsErrorListener extends BushListener { + public constructor() { + super('discordJsError', { + emitter: 'client', + event: 'error', + category: 'client' + }); + } + + public override async exec(...[error]: BushClientEvents['error']): Promise<void> { + void client.console.superVerbose('dc.js-error', error); + } +} diff --git a/src/listeners/client/dcjsWarn.ts b/src/listeners/client/dcjsWarn.ts new file mode 100644 index 0000000..cc2b30f --- /dev/null +++ b/src/listeners/client/dcjsWarn.ts @@ -0,0 +1,15 @@ +import { BushListener, type BushClientEvents } from '#lib'; + +export default class DiscordJsWarnListener extends BushListener { + public constructor() { + super('discordJsWarn', { + emitter: 'client', + event: 'warn', + category: 'client' + }); + } + + public override async exec(...[message]: BushClientEvents['warn']): Promise<void> { + void client.console.superVerbose('dc.js-warn', message); + } +} diff --git a/src/listeners/commands/commandError.ts b/src/listeners/commands/commandError.ts index c005574..f26ff7f 100644 --- a/src/listeners/commands/commandError.ts +++ b/src/listeners/commands/commandError.ts @@ -1,7 +1,7 @@ import { type BushCommandHandlerEvents } from '#lib'; import { Severity } from '@sentry/types'; -import { Command, type AkairoMessage, type GuildTextBasedChannels } from 'discord-akairo'; -import { Formatters, MessageEmbed, type DMChannel, type Message } from 'discord.js'; +import { Command, type AkairoMessage } from 'discord-akairo'; +import { Formatters, GuildTextBasedChannel, MessageEmbed, type DMChannel, type Message } from 'discord.js'; import { BushListener } from '../../lib/extensions/discord-akairo/BushListener.js'; export default class CommandErrorListener extends BushListener { @@ -24,7 +24,7 @@ export default class CommandErrorListener extends BushListener { const channel = message.channel?.type === 'DM' ? (message.channel as DMChannel)!.recipient.tag - : (message.channel as GuildTextBasedChannels)!.name; + : (message.channel as GuildTextBasedChannel)!.name; const command = _command ?? message.util?.parsed?.command; client.sentry.captureException(error, { @@ -38,7 +38,7 @@ export default class CommandErrorListener extends BushListener { 'channel.id': message.channel!.type === 'DM' ? (message.channel as DMChannel)!.recipient.id - : (message.channel as GuildTextBasedChannels)!.id, + : (message.channel as GuildTextBasedChannel)!.id, 'channel.name': channel, 'guild.id': message.guild?.id, 'guild.name': message.guild?.name, diff --git a/src/listeners/commands/commandStarted.ts b/src/listeners/commands/commandStarted.ts index afc689d..76974c8 100644 --- a/src/listeners/commands/commandStarted.ts +++ b/src/listeners/commands/commandStarted.ts @@ -1,7 +1,6 @@ import { BushListener, type BushCommandHandlerEvents } from '#lib'; import { Severity } from '@sentry/types'; -import { type GuildTextBasedChannels } from 'discord-akairo'; -import { type DMChannel } from 'discord.js'; +import { GuildTextBasedChannel, type DMChannel } from 'discord.js'; export default class CommandStartedListener extends BushListener { public constructor() { @@ -25,11 +24,11 @@ export default class CommandStartedListener extends BushListener { 'channel.id': message.channel!.type === 'DM' ? (message.channel as DMChannel)!.recipient.id - : (message.channel as GuildTextBasedChannels)!.id, + : (message.channel as GuildTextBasedChannel)!.id, 'channel.name': message.channel!.type === 'DM' ? (message.channel as DMChannel)!.recipient.tag - : (message.channel as GuildTextBasedChannels)!.name, + : (message.channel as GuildTextBasedChannel)!.name, 'guild.id': message.guild?.id, 'guild.name': message.guild?.name, 'environment': client.config.environment diff --git a/src/listeners/message/autoThread.ts b/src/listeners/message/autoThread.ts index c326cec..6ee19ed 100644 --- a/src/listeners/message/autoThread.ts +++ b/src/listeners/message/autoThread.ts @@ -1,6 +1,5 @@ import { BushListener, type BushClientEvents, type BushTextChannel } from '#lib'; -import { type GuildTextBasedChannels } from 'discord-akairo'; -import { MessageEmbed } from 'discord.js'; +import { GuildTextBasedChannel, MessageEmbed } from 'discord.js'; export default class autoThreadListener extends BushListener { public constructor() { @@ -56,9 +55,9 @@ export default class autoThreadListener extends BushListener { .then(() => client.console.info( 'supportThread', - `opened a support thread for <<${message.author.tag}>> in <<${ - (message.channel as GuildTextBasedChannels).name - }>> in <<${message.guild!.name}>>.` + `opened a support thread for <<${message.author.tag}>> in <<${(message.channel as GuildTextBasedChannel).name}>> in <<${ + message.guild!.name + }>>.` ) ); } diff --git a/src/listeners/other/warning.ts b/src/listeners/other/warning.ts index 4dd79fe..57f2a7d 100644 --- a/src/listeners/other/warning.ts +++ b/src/listeners/other/warning.ts @@ -11,6 +11,8 @@ export default class WarningListener extends BushListener { } public override async exec(error: Error) { + if (error.name === 'ExperimentalWarning') return; + client.sentry.captureException(error, { level: Severity.Warning }); diff --git a/tsconfig.json b/tsconfig.json index 6b7feca..df315ca 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,9 +23,8 @@ "removeComments": true, "paths": { "#lib": ["./src/lib/index.js"] - }, - "skipLibCheck": true + } }, - "include": ["src/**/*.ts", "lib/**/*.ts", "ecosystem.config.cjs"], + "include": ["src/**/*.ts", "src/**/*d.ts", "lib/**/*.ts", "ecosystem.config.cjs"], "exclude": ["dist", "node_modules"] } @@ -14,34 +14,23 @@ __metadata: languageName: node linkType: hard -"@discordjs/builders@npm:^0.9.0": - version: 0.9.0 - resolution: "@discordjs/builders@npm:0.9.0" +"@discordjs/builders@npm:^0.10.0": + version: 0.10.0 + resolution: "@discordjs/builders@npm:0.10.0" dependencies: "@sindresorhus/is": ^4.2.0 - discord-api-types: ^0.24.0 + discord-api-types: ^0.25.2 ts-mixer: ^6.0.0 tslib: ^2.3.1 zod: ^3.11.6 - checksum: 75278bd4cb2ba09a83e9d308d357e6550fc55af77d7b95d1749ba351d97df80af840144f14f849aa2910966f26a85fcde2271bc1ff5342bbb8ffabe76dd2640c - languageName: node - linkType: hard - -"@discordjs/collection@npm:^0.3.2": - version: 0.3.2 - resolution: "@discordjs/collection@npm:0.3.2" - checksum: 77def13598fce0f33cdd1e45e0e57b4452a1fa348e30d7d3a3b5a56409def4c4785462828b163aea83d659b93412d5dc18c22c85eff499dfc11c24011e8b18e6 + checksum: 8195f519f28587dee05d9b975c19357c40623d17dc4ced01db11d24faab16969743c40d7cfb959ccb78ffb525c6d03f4f9c821dd712fef57bdd609b53add8727 languageName: node linkType: hard -"@discordjs/form-data@npm:^3.0.1": - version: 3.0.1 - resolution: "@discordjs/form-data@npm:3.0.1" - dependencies: - asynckit: ^0.4.0 - combined-stream: ^1.0.8 - mime-types: ^2.1.12 - checksum: 2b431b1a14f8ac521e1c13567856cef7e61e05aafe2721e03d9e074bb7e5f45c89fc123b126964943edf301ec612d04515dcf068ea9773a2e30a99a844c00603 +"@discordjs/collection@npm:^0.4.0": + version: 0.4.0 + resolution: "@discordjs/collection@npm:0.4.0" + checksum: fa8fc4246921f3230eb6c5d6d4dc0caf9dd659fcc903175944edf4fb0a9ed9913fdf164733d3f1e644ef469bc79b0d38a526ee620b92169cb40e79b40b0c716b languageName: node linkType: hard @@ -372,13 +361,13 @@ __metadata: linkType: hard "@types/express-serve-static-core@npm:^4.17.18": - version: 4.17.26 - resolution: "@types/express-serve-static-core@npm:4.17.26" + version: 4.17.27 + resolution: "@types/express-serve-static-core@npm:4.17.27" dependencies: "@types/node": "*" "@types/qs": "*" "@types/range-parser": "*" - checksum: 064080c3c21136f9017e108559602ec5989ce90828d6ede6e3c375e5693a72500b3c06206cdc4a59496ae1ad8af1e282223efb3d79907233fc4811a2cf4d4392 + checksum: fef52b941f903011e31a5886369301d7765580a034cd011a2d3a7dbe6a6edf4f77537710a52e3e2258c6fc59c611f228594c213f984cda767654ab6c5c199e9e languageName: node linkType: hard @@ -473,9 +462,9 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:^17.0.2": - version: 17.0.2 - resolution: "@types/node@npm:17.0.2" - checksum: a827d2542ef7adba5c79ba7f85b7c2ba8256d317bd99d77ed7af237cfebae0034dff5c4182e1845e6fbef29ae4c78186c4b4a7dbf236037a04120783aa30ba74 + version: 17.0.4 + resolution: "@types/node@npm:17.0.4" + checksum: 92e6a25fea2314cd34e81962bd07c8b79b92cae04d84a0336a8c49a2b8aa4c34ff8cb428baeac2022daf597809bd3b7987c624b07a91c4d01b6230f82c293190 languageName: node linkType: hard @@ -564,9 +553,9 @@ __metadata: linkType: hard "@types/validator@npm:^13.7.0": - version: 13.7.0 - resolution: "@types/validator@npm:13.7.0" - checksum: 93b1ea1c24efc851db72f86da8e8cb85c16790ae9c6f8edc351048843224f3a2d3596fb8b207443d64300b88638d079f7da9b91a0d9b4426ef7f5f95189b7e85 + version: 13.7.1 + resolution: "@types/validator@npm:13.7.1" + checksum: 810649a23bc46928c85d933be73b106bacb651a412c4172e30a70e84c63cada595d391042274b16e1c05b391af3a4f157b360890fbc5462ee347275222d59b64 languageName: node linkType: hard @@ -1189,11 +1178,11 @@ __metadata: linkType: hard "discord-akairo@npm:@notenoughupdates/discord-akairo@dev": - version: 9.0.10-dev.1640130180.3e8fb1a - resolution: "@notenoughupdates/discord-akairo@npm:9.0.10-dev.1640130180.3e8fb1a" + version: 9.0.10-dev.1640462480.67bad80 + resolution: "@notenoughupdates/discord-akairo@npm:9.0.10-dev.1640462480.67bad80" dependencies: source-map-support: ^0.5.21 - checksum: b637b5bc90f06f40801ad775ed3d94fe3404bfae7e717877552f7fe1e62cc0f100fbacda31733f6a46dab6d66955eba85e3d6df62f82858b363a1127614e3dd5 + checksum: cf03a768d33d74c6501aba4462c6c3f727a4e164643a41fca0256dfae706d28a104471c3292c919c9977b5c9e2f1aa6a92c918c8872f375fb4e524b1e593d8b8 languageName: node linkType: hard @@ -1204,27 +1193,20 @@ __metadata: languageName: node linkType: hard -"discord-api-types@npm:^0.24.0": - version: 0.24.0 - resolution: "discord-api-types@npm:0.24.0" - checksum: b1a17cb3be4ade974193cedb92fb37e51ba8f1832dfe3ec0d188ba41f255f3dab8359c9d618d32469f1e648126258f9d6ed8828dc1cb50b74d9fd9d875f3390a - languageName: node - linkType: hard - "discord.js@npm:@notenoughupdates/discord.js@dev": - version: 13.4.0-dev.1640088545.d9880cf - resolution: "@notenoughupdates/discord.js@npm:13.4.0-dev.1640088545.d9880cf" + version: 13.5.0-dev.1640476945.bfe6e5f + resolution: "@notenoughupdates/discord.js@npm:13.5.0-dev.1640476945.bfe6e5f" dependencies: - "@discordjs/builders": ^0.9.0 - "@discordjs/collection": ^0.3.2 - "@discordjs/form-data": ^3.0.1 + "@discordjs/builders": ^0.10.0 + "@discordjs/collection": ^0.4.0 "@sapphire/async-queue": ^1.1.9 "@types/node-fetch": ^2.5.12 "@types/ws": ^8.2.2 discord-api-types: ^0.25.2 + form-data: ^4.0.0 node-fetch: ^2.6.1 - ws: ^8.3.0 - checksum: 0327997ccc235301ef801a89e167d80ca2e1da40be91a5bf71f88c4a73ec49e1a734f17bb5453215e3404b2af83983d49ca4c310eff13c539e3de563f19c2b81 + ws: ^8.4.0 + checksum: 782fcb3f0103bc2228fd0a046cf1bb9d3081ebaf9432aa23f94bde269d7cc36644503672c894d6de0bfca2d4101d43e2cba393e30b1cec55df3dae99917a132b languageName: node linkType: hard @@ -1570,6 +1552,17 @@ __metadata: languageName: node linkType: hard +"form-data@npm:^4.0.0": + version: 4.0.0 + resolution: "form-data@npm:4.0.0" + dependencies: + asynckit: ^0.4.0 + combined-stream: ^1.0.8 + mime-types: ^2.1.12 + checksum: 01135bf8675f9d5c61ff18e2e2932f719ca4de964e3be90ef4c36aacfc7b9cb2fceb5eca0b7e0190e3383fe51c5b37f4cb80b62ca06a99aaabfcfd6ac7c9328c + languageName: node + linkType: hard + "fraction.js@npm:^4.1.1": version: 4.1.2 resolution: "fraction.js@npm:4.1.2" @@ -1601,9 +1594,9 @@ __metadata: linkType: hard "fuse.js@npm:^6.4.6": - version: 6.4.6 - resolution: "fuse.js@npm:6.4.6" - checksum: 012dfacdc9a3c065d05d031b4eaaad7dd460f66dca100fcef182ed4e70a6b763b6753071702589d0b28def34b48ad077f01426d4ac1d75ef374564f6baa943fd + version: 6.5.3 + resolution: "fuse.js@npm:6.5.3" + checksum: f7c14f4422000e7f7e3515c66f7cefdfc38adec4cf380097f4146a201ea438af60b67dc5849b3c0de0115ce3f9bc5afad6fc6570c08dcfcef5bf6e95eb8e6d6f languageName: node linkType: hard @@ -2091,8 +2084,8 @@ __metadata: linkType: hard "mathjs@npm:^10.0.0": - version: 10.0.0 - resolution: "mathjs@npm:10.0.0" + version: 10.0.1 + resolution: "mathjs@npm:10.0.1" dependencies: "@babel/runtime": ^7.16.0 complex.js: ^2.0.15 @@ -2105,7 +2098,7 @@ __metadata: typed-function: ^2.0.0 bin: mathjs: bin/cli.js - checksum: 86f4f45804bd799182d5fe54c93a8cc88f1a03a3ab73a9851bc520981310423ec379421b0fb79e50c0a0c22c64cbcf1dcac0636eeaa1b65577366a1c55f27da8 + checksum: 0665088f28e420d4025e1f8c7413b3735184816496dd57487c7121eb788a056186a960bb78ec282eda59e860a214af70ff5e54783ab0600acdcd35ab841b1a42 languageName: node linkType: hard @@ -3283,7 +3276,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.3.0": +"ws@npm:^8.4.0": version: 8.4.0 resolution: "ws@npm:8.4.0" peerDependencies: |