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