diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000..9123a5e2 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,26 @@ +name: 'publish' + +on: + push: + branches: [main] + +jobs: + release: + name: 🚀 publish + runs-on: ubuntu-latest + steps: + - name: 📚 checkout + uses: actions/checkout@v3 + - name: 🟢 node + uses: actions/setup-node@v2 + with: + node-version: 16 + registry-url: https://registry.npmjs.org + - name: 🍳 prepare + run: | + npm install + npm run build + - name: 🚚 publish + run: npm publish + env: + NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}} diff --git a/CHANGELOG.md b/CHANGELOG.md index 36522c62..5ceb7839 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,35 +4,46 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## [0.0.9] - 2023-08-09 + +### Added + +- Support for developer role IDs +- Ability to skip built-in validations by setting `skipBuiltInValidations` to true inside the `CommandKit` constructor + +### Changed + +- Change validations to run custom user validations first, then CommandKit's built-in validations. + ## [0.0.8] - 2023-07-03 ### Added -- Support for nested files inside of each event folder. +- Support for nested files inside of each event folder. ## [0.0.7] - 2023-07-02 ### Changed -- Give validation functions access to the full command object (commandObj) excluding the run function (as that is handled by the command handler), as opposed to just the `data` and `options` properties. +- Give validation functions access to the full command object (commandObj) excluding the run function (as that is handled by the command handler), as opposed to just the `data` and `options` properties. ## [0.0.6] - 2023-07-02 ### Fixed -- Fixed a bug where wrong event names were being registered on Windows. +- Fixed a bug where wrong event names were being registered on Windows. ## [0.0.5] - 2023-07-02 ### Added -- Ability to automatically update application commands (guilds and global) when there's changes to the description or number of options (slash commands only). +- Ability to automatically update application commands (guilds and global) when there's changes to the description or number of options (slash commands only). ## [0.0.4] - 2023-07-01 ### Updated -- Update package.json with new URLs, scripts, and version +- Update package.json with new URLs, scripts, and version ## [0.0.3] - 2023-07-01 diff --git a/README.md b/README.md index 2b0d1cdc..476678ff 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ # CommandKit -CommandKit is a library that makes it easy to handle commands (+validations), and events in your Discord.js projects. +CommandKit is a library that makes it easy to handle commands (+ validations), and events in your Discord.js projects. -_Tested with Discord.js version `v14.11.0`_ +**Supports Discord.js version 14** # Features -- Very beginner friendly 🚀 -- Support for slash and context menu commands ✅ -- Automatic command registration, edits, and deletion 🤖 -- Supports multiple development servers 🤝 -- Supports multiple users as bot developers 👥 -- Object oriented 💻 +- Very beginner friendly 🚀 +- Support for slash and context menu commands ✅ +- Automatic command registration, edits, and deletion 🤖 +- Supports multiple development servers 🤝 +- Supports multiple users as bot developers 👥 +- Object oriented 💻 # Documentation @@ -37,38 +37,46 @@ yarn add commandkit # Usage -This is a simple overview of how to set up this library with all the options. - -**It's highly recommended you check out the [documentation](https://commandkit.underctrl.io) to fully understand how to work with this library.** +This is a simple overview of how to set up this library with all the options. You can read more in the [full documentation](https://commandkit.underctrl.io) ```js // index.js -const { Client, IntentsBitField } = require('discord.js'); +const { Client, GatewayIntentBits } = require('discord.js'); const { CommandKit } = require('commandkit'); const path = require('path'); const client = new Client({ - intents: [IntentsBitField.Flags.Guilds], + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], }); new CommandKit({ - // Your discord.js client object - client, + // Your discord.js client object + client, + + // Path to the commands folder + commandsPath: path.join(__dirname, 'commands'), + + // Path to the events folder + eventsPath: path.join(__dirname, 'events'), - // Path to the commands folder - commandsPath: path.join(__dirname, 'commands'), + // Path to the validations folder (only valid if "commandsPath" was provided) + validationsPath: path.join(__dirname, 'validations'), - // Path to the events folder - eventsPath: path.join(__dirname, 'events'), + // Array of development server IDs (used to register and run devOnly commands) + devGuildIds: ['DEV_SERVER_ID_1', 'DEV_SERVER_ID_2'], - // Path to the validations folder (only valid if "commandsPath" was provided) - validationsPath: path.join(__dirname, 'validations'), + // Array of developer user IDs (used for devOnly commands) + devUserIds: ['DEV_USER_ID_1', 'DEV_USER_ID_2'], - // Array of development server IDs (used to register and run devOnly commands) - devGuildIds: ['DEV_SERVER_ID_1', 'DEV_SERVER_ID_2'], + // Array of developer role IDs (used for devOnly commands) + devRoleIds: ['DEV_ROLE_ID_1', 'DEV_ROLE_ID_2'], - // Array of developer user IDs (used for devOnly commands) - devUserIds: ['DEV_USER_ID_1', 'DEV_USER_ID_2'], + // A property that disables CommandKit's built-in validations + skipBuiltInValidations: true, }); client.login('YOUR_TOKEN_HERE'); diff --git a/package.json b/package.json index fb4042f3..18dd0db0 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,25 @@ { - "name": "commandkit", - "version": "0.0.8", - "main": "dist/index.js", - "license": "MIT", - "scripts": { - "build": "tsc" - }, - "repository": { - "type": "git", - "url": "https://github.com/notunderctrl/commandkit" - }, - "homepage": "https://commandkit.underctrl.io", - "keywords": [ - "discord.js", - "command handler", - "event handler", - "command validations" - ], - "dependencies": {}, - "devDependencies": { - "discord.js": "^14.11.0", - "tsc": "^2.0.4" - } + "name": "commandkit", + "version": "0.0.9", + "main": "dist/index.js", + "license": "MIT", + "scripts": { + "build": "tsc" + }, + "repository": { + "type": "git", + "url": "https://github.com/notunderctrl/commandkit" + }, + "homepage": "https://commandkit.underctrl.io", + "keywords": [ + "discord.js", + "command handler", + "event handler", + "command validations" + ], + "dependencies": {}, + "devDependencies": { + "discord.js": "^14.12.1", + "typescript": "^5.1.6" + } } diff --git a/src/CommandKit.ts b/src/CommandKit.ts index f91b2e8b..9a65ba95 100644 --- a/src/CommandKit.ts +++ b/src/CommandKit.ts @@ -2,63 +2,65 @@ import { CommandHandler, EventHandler, ValidationHandler } from './handlers'; import { CommandKitData, CommandKitOptions } from '../typings'; export class CommandKit { - private _data: CommandKitData; + private _data: CommandKitData; - constructor({ ...options }: CommandKitOptions) { - if (!options.client) { - throw new Error('"client" is required when instantiating CommandKit.'); - } - - if (options.validationsPath && !options.commandsPath) { - throw new Error('"commandsPath" is required when "validationsPath" is set.'); - } + constructor({ ...options }: CommandKitOptions) { + if (!options.client) { + throw new Error('"client" is required when instantiating CommandKit.'); + } - this._data = { - ...options, - commands: [], - }; + if (options.validationsPath && !options.commandsPath) { + throw new Error('"commandsPath" is required when "validationsPath" is set.'); + } - this._init(); - } + this._data = { + ...options, + commands: [], + }; - private _init() { - // Event handler - if (this._data.eventsPath) { - new EventHandler({ - client: this._data.client, - eventsPath: this._data.eventsPath, - }); + this._init(); } - // Validation handler - let validationFunctions: Function[] = []; + private _init() { + // Event handler + if (this._data.eventsPath) { + new EventHandler({ + client: this._data.client, + eventsPath: this._data.eventsPath, + }); + } - if (this._data.validationsPath) { - const validationHandler = new ValidationHandler({ - validationsPath: this._data.validationsPath, - }); + // Validation handler + let validationFunctions: Function[] = []; - validationFunctions = validationHandler.getValidations(); - } + if (this._data.validationsPath) { + const validationHandler = new ValidationHandler({ + validationsPath: this._data.validationsPath, + }); + + validationHandler.getValidations().forEach((v) => validationFunctions.push(v)); + } - // Command handler - if (this._data.commandsPath) { - const commandHandler = new CommandHandler({ - client: this._data.client, - commandsPath: this._data.commandsPath, - devGuildIds: this._data.devGuildIds || [], - devUserIds: this._data.devUserIds || [], - validations: validationFunctions, - }); + // Command handler + if (this._data.commandsPath) { + const commandHandler = new CommandHandler({ + client: this._data.client, + commandsPath: this._data.commandsPath, + devGuildIds: this._data.devGuildIds || [], + devUserIds: this._data.devUserIds || [], + devRoleIds: this._data.devRoleIds || [], + customValidations: validationFunctions, + skipBuiltInValidations: this._data.skipBuiltInValidations || false, + }); - this._data.commands = commandHandler.getCommands(); + this._data.commands = commandHandler.getCommands(); + } } - } - get commands() { - return this._data.commands.map((cmd) => { - const { run, ...command } = cmd; - return command; - }); - } + get commands() { + return this._data.commands.map((cmd) => { + const { run, ...command } = cmd; + return command; + }); + } } diff --git a/src/handlers/command-handler/CommandHandler.ts b/src/handlers/command-handler/CommandHandler.ts index 99af61b5..f3db3f0b 100644 --- a/src/handlers/command-handler/CommandHandler.ts +++ b/src/handlers/command-handler/CommandHandler.ts @@ -1,335 +1,77 @@ -import { Guild, GuildApplicationCommandManager } from 'discord.js'; -import { getFilePaths } from '../../utils/get-paths'; -import { CommandHandlerData, CommandHandlerOptions } from './typings'; +import { BuiltInValidation, CommandHandlerData, CommandHandlerOptions } from './typings'; import { ContextCommandObject, SlashCommandObject } from '../../../typings'; +import { getFilePaths } from '../../utils/get-paths'; +import registerCommands from './functions/registerCommands'; +import handleCommands from './functions/handleCommands'; +import path from 'path'; export class CommandHandler { - _data: CommandHandlerData; - - constructor({ ...options }: CommandHandlerOptions) { - this._data = { - ...options, - commands: [], - }; - - this._init(); - } - - _init() { - this._buildCommands(); - this._registerCommands(); - this._handleCommands(); - } - - _buildCommands() { - const commandFilePaths = getFilePaths(this._data.commandsPath, true).filter( - (path) => path.endsWith('.js') || path.endsWith('.ts') - ); - - for (const commandFilePath of commandFilePaths) { - const commandObj: SlashCommandObject | ContextCommandObject = require(commandFilePath); - - if (!commandObj.data) { - console.log(`⏩ Ignoring: Command ${commandFilePath} does not export "data".`); - continue; - } + _data: CommandHandlerData; - if (!commandObj.run) { - console.log(`⏩ Ignoring: Command ${commandFilePath} does not export "run".`); - continue; - } + constructor({ ...options }: CommandHandlerOptions) { + this._data = { + ...options, + builtInValidations: [], + commands: [], + }; - this._data.commands.push(commandObj); + this._init(); } - } - - _registerCommands() { - const client = this._data.client; - const commands = this._data.commands; - - client.once('ready', async () => { - const devGuilds: Guild[] = []; - - for (const devGuildId of this._data.devGuildIds) { - const guild = client.guilds.cache.get(devGuildId); - - if (!guild) { - console.log( - `⏩ Ignoring: Guild ${devGuildId} does not exist or client isn't in this guild.` - ); - continue; - } - - devGuilds.push(guild); - } - - const appCommands = client.application?.commands; - await appCommands?.fetch(); - const devGuildCommands: GuildApplicationCommandManager[] = []; - - for (const guild of devGuilds) { - const guildCommands = guild.commands; - await guildCommands?.fetch(); - devGuildCommands.push(guildCommands); - } - - for (const command of commands) { - // - if (command.options?.deleted) { - const targetCommand = appCommands?.cache.find((cmd) => cmd.name === command.data.name); - - if (!targetCommand) { - console.log( - `⏩ Ignoring: Command "${command.data.name}" is globally marked as deleted.` - ); - } else { - targetCommand.delete().then(() => { - console.log(`🚮 Deleted command "${command.data.name}" globally.`); - }); - } - - for (const guildCommands of devGuildCommands) { - const targetCommand = guildCommands.cache.find((cmd) => cmd.name === command.data.name); - - if (!targetCommand) { - console.log( - `⏩ Ignoring: Command "${command.data.name}" is marked as deleted for ${guildCommands.guild.name}.` - ); - } else { - targetCommand.delete().then(() => { - console.log( - `🚮 Deleted command "${command.data.name}" in ${guildCommands.guild.name}.` - ); - }); - } - } - - continue; - } - - // - let commandData = command.data; - let editedCommand = false; - - (() => { - // global - const appGlobalCommand = appCommands?.cache.find((cmd) => cmd.name === command.data.name); + _init() { + this._buildCommands(); + this._buildValidations(); + this._registerCommands(); + this._handleCommands(); + } - if (appGlobalCommand) { - const commandsAreDifferent = this._areSlashCommandsDifferent( - appGlobalCommand, - commandData - ); + _buildCommands() { + const commandFilePaths = getFilePaths(this._data.commandsPath, true).filter( + (path) => path.endsWith('.js') || path.endsWith('.ts') + ); - if (commandsAreDifferent) { - appGlobalCommand - .edit(commandData) - .then(() => { - console.log(`✅ Edited command "${commandData.name}" globally.`); - }) - .catch((error) => { - console.log(`❌ Failed to edit command "${commandData.name}" globally.`); - console.error(error); - }); + for (const commandFilePath of commandFilePaths) { + const commandObj: SlashCommandObject | ContextCommandObject = require(commandFilePath); - editedCommand = true; + if (!commandObj.data) { + console.log(`⏩ Ignoring: Command ${commandFilePath} does not export "data".`); + continue; } - } - - // guilds - for (const guildCommands of devGuildCommands) { - const appGuildCommand = guildCommands.cache.find( - (cmd) => cmd.name === commandData.name - ); - - if (appGuildCommand) { - const commandsAreDifferent = this._areSlashCommandsDifferent( - appGuildCommand, - commandData - ); - - if (commandsAreDifferent) { - appGuildCommand - .edit(commandData) - .then(() => { - console.log( - `✅ Edited command "${commandData.name}" in ${guildCommands.guild.name}.` - ); - }) - .catch((error) => { - console.log( - `❌ Failed to edit command "${commandData.name}" in ${guildCommands.guild.name}.` - ); - console.error(error); - }); - editedCommand = true; - } + if (!commandObj.run) { + console.log(`⏩ Ignoring: Command ${commandFilePath} does not export "run".`); + continue; } - } - })(); - if (editedCommand) continue; - - // - // guild-based command registration - if (command.options?.devOnly) { - if (!devGuilds.length) { - console.log( - `⏩ Ignoring: Cannot register command "${command.data.name}" as no valid "devGuildIds" were provided.` - ); - continue; - } - - for (const guild of devGuilds) { - const cmdExists = guild.commands.cache.some((cmd) => cmd.name === command.data.name); - if (cmdExists) continue; - - guild?.commands - .create(command.data) - .then(() => { - console.log(`✅ Registered command "${command.data.name}" in ${guild.name}.`); - }) - .catch((error) => { - console.log( - `❌ Failed to register command "${command.data.name}" in ${guild.name}.` - ); - console.error(error); - }); - } - } - // global command registration - else { - const cmdExists = appCommands?.cache.some((cmd) => cmd.name === command.data.name); - if (cmdExists) continue; - - appCommands - ?.create(command.data) - .then(() => { - console.log(`✅ Registered command "${command.data.name}" globally.`); - }) - .catch((error) => { - console.log(`❌ Failed to register command "${command.data.name}" globally.`); - console.error(error); - }); + this._data.commands.push(commandObj); } - } - }); - } - - _handleCommands() { - const client = this._data.client; - - client.on('interactionCreate', async (interaction) => { - if (!interaction.isChatInputCommand() && !interaction.isContextMenuCommand()) return; - - const targetCommand = this._data.commands.find( - (cmd) => cmd.data.name === interaction.commandName - ); - - if (!targetCommand) return; - - // Options validation - // options.guildOnly - if (targetCommand.options?.guildOnly && !interaction.inGuild()) { - interaction.reply({ - content: '❌ This command can only be used inside a server.', - ephemeral: true, - }); - return; - } - - // options.devOnly - if (targetCommand.options?.devOnly) { - const isDevUser = this._data.devUserIds.includes(interaction.user.id); - - if (!isDevUser) { - interaction.reply({ - content: '❌ This command can only be used by developers.', - ephemeral: true, - }); - return; - } - } - - // options.userPermissions - const memberPermissions = interaction.memberPermissions; - if (targetCommand.options?.userPermissions && memberPermissions) { - for (const permission of targetCommand.options.userPermissions) { - const hasPermission = memberPermissions.has(permission); - - if (!hasPermission) { - interaction.reply({ - content: `❌ You do not have enough permission to run this command. Required permission: \`${permission}\``, - ephemeral: true, - }); - return; - } - } - } - - // options.botPermissions - const botMember = interaction.guild?.members.me; - - if (targetCommand.options?.botPermissions && botMember) { - for (const permission of targetCommand.options.botPermissions) { - const hasPermission = botMember.permissions.has(permission); - - if (!hasPermission) { - interaction.reply({ - content: `❌ I do not have enough permission to execute this command. Required permission: \`${permission}\``, - ephemeral: true, - }); - return; - } - } - } - - // Run user validation functions - const validationFunctions = this._data.validations; - - const { data, options, run, ...rest } = targetCommand; + } - const commandObj = { - data: targetCommand.data, - options: targetCommand.options, - ...rest, - }; + _buildValidations() { + const validationFilePaths = getFilePaths(path.join(__dirname, 'validations'), true).filter( + (path) => path.endsWith('.js') + ); - let canRun = true; + for (const validationFilePath of validationFilePaths) { + const validationFunction: Function = require(validationFilePath); - for (const validationFunction of validationFunctions) { - const stopValidationLoop = await validationFunction({ interaction, client, commandObj }); + if (typeof validationFunction !== 'function') { + continue; + } - if (stopValidationLoop) { - canRun = false; - break; + this._data.builtInValidations.push(validationFunction as BuiltInValidation); } - } - - if (canRun) { - targetCommand.run({ interaction, client }); - } - }); - } - - _areSlashCommandsDifferent(appCommand: any, localCommand: any) { - if (!appCommand.options) appCommand.options = []; - if (!localCommand.options) localCommand.options = []; + } - if (!appCommand.description) appCommand.description = ''; - if (!localCommand.description) localCommand.description = ''; + _registerCommands() { + registerCommands(this); + } - if ( - localCommand.description !== appCommand.description || - localCommand.options.length !== appCommand.options.length - ) { - return true; + _handleCommands() { + handleCommands(this); } - } - getCommands() { - return this._data.commands; - } + getCommands() { + return this._data.commands; + } } diff --git a/src/handlers/command-handler/functions/handleCommands.ts b/src/handlers/command-handler/functions/handleCommands.ts new file mode 100644 index 00000000..d0d983e7 --- /dev/null +++ b/src/handlers/command-handler/functions/handleCommands.ts @@ -0,0 +1,60 @@ +import { CommandHandler } from '../CommandHandler'; + +export default function handleCommands(commandHandler: CommandHandler) { + const client = commandHandler._data.client; + + client.on('interactionCreate', async (interaction) => { + if (!interaction.isChatInputCommand() && !interaction.isContextMenuCommand()) return; + + const targetCommand = commandHandler._data.commands.find( + (cmd) => cmd.data.name === interaction.commandName + ); + + if (!targetCommand) return; + + const { data, options, run, ...rest } = targetCommand; + + const commandObj = { + data: targetCommand.data, + options: targetCommand.options, + ...rest, + }; + + let canRun = true; + + for (const validationFunction of commandHandler._data.customValidations) { + const stopValidationLoop = await validationFunction({ + interaction, + client, + commandObj, + }); + + if (stopValidationLoop) { + canRun = false; + break; + } + } + + if (!canRun) return; + + // If custom validations pass and !skipBuiltInValidations, run built-in CommandKit validation functions + if (!commandHandler._data.skipBuiltInValidations) { + for (const validation of commandHandler._data.builtInValidations) { + const stopValidationLoop = validation({ + targetCommand, + interaction, + handlerData: commandHandler._data, + }); + + if (stopValidationLoop) { + canRun = false; + break; + } + } + } + + if (!canRun) return; + + targetCommand.run({ interaction, client }); + }); +} diff --git a/src/handlers/command-handler/functions/registerCommands.ts b/src/handlers/command-handler/functions/registerCommands.ts new file mode 100644 index 00000000..c222c656 --- /dev/null +++ b/src/handlers/command-handler/functions/registerCommands.ts @@ -0,0 +1,191 @@ +import { Guild, GuildApplicationCommandManager } from 'discord.js'; +import { CommandHandler } from '../CommandHandler'; +import areSlashCommandsDifferent from '../utils/areSlashCommandsDifferent'; + +export default function registerCommands(commandHandler: CommandHandler) { + const client = commandHandler._data.client; + const devGuildIds = commandHandler._data.devGuildIds; + const commands = commandHandler._data.commands; + + client.once('ready', async () => { + const devGuilds: Guild[] = []; + + for (const devGuildId of devGuildIds) { + const guild = client.guilds.cache.get(devGuildId); + + if (!guild) { + console.log( + `⏩ Ignoring: Guild ${devGuildId} does not exist or client isn't in this guild.` + ); + continue; + } + + devGuilds.push(guild); + } + + const appCommands = client.application?.commands; + await appCommands?.fetch(); + + const devGuildCommands: GuildApplicationCommandManager[] = []; + + for (const guild of devGuilds) { + const guildCommands = guild.commands; + await guildCommands?.fetch(); + devGuildCommands.push(guildCommands); + } + + for (const command of commands) { + // + if (command.options?.deleted) { + const targetCommand = appCommands?.cache.find( + (cmd) => cmd.name === command.data.name + ); + + if (!targetCommand) { + console.log( + `⏩ Ignoring: Command "${command.data.name}" is globally marked as deleted.` + ); + } else { + targetCommand.delete().then(() => { + console.log(`🚮 Deleted command "${command.data.name}" globally.`); + }); + } + + for (const guildCommands of devGuildCommands) { + const targetCommand = guildCommands.cache.find( + (cmd) => cmd.name === command.data.name + ); + + if (!targetCommand) { + console.log( + `⏩ Ignoring: Command "${command.data.name}" is marked as deleted for ${guildCommands.guild.name}.` + ); + } else { + targetCommand.delete().then(() => { + console.log( + `🚮 Deleted command "${command.data.name}" in ${guildCommands.guild.name}.` + ); + }); + } + } + + continue; + } + + // + let commandData = command.data; + let editedCommand = false; + + // Edit command globally + const appGlobalCommand = appCommands?.cache.find( + (cmd) => cmd.name === command.data.name + ); + + if (appGlobalCommand) { + const commandsAreDifferent = areSlashCommandsDifferent( + appGlobalCommand, + commandData + ); + + if (commandsAreDifferent) { + appGlobalCommand + .edit(commandData) + .then(() => { + console.log(`✅ Edited command "${commandData.name}" globally.`); + }) + .catch((error) => { + console.log( + `❌ Failed to edit command "${commandData.name}" globally.` + ); + console.error(error); + }); + + editedCommand = true; + } + } + + // Edit command in a specific guild + for (const guildCommands of devGuildCommands) { + const appGuildCommand = guildCommands.cache.find( + (cmd) => cmd.name === commandData.name + ); + + if (appGuildCommand) { + const commandsAreDifferent = areSlashCommandsDifferent( + appGuildCommand, + commandData + ); + + if (commandsAreDifferent) { + appGuildCommand + .edit(commandData) + .then(() => { + console.log( + `✅ Edited command "${commandData.name}" in ${guildCommands.guild.name}.` + ); + }) + .catch((error) => { + console.log( + `❌ Failed to edit command "${commandData.name}" in ${guildCommands.guild.name}.` + ); + console.error(error); + }); + + editedCommand = true; + } + } + } + + if (editedCommand) continue; + + // + // Register command in a specific guild + if (command.options?.devOnly) { + if (!devGuilds.length) { + console.log( + `⏩ Ignoring: Cannot register command "${command.data.name}" as no valid "devGuildIds" were provided.` + ); + continue; + } + + for (const guild of devGuilds) { + const cmdExists = guild.commands.cache.some( + (cmd) => cmd.name === command.data.name + ); + if (cmdExists) continue; + + guild?.commands + .create(command.data) + .then(() => { + console.log( + `✅ Registered command "${command.data.name}" in ${guild.name}.` + ); + }) + .catch((error) => { + console.log( + `❌ Failed to register command "${command.data.name}" in ${guild.name}.` + ); + console.error(error); + }); + } + } + // Register command globally + else { + const cmdExists = appCommands?.cache.some((cmd) => cmd.name === command.data.name); + if (cmdExists) continue; + + appCommands + ?.create(command.data) + .then(() => { + console.log(`✅ Registered command "${command.data.name}" globally.`); + }) + .catch((error) => { + console.log( + `❌ Failed to register command "${command.data.name}" globally.` + ); + console.error(error); + }); + } + } + }); +} diff --git a/src/handlers/command-handler/typings.d.ts b/src/handlers/command-handler/typings.d.ts index 037cacda..b2dc64c9 100644 --- a/src/handlers/command-handler/typings.d.ts +++ b/src/handlers/command-handler/typings.d.ts @@ -1,14 +1,25 @@ -import { Client } from 'discord.js'; +import { ChatInputCommandInteraction, Client, ContextMenuCommandInteraction } from 'discord.js'; import { ContextCommandObject, SlashCommandObject } from '../../../typings'; export interface CommandHandlerOptions { - client: Client; - commandsPath: string; - devGuildIds: string[]; - devUserIds: string[]; - validations: Function[]; + client: Client; + commandsPath: string; + devGuildIds: string[]; + devUserIds: string[]; + devRoleIds: string[]; + customValidations: Function[]; + skipBuiltInValidations: boolean; } export interface CommandHandlerData extends CommandHandlerOptions { - commands: Array; + commands: Array; + builtInValidations: Array; } + +export interface BuiltInValidationParams { + targetCommand: SlashCommandObject | ContextCommandObject; + interaction: ChatInputCommandInteraction | ContextMenuCommandInteraction; + handlerData: CommandHandlerData; +} + +export type BuiltInValidation = ({}: BuiltInValidationParams) => boolean | void; diff --git a/src/handlers/command-handler/utils/areSlashCommandsDifferent.ts b/src/handlers/command-handler/utils/areSlashCommandsDifferent.ts new file mode 100644 index 00000000..b13c2574 --- /dev/null +++ b/src/handlers/command-handler/utils/areSlashCommandsDifferent.ts @@ -0,0 +1,14 @@ +export default function areSlashCommandsDifferent(appCommand: any, localCommand: any) { + if (!appCommand.options) appCommand.options = []; + if (!localCommand.options) localCommand.options = []; + + if (!appCommand.description) appCommand.description = ''; + if (!localCommand.description) localCommand.description = ''; + + if ( + localCommand.description !== appCommand.description || + localCommand.options.length !== appCommand.options.length + ) { + return true; + } +} diff --git a/src/handlers/command-handler/validations/botPermissions.ts b/src/handlers/command-handler/validations/botPermissions.ts new file mode 100644 index 00000000..f63b0dba --- /dev/null +++ b/src/handlers/command-handler/validations/botPermissions.ts @@ -0,0 +1,20 @@ +import { BuiltInValidationParams } from '../typings'; + +module.exports = ({ interaction, targetCommand }: BuiltInValidationParams) => { + const botMember = interaction.guild?.members.me; + + if (targetCommand.options?.botPermissions && botMember) { + for (const permission of targetCommand.options.botPermissions) { + const hasPermission = botMember.permissions.has(permission); + + if (!hasPermission) { + interaction.reply({ + content: `❌ I do not have enough permission to execute this command. Required permission: \`${permission}\``, + ephemeral: true, + }); + + return true; + } + } + } +}; diff --git a/src/handlers/command-handler/validations/devOnly.ts b/src/handlers/command-handler/validations/devOnly.ts new file mode 100644 index 00000000..994ceedb --- /dev/null +++ b/src/handlers/command-handler/validations/devOnly.ts @@ -0,0 +1,36 @@ +import { BuiltInValidationParams } from '../typings'; + +module.exports = ({ interaction, targetCommand, handlerData }: BuiltInValidationParams) => { + if (targetCommand.options?.devOnly) { + if (interaction.inGuild() && !handlerData.devGuildIds.includes(interaction.guildId)) { + interaction.reply({ + content: '❌ This command can only be used inside development servers.', + ephemeral: true, + }); + + return true; + } + + const guildMember = interaction.guild?.members.cache.get(interaction.user.id); + const memberRoles = guildMember?.roles.cache; + + let hasDevRole = false; + + memberRoles?.forEach((role) => { + if (handlerData.devRoleIds?.includes(role.id)) { + hasDevRole = true; + } + }); + + const isDevUser = handlerData.devUserIds.includes(interaction.user.id) || hasDevRole; + + if (!isDevUser) { + interaction.reply({ + content: '❌ This command can only be used by developers.', + ephemeral: true, + }); + + return true; + } + } +}; diff --git a/src/handlers/command-handler/validations/guildOnly.ts b/src/handlers/command-handler/validations/guildOnly.ts new file mode 100644 index 00000000..bb5ebff1 --- /dev/null +++ b/src/handlers/command-handler/validations/guildOnly.ts @@ -0,0 +1,12 @@ +import { BuiltInValidationParams } from '../typings'; + +module.exports = ({ interaction, targetCommand }: BuiltInValidationParams) => { + if (targetCommand.options?.guildOnly && !interaction.inGuild()) { + interaction.reply({ + content: '❌ This command can only be used inside a server.', + ephemeral: true, + }); + + return true; + } +}; diff --git a/src/handlers/command-handler/validations/userPermissions.ts b/src/handlers/command-handler/validations/userPermissions.ts new file mode 100644 index 00000000..88582bf6 --- /dev/null +++ b/src/handlers/command-handler/validations/userPermissions.ts @@ -0,0 +1,20 @@ +import { BuiltInValidationParams } from '../typings'; + +module.exports = ({ interaction, targetCommand }: BuiltInValidationParams) => { + const memberPermissions = interaction.memberPermissions; + + if (targetCommand.options?.userPermissions && memberPermissions) { + for (const permission of targetCommand.options.userPermissions) { + const hasPermission = memberPermissions.has(permission); + + if (!hasPermission) { + interaction.reply({ + content: `❌ You do not have enough permission to run this command. Required permission: \`${permission}\``, + ephemeral: true, + }); + + return true; + } + } + } +}; diff --git a/src/handlers/event-handler/EventHandler.ts b/src/handlers/event-handler/EventHandler.ts index 85293efa..7e79cd9f 100644 --- a/src/handlers/event-handler/EventHandler.ts +++ b/src/handlers/event-handler/EventHandler.ts @@ -2,65 +2,65 @@ import { getFilePaths, getFolderPaths } from '../../utils/get-paths'; import { EventHandlerOptions, EventHandlerData } from './typings'; export class EventHandler { - _data: EventHandlerData; + _data: EventHandlerData; - constructor({ ...options }: EventHandlerOptions) { - this._data = { - ...options, - events: [], - }; + constructor({ ...options }: EventHandlerOptions) { + this._data = { + ...options, + events: [], + }; - this._buildEvents(); - this._registerEvents(); - } + this._buildEvents(); + this._registerEvents(); + } - _buildEvents() { - const eventFolderPaths = getFolderPaths(this._data.eventsPath); + _buildEvents() { + const eventFolderPaths = getFolderPaths(this._data.eventsPath); - for (const eventFolderPath of eventFolderPaths) { - const eventName = eventFolderPath.replace(/\\/g, '/').split('/').pop() as string; + for (const eventFolderPath of eventFolderPaths) { + const eventName = eventFolderPath.replace(/\\/g, '/').split('/').pop() as string; - const eventFilePaths = getFilePaths(eventFolderPath, true).filter( - (path) => path.endsWith('.js') || path.endsWith('.ts') - ); + const eventFilePaths = getFilePaths(eventFolderPath, true).filter( + (path) => path.endsWith('.js') || path.endsWith('.ts') + ); - const eventObj = { - name: eventName, - functions: [] as Function[], - }; + const eventObj = { + name: eventName, + functions: [] as Function[], + }; - this._data.events.push(eventObj); + this._data.events.push(eventObj); - for (const eventFilePath of eventFilePaths) { - const eventFunction = require(eventFilePath); + for (const eventFilePath of eventFilePaths) { + const eventFunction = require(eventFilePath); - if (typeof eventFunction !== 'function') { - console.log(`Ignoring: Event ${eventFilePath} does not export a function.`); - continue; - } + if (typeof eventFunction !== 'function') { + console.log(`Ignoring: Event ${eventFilePath} does not export a function.`); + continue; + } - eventObj.functions.push(eventFunction); - } + eventObj.functions.push(eventFunction); + } + } } - } - _registerEvents() { - const client = this._data.client; + _registerEvents() { + const client = this._data.client; - for (const eventObj of this._data.events) { - client.on(eventObj.name, async (...params) => { - for (const eventFunction of eventObj.functions) { - const stopEventLoop = await eventFunction(...params, client); + for (const eventObj of this._data.events) { + client.on(eventObj.name, async (...params) => { + for (const eventFunction of eventObj.functions) { + const stopEventLoop = await eventFunction(...params, client); - if (stopEventLoop) { - break; - } + if (stopEventLoop) { + break; + } + } + }); } - }); } - } - getEvents() { - return this._data.events; - } + getEvents() { + return this._data.events; + } } diff --git a/src/handlers/event-handler/typings.d.ts b/src/handlers/event-handler/typings.d.ts index 66b2f462..bb202a12 100644 --- a/src/handlers/event-handler/typings.d.ts +++ b/src/handlers/event-handler/typings.d.ts @@ -1,10 +1,10 @@ import { Client } from 'discord.js'; export interface EventHandlerOptions { - client: Client; - eventsPath: string; + client: Client; + eventsPath: string; } export interface EventHandlerData extends EventHandlerOptions { - events: { name: string; functions: Function[] }[]; + events: { name: string; functions: Function[] }[]; } diff --git a/src/handlers/validation-handler/ValidationHandler.ts b/src/handlers/validation-handler/ValidationHandler.ts index 11e3dce0..62bfe5c8 100644 --- a/src/handlers/validation-handler/ValidationHandler.ts +++ b/src/handlers/validation-handler/ValidationHandler.ts @@ -2,35 +2,37 @@ import { getFilePaths } from '../../utils/get-paths'; import { ValidationHandlerData, ValidationHandlerOptions } from './typings'; export class ValidationHandler { - _data: ValidationHandlerData; + _data: ValidationHandlerData; - constructor({ ...options }: ValidationHandlerOptions) { - this._data = { - ...options, - validations: [], - }; + constructor({ ...options }: ValidationHandlerOptions) { + this._data = { + ...options, + validations: [], + }; - this._buildValidations(); - } + this._buildValidations(); + } - _buildValidations() { - const validationFilePaths = getFilePaths(this._data.validationsPath, true).filter( - (path) => path.endsWith('.js') || path.endsWith('.ts') - ); + _buildValidations() { + const validationFilePaths = getFilePaths(this._data.validationsPath, true).filter( + (path) => path.endsWith('.js') || path.endsWith('.ts') + ); - for (const validationFilePath of validationFilePaths) { - const validationFunction = require(validationFilePath); + for (const validationFilePath of validationFilePaths) { + const validationFunction = require(validationFilePath); - if (typeof validationFunction !== 'function') { - console.log(`Ignoring: Validation ${validationFilePath} does not export a function.`); - continue; - } + if (typeof validationFunction !== 'function') { + console.log( + `Ignoring: Validation ${validationFilePath} does not export a function.` + ); + continue; + } - this._data.validations.push(validationFunction); + this._data.validations.push(validationFunction); + } } - } - getValidations() { - return this._data.validations; - } + getValidations() { + return this._data.validations; + } } diff --git a/src/handlers/validation-handler/typings.d.ts b/src/handlers/validation-handler/typings.d.ts index 4919a173..30545dc3 100644 --- a/src/handlers/validation-handler/typings.d.ts +++ b/src/handlers/validation-handler/typings.d.ts @@ -1,7 +1,7 @@ export interface ValidationHandlerOptions { - validationsPath: string; + validationsPath: string; } export interface ValidationHandlerData extends ValidationHandlerOptions { - validations: Function[]; + validations: Function[]; } diff --git a/src/utils/get-paths.ts b/src/utils/get-paths.ts index f443c29b..8f3c3940 100644 --- a/src/utils/get-paths.ts +++ b/src/utils/get-paths.ts @@ -2,47 +2,47 @@ import path from 'path'; import fs from 'fs'; function getFilePaths(directory: string, nesting?: boolean): string[] { - let filePaths: string[] = []; + let filePaths: string[] = []; - if (!directory) return filePaths; + if (!directory) return filePaths; - const files = fs.readdirSync(directory, { withFileTypes: true }); + const files = fs.readdirSync(directory, { withFileTypes: true }); - for (const file of files) { - const filePath = path.join(directory, file.name); + for (const file of files) { + const filePath = path.join(directory, file.name); - if (file.isFile()) { - filePaths.push(filePath); - } + if (file.isFile()) { + filePaths.push(filePath); + } - if (nesting && file.isDirectory()) { - filePaths = [...filePaths, ...getFilePaths(filePath, true)]; + if (nesting && file.isDirectory()) { + filePaths = [...filePaths, ...getFilePaths(filePath, true)]; + } } - } - return filePaths; + return filePaths; } function getFolderPaths(directory: string, nesting?: boolean): string[] { - let folderPaths: string[] = []; + let folderPaths: string[] = []; - if (!directory) return folderPaths; + if (!directory) return folderPaths; - const folders = fs.readdirSync(directory, { withFileTypes: true }); + const folders = fs.readdirSync(directory, { withFileTypes: true }); - for (const folder of folders) { - const folderPath = path.join(directory, folder.name); + for (const folder of folders) { + const folderPath = path.join(directory, folder.name); - if (folder.isDirectory()) { - folderPaths.push(folderPath); + if (folder.isDirectory()) { + folderPaths.push(folderPath); - if (nesting) { - folderPaths = [...folderPaths, ...getFolderPaths(folderPath, true)]; - } + if (nesting) { + folderPaths = [...folderPaths, ...getFolderPaths(folderPath, true)]; + } + } } - } - return folderPaths; + return folderPaths; } export { getFilePaths, getFolderPaths }; diff --git a/tsconfig.json b/tsconfig.json index 846a962d..7b4f4376 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,14 +1,14 @@ { - "compilerOptions": { - "outDir": "dist", - "strict": true, - "noImplicitAny": true, - "esModuleInterop": true, - "strictNullChecks": true, - "target": "ES2022", - "moduleResolution": "Node", - "module": "CommonJS", - "declaration": true - }, - "include": ["src/**/*"] + "compilerOptions": { + "outDir": "dist", + "strict": true, + "noImplicitAny": true, + "esModuleInterop": true, + "strictNullChecks": true, + "target": "ES2022", + "moduleResolution": "Node", + "module": "CommonJS", + "declaration": true + }, + "include": ["src/**/*"] } diff --git a/typings.d.ts b/typings.d.ts index 5467ad0f..73450637 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -1,61 +1,63 @@ import { - Client, - APIApplicationCommandOption, - ContextMenuCommandType, - Interaction, - PermissionResolvable, - SlashCommandBuilder, - ContextMenuCommandBuilder, + Client, + APIApplicationCommandOption, + ContextMenuCommandType, + Interaction, + PermissionResolvable, + SlashCommandBuilder, + ContextMenuCommandBuilder, } from 'discord.js'; export interface CommandKitOptions { - client: Client; - commandsPath?: string; - eventsPath?: string; - validationsPath?: string; - devGuildIds?: string[]; - devUserIds?: string[]; + client: Client; + commandsPath?: string; + eventsPath?: string; + validationsPath?: string; + devGuildIds?: string[]; + devUserIds?: string[]; + devRoleIds?: string[]; + skipBuiltInValidations?: boolean; } export interface CommandKitData extends CommandKitOptions { - commands: Array; + commands: Array; } export interface SlashCommandObject { - data: - | SlashCommandBuilder - | { - name: string; - name_localizations?: any; - description: string; - dm_permission?: boolean; - options?: APIApplicationCommandOption[]; - }; - options?: { - guildOnly?: boolean; - devOnly?: boolean; - deleted?: boolean; - userPermissions?: PermissionResolvable[]; - botPermissions?: PermissionResolvable[]; - }; - run: ({}: { interaction: Interaction; client: Client }) => void; + data: + | SlashCommandBuilder + | { + name: string; + name_localizations?: any; + description: string; + dm_permission?: boolean; + options?: APIApplicationCommandOption[]; + }; + options?: { + guildOnly?: boolean; + devOnly?: boolean; + deleted?: boolean; + userPermissions?: PermissionResolvable[]; + botPermissions?: PermissionResolvable[]; + }; + run: ({}: { interaction: Interaction; client: Client }) => void; } export interface ContextCommandObject { - data: - | ContextMenuCommandBuilder - | { - name: string; - name_localizations?: any; - type: ContextMenuCommandType; - dm_permission?: boolean; - }; - options?: { - guildOnly?: boolean; - devOnly?: boolean; - deleted?: boolean; - userPermissions?: PermissionResolvable[]; - botPermissions?: PermissionResolvable[]; - }; - run: ({}: { interaction: Interaction; client: Client }) => void; + data: + | ContextMenuCommandBuilder + | { + name: string; + name_localizations?: any; + type: ContextMenuCommandType; + dm_permission?: boolean; + }; + options?: { + guildOnly?: boolean; + devOnly?: boolean; + deleted?: boolean; + userPermissions?: PermissionResolvable[]; + botPermissions?: PermissionResolvable[]; + }; + run: ({}: { interaction: Interaction; client: Client }) => void; } diff --git a/yarn.lock b/yarn.lock index e0ea4968..18ea277e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,23 +2,23 @@ # yarn lockfile v1 -"@discordjs/builders@^1.6.3": - version "1.6.3" - resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-1.6.3.tgz#994b4fe57e77b47096f74bb5a1f664870a930a43" - integrity sha512-CTCh8NqED3iecTNuiz49mwSsrc2iQb4d0MjMdmS/8pb69Y4IlzJ/DIy/p5GFlgOrFbNO2WzMHkWKQSiJ3VNXaw== +"@discordjs/builders@^1.6.4": + version "1.6.4" + resolved "https://registry.yarnpkg.com/@discordjs/builders/-/builders-1.6.4.tgz#d99f4e76684ef9b1c3b9e1c4d0bc17fafb19b960" + integrity sha512-ARFKvmAkLhfkQQiNxqi0YIWqwUExvBRtvdtMFVJXvJoibsGkFrB/DWTf9byU7BTVUfsmW8w7NM55tYXR5S/iSg== dependencies: "@discordjs/formatters" "^0.3.1" - "@discordjs/util" "^0.3.1" - "@sapphire/shapeshift" "^3.8.2" - discord-api-types "^0.37.41" + "@discordjs/util" "^1.0.0" + "@sapphire/shapeshift" "^3.9.2" + discord-api-types "^0.37.50" fast-deep-equal "^3.1.3" ts-mixer "^6.0.3" - tslib "^2.5.0" + tslib "^2.6.1" -"@discordjs/collection@^1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-1.5.1.tgz#bc7ca557838dc29247bf19860426637f103bc383" - integrity sha512-aWEc9DCf3TMDe9iaJoOnO2+JVAjeRNuRxPZQA6GVvBf+Z3gqUuWYBy2NWh4+5CLYq5uoc3MOvUQ5H5m8CJBqOA== +"@discordjs/collection@^1.5.2": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-1.5.2.tgz#3ba34c216e920999b5075f8fdc62f70bb2a7e0fb" + integrity sha512-LDplPy8SPbc8MYkuCdnLRGWqygAX97E8NH7gA9uz+NZ/hXknUKJHuxsOmhC6pmHnF9Zmg0kvfwrDjGsRIljt9g== "@discordjs/formatters@^0.3.1": version "0.3.1" @@ -27,38 +27,39 @@ dependencies: discord-api-types "^0.37.41" -"@discordjs/rest@^1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@discordjs/rest/-/rest-1.7.1.tgz#eeef0e71a37c95fa27962129729b2aa9de8e3752" - integrity sha512-Ofa9UqT0U45G/eX86cURQnX7gzOJLG2oC28VhIk/G6IliYgQF7jFByBJEykPSHE4MxPhqCleYvmsrtfKh1nYmQ== +"@discordjs/rest@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@discordjs/rest/-/rest-2.0.0.tgz#d82d4035d93bc860f9f34b07af3178dc12a296c4" + integrity sha512-CW9ldfzsRzUbHcS4Oqu5+Moo+yrQ5qQ9groKNxPOzcoq2nuXa/fXOXkuQtQHcTeSVXsC9cmJ56M8gBDBUyLgGA== dependencies: - "@discordjs/collection" "^1.5.1" - "@discordjs/util" "^0.3.0" + "@discordjs/collection" "^1.5.2" + "@discordjs/util" "^1.0.0" "@sapphire/async-queue" "^1.5.0" - "@sapphire/snowflake" "^3.4.2" - discord-api-types "^0.37.41" - file-type "^18.3.0" - tslib "^2.5.0" - undici "^5.22.0" - -"@discordjs/util@^0.3.0", "@discordjs/util@^0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@discordjs/util/-/util-0.3.1.tgz#4e8737e1dcff7e9f5eccc3116fb44755b65b1e97" - integrity sha512-HxXKYKg7vohx2/OupUN/4Sd02Ev3PBJ5q0gtjdcvXb0ErCva8jNHWfe/v5sU3UKjIB/uxOhc+TDOnhqffj9pRA== - -"@discordjs/ws@^0.8.3": - version "0.8.3" - resolved "https://registry.yarnpkg.com/@discordjs/ws/-/ws-0.8.3.tgz#77db8d563b731a2198c1b40f63b1ef8d230504f7" - integrity sha512-hcYtppanjHecbdNyCKQNH2I4RP9UrphDgmRgLYrATEQF1oo4sYSve7ZmGsBEXSzH72MO2tBPdWSThunbxUVk0g== + "@sapphire/snowflake" "^3.5.1" + "@vladfrangu/async_event_emitter" "^2.2.2" + discord-api-types "^0.37.50" + magic-bytes.js "^1.0.15" + tslib "^2.6.1" + undici "^5.22.1" + +"@discordjs/util@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@discordjs/util/-/util-1.0.0.tgz#8b4d3756ee725f6fc1a4999834d6ca7c4a353837" + integrity sha512-U2Iiab0mo8cFe+o4ZY4GROoAetGjFYA1PhhxiXEW82LuPUjOU/seHZDtVjDpOf6n3rz4IRm84wNtgHdpqRY5CA== + +"@discordjs/ws@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@discordjs/ws/-/ws-1.0.0.tgz#99b6aef63374ae406b481fae2e76a2666e95a1c6" + integrity sha512-POiImjuQJzwCxjJs4JCtDcTjzvjVsVQbnsaoW/F03yTVdrj/xSpmgv4383AnpNEYXI+CA6ggkz37phZDsZQ1NQ== dependencies: - "@discordjs/collection" "^1.5.1" - "@discordjs/rest" "^1.7.1" - "@discordjs/util" "^0.3.1" + "@discordjs/collection" "^1.5.2" + "@discordjs/rest" "^2.0.0" + "@discordjs/util" "^1.0.0" "@sapphire/async-queue" "^1.5.0" - "@types/ws" "^8.5.4" - "@vladfrangu/async_event_emitter" "^2.2.1" - discord-api-types "^0.37.41" - tslib "^2.5.0" + "@types/ws" "^8.5.5" + "@vladfrangu/async_event_emitter" "^2.2.2" + discord-api-types "^0.37.50" + tslib "^2.6.1" ws "^8.13.0" "@sapphire/async-queue@^1.5.0": @@ -66,7 +67,7 @@ resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.5.0.tgz#2f255a3f186635c4fb5a2381e375d3dfbc5312d8" integrity sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA== -"@sapphire/shapeshift@^3.8.2": +"@sapphire/shapeshift@^3.9.2": version "3.9.2" resolved "https://registry.yarnpkg.com/@sapphire/shapeshift/-/shapeshift-3.9.2.tgz#a9c12cd51e1bc467619bb56df804450dd14871ac" integrity sha512-YRbCXWy969oGIdqR/wha62eX8GNHsvyYi0Rfd4rNW6tSVVa8p0ELiMEuOH/k8rgtvRoM+EMV7Csqz77YdwiDpA== @@ -74,29 +75,24 @@ fast-deep-equal "^3.1.3" lodash "^4.17.21" -"@sapphire/snowflake@^3.4.2": +"@sapphire/snowflake@^3.5.1": version "3.5.1" resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.5.1.tgz#254521c188b49e8b2d4cc048b475fb2b38737fec" integrity sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA== -"@tokenizer/token@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" - integrity sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A== - "@types/node@*": version "20.3.2" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.2.tgz#fa6a90f2600e052a03c18b8cb3fd83dd4e599898" integrity sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw== -"@types/ws@^8.5.4": +"@types/ws@^8.5.5": version "8.5.5" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" integrity sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg== dependencies: "@types/node" "*" -"@vladfrangu/async_event_emitter@^2.2.1": +"@vladfrangu/async_event_emitter@^2.2.2": version "2.2.2" resolved "https://registry.yarnpkg.com/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.2.tgz#84c5a3f8d648842cec5cc649b88df599af32ed88" integrity sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ== @@ -113,24 +109,29 @@ discord-api-types@^0.37.41: resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.47.tgz#d622d5f5629a71ca2cd534442ae2800844e1888d" integrity sha512-rNif8IAv6duS2z47BMXq/V9kkrLfkAoiwpFY3sLxxbyKprk065zqf3HLTg4bEoxRSmi+Lhc7yqGDrG8C3j8GFA== -discord.js@^14.11.0: - version "14.11.0" - resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-14.11.0.tgz#6529d49f30d10fc5a9ff8e6796661aa998769afe" - integrity sha512-CkueWYFQ28U38YPR8HgsBR/QT35oPpMbEsTNM30Fs8loBIhnA4s70AwQEoy6JvLcpWWJO7GY0y2BUzZmuBMepQ== +discord-api-types@^0.37.50: + version "0.37.52" + resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.37.52.tgz#0d47a4409f1e17252dc895f2615dea3d161f7b5f" + integrity sha512-TP99aMgY6rHuDIy056GDm1j2nGOcaLbFpjVbvAmv6N6vhkjHNqHXCHVqKtGisaQ8D15slbxTHNl/SSkPdx2syg== + +discord.js@^14.12.1: + version "14.12.1" + resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-14.12.1.tgz#f3073d6fedaaf4948209311165c574dffa417df4" + integrity sha512-gGjhTkauIPgFXxpBl0UZgyehrKhDe90cIS8Hn1xFBYQ63EuUAkKoUqRNmc/pcla6DD16s4cUz5tAbdSpXivnxw== dependencies: - "@discordjs/builders" "^1.6.3" - "@discordjs/collection" "^1.5.1" + "@discordjs/builders" "^1.6.4" + "@discordjs/collection" "^1.5.2" "@discordjs/formatters" "^0.3.1" - "@discordjs/rest" "^1.7.1" - "@discordjs/util" "^0.3.1" - "@discordjs/ws" "^0.8.3" - "@sapphire/snowflake" "^3.4.2" - "@types/ws" "^8.5.4" - discord-api-types "^0.37.41" + "@discordjs/rest" "^2.0.0" + "@discordjs/util" "^1.0.0" + "@discordjs/ws" "^1.0.0" + "@sapphire/snowflake" "^3.5.1" + "@types/ws" "^8.5.5" + discord-api-types "^0.37.50" fast-deep-equal "^3.1.3" lodash.snakecase "^4.1.1" - tslib "^2.5.0" - undici "^5.22.0" + tslib "^2.6.1" + undici "^5.22.1" ws "^8.13.0" fast-deep-equal@^3.1.3: @@ -138,25 +139,6 @@ fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -file-type@^18.3.0: - version "18.5.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-18.5.0.tgz#604a001ba0d32577d4c3fa420ee104d656b914d2" - integrity sha512-yvpl5U868+V6PqXHMmsESpg6unQ5GfnPssl4dxdJudBrr9qy7Fddt7EVX1VLlddFfe8Gj9N7goCZH22FXuSQXQ== - dependencies: - readable-web-to-node-stream "^3.0.2" - strtok3 "^7.0.0" - token-types "^5.0.1" - -ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -inherits@^2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - lodash.snakecase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" @@ -167,60 +149,16 @@ lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -peek-readable@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/peek-readable/-/peek-readable-5.0.0.tgz#7ead2aff25dc40458c60347ea76cfdfd63efdfec" - integrity sha512-YtCKvLUOvwtMGmrniQPdO7MwPjgkFBtFIrmfSbYmYuq3tKDV/mcfAhBth1+C3ru7uXIZasc/pHnb+YDYNkkj4A== - -readable-stream@^3.6.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-web-to-node-stream@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb" - integrity sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw== - dependencies: - readable-stream "^3.6.0" - -safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +magic-bytes.js@^1.0.15: + version "1.0.15" + resolved "https://registry.yarnpkg.com/magic-bytes.js/-/magic-bytes.js-1.0.15.tgz#3c9d2b7d45bb8432482646b5f74bbf6725274616" + integrity sha512-bpRmwbRHqongRhA+mXzbLWjVy7ylqmfMBYaQkSs6pac0z6hBTvsgrH0r4FBYd/UYVJBmS6Rp/O+oCCQVLzKV1g== streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -strtok3@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/strtok3/-/strtok3-7.0.0.tgz#868c428b4ade64a8fd8fee7364256001c1a4cbe5" - integrity sha512-pQ+V+nYQdC5H3Q7qBZAz/MO6lwGhoC2gOAjuouGf/VO0m7vQRh8QNMl2Uf6SwAtzZ9bOw3UIeBukEGNJl5dtXQ== - dependencies: - "@tokenizer/token" "^0.3.0" - peek-readable "^5.0.0" - -token-types@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/token-types/-/token-types-5.0.1.tgz#aa9d9e6b23c420a675e55413b180635b86a093b4" - integrity sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg== - dependencies: - "@tokenizer/token" "^0.3.0" - ieee754 "^1.2.1" - ts-mixer@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/ts-mixer/-/ts-mixer-6.0.3.tgz#69bd50f406ff39daa369885b16c77a6194c7cae6" @@ -231,23 +169,23 @@ tsc@^2.0.4: resolved "https://registry.yarnpkg.com/tsc/-/tsc-2.0.4.tgz#5f6499146abea5dca4420b451fa4f2f9345238f5" integrity sha512-fzoSieZI5KKJVBYGvwbVZs/J5za84f2lSTLPYf6AGiIf43tZ3GNrI1QzTLcjtyDDP4aLxd46RTZq1nQxe7+k5Q== -tslib@^2.5.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3" - integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA== +tslib@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" + integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== + +typescript@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== -undici@^5.22.0: - version "5.22.1" - resolved "https://registry.yarnpkg.com/undici/-/undici-5.22.1.tgz#877d512effef2ac8be65e695f3586922e1a57d7b" - integrity sha512-Ji2IJhFXZY0x/0tVBXeQwgPlLWw13GVzpsWPQ3rV50IFMMof2I55PZZxtm4P6iNq+L5znYN9nSTAq0ZyE6lSJw== +undici@^5.22.1: + version "5.23.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-5.23.0.tgz#e7bdb0ed42cebe7b7aca87ced53e6eaafb8f8ca0" + integrity sha512-1D7w+fvRsqlQ9GscLBwcAJinqcZGHUKjbOmXdlE/v8BvEGXjeWAax+341q44EuTcHXXnfyKNbKRq4Lg7OzhMmg== dependencies: busboy "^1.6.0" -util-deprecate@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - ws@^8.13.0: version "8.13.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0"