diff --git a/docs/examples/commands.md b/docs/examples/commands.md new file mode 100644 index 000000000000..9bbaa67b86ab --- /dev/null +++ b/docs/examples/commands.md @@ -0,0 +1,52 @@ +# Slash Commands + +In this example, you'll get to know how to create commands and listen to incoming interactions. + +## Creating a Command + +First off, we need to create a command so that users can use it. We will create an `echo` command which simply returns what the user inputted. Note that global commands can take up to an hour to appear in the client, so if you want to test a new command, you should create it for one guild first. + +```js +// The data for our command +const commandData = { + name: 'echo', + description: 'Replies with your input!', + options: [{ + name: 'input', + type: 'STRING', + description: 'The input which should be echoed back', + required: true, + }], +}; + +client.once('ready', () => { + // Creating a global command + client.application.commands.create(commandData); + + // Creating a guild-specific command + client.guilds.cache.get('id').commands.create(commandData); +}); +``` + +And that's it! As soon as your client is ready, it will register the `echo` command. + +## Handling Commands + +Now let's implement a simple handler for it: + +```js +client.on('interaction', interaction => { + // If the interaction isn't a slash command, return + if (!interaction.isCommand()) return; + + // Check if it is the correct command + if (interaction.commandName === 'echo') { + // Get the input of the user + const input = interaction.options[0].value; + // Reply to the command + interaction.reply(input); + } +}); +``` + +The `interaction` event will get emitted every time the client receives an interaction. Only our own slash commands trigger this event, so there is no need to implement a check for commands that belong to other bots. diff --git a/docs/index.yml b/docs/index.yml index 9a7cfbcc6a14..fc639df96c03 100644 --- a/docs/index.yml +++ b/docs/index.yml @@ -28,3 +28,5 @@ path: moderation.md - name: Webhook path: webhook.js + - name: Slash Commands + path: commands.md diff --git a/esm/discord.mjs b/esm/discord.mjs index 6c420bf0acdc..befac0f11003 100644 --- a/esm/discord.mjs +++ b/esm/discord.mjs @@ -28,8 +28,10 @@ export const { UserFlags, Util, version, + ApplicationCommandManager, BaseGuildEmojiManager, ChannelManager, + GuildApplicationCommandManager, GuildChannelManager, GuildEmojiManager, GuildEmojiRoleManager, @@ -49,6 +51,7 @@ export const { resolveString, splitMessage, Application, + ApplicationCommand, Base, Activity, APIMessage, @@ -59,6 +62,7 @@ export const { ClientApplication, ClientUser, Collector, + CommandInteraction, DMChannel, Emoji, Guild, @@ -70,6 +74,7 @@ export const { GuildTemplate, Integration, IntegrationApplication, + Interaction, Invite, Message, MessageAttachment, diff --git a/package-lock.json b/package-lock.json index 0dd53df56043..6c53f8c33e8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2383,9 +2383,9 @@ "dev": true }, "discord-api-types": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.18.0.tgz", - "integrity": "sha512-KxS+opGqrNbzo+wb3SxOqfuv/xX2CyrwEjCQLTI68gzjbSMn55h6o7JtPkWAXItadkNrUWyDqMMsfOz8DnpiNA==", + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.18.1.tgz", + "integrity": "sha512-hNC38R9ZF4uaujaZQtQfm5CdQO58uhdkoHQAVvMfIL0LgOSZeW575W8H6upngQOuoxWd8tiRII3LLJm9zuQKYg==", "dev": true }, "discord.js-docgen": { diff --git a/package.json b/package.json index 0371a25b71d1..501facd049f7 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@types/node": "^12.12.6", "@types/ws": "^7.2.7", "cross-env": "^7.0.2", - "discord-api-types": "^0.18.0", + "discord-api-types": "^0.18.1", "discord.js-docgen": "git+https://github.com/discordjs/docgen.git", "dtslint": "^4.0.4", "eslint": "^7.11.0", diff --git a/src/client/websocket/handlers/INTERACTION_CREATE.js b/src/client/websocket/handlers/INTERACTION_CREATE.js new file mode 100644 index 000000000000..cef671b97d88 --- /dev/null +++ b/src/client/websocket/handlers/INTERACTION_CREATE.js @@ -0,0 +1,23 @@ +'use strict'; + +const { Events, InteractionTypes } = require('../../../util/Constants'); +let Structures; + +module.exports = (client, { d: data }) => { + if (data.type === InteractionTypes.APPLICATION_COMMAND) { + if (!Structures) Structures = require('../../../util/Structures'); + const CommandInteraction = Structures.get('CommandInteraction'); + + const interaction = new CommandInteraction(client, data); + + /** + * Emitted when an interaction is created. + * @event Client#interaction + * @param {Interaction} interaction The interaction which was created + */ + client.emit(Events.INTERACTION_CREATE, interaction); + return; + } + + client.emit(Events.DEBUG, `[INTERACTION] Received interaction with unknown type: ${data.type}`); +}; diff --git a/src/errors/Messages.js b/src/errors/Messages.js index c84006503647..b8f309c0c7bd 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -110,6 +110,8 @@ const Messages = { FETCH_GROUP_DM_CHANNEL: "Bots don't have access to Group DM Channels and cannot fetch them", MEMBER_FETCH_NONCE_LENGTH: 'Nonce length must not exceed 32 characters.', + + INTERACTION_ALREADY_REPLIED: 'This interaction has already been deferred or replied to.', }; for (const [name, message] of Object.entries(Messages)) register(name, message); diff --git a/src/index.js b/src/index.js index 5ca8de77d581..3d4f0519a111 100644 --- a/src/index.js +++ b/src/index.js @@ -33,8 +33,10 @@ module.exports = { version: require('../package.json').version, // Managers + ApplicationCommandManager: require('./managers/ApplicationCommandManager'), BaseGuildEmojiManager: require('./managers/BaseGuildEmojiManager'), ChannelManager: require('./managers/ChannelManager'), + GuildApplicationCommandManager: require('./managers/GuildApplicationCommandManager'), GuildChannelManager: require('./managers/GuildChannelManager'), GuildEmojiManager: require('./managers/GuildEmojiManager'), GuildEmojiRoleManager: require('./managers/GuildEmojiRoleManager'), @@ -58,6 +60,7 @@ module.exports = { // Structures Application: require('./structures/interfaces/Application'), + ApplicationCommand: require('./structures/ApplicationCommand'), Base: require('./structures/Base'), Activity: require('./structures/Presence').Activity, APIMessage: require('./structures/APIMessage'), @@ -71,6 +74,7 @@ module.exports = { return require('./structures/ClientUser'); }, Collector: require('./structures/interfaces/Collector'), + CommandInteraction: require('./structures/CommandInteraction'), DMChannel: require('./structures/DMChannel'), Emoji: require('./structures/Emoji'), Guild: require('./structures/Guild'), @@ -82,6 +86,7 @@ module.exports = { GuildTemplate: require('./structures/GuildTemplate'), Integration: require('./structures/Integration'), IntegrationApplication: require('./structures/IntegrationApplication'), + Interaction: require('./structures/Interaction'), Invite: require('./structures/Invite'), Message: require('./structures/Message'), MessageAttachment: require('./structures/MessageAttachment'), diff --git a/src/managers/ApplicationCommandManager.js b/src/managers/ApplicationCommandManager.js new file mode 100644 index 000000000000..d49ca3534e17 --- /dev/null +++ b/src/managers/ApplicationCommandManager.js @@ -0,0 +1,301 @@ +'use strict'; + +const BaseManager = require('./BaseManager'); +const { TypeError } = require('../errors'); +const ApplicationCommand = require('../structures/ApplicationCommand'); +const Collection = require('../util/Collection'); +const { ApplicationCommandPermissionTypes } = require('../util/Constants'); + +/** + * Manages API methods for application commands and stores their cache. + * @extends {BaseManager} + */ +class ApplicationCommandManager extends BaseManager { + constructor(client, iterable) { + super(client, iterable, ApplicationCommand); + } + + /** + * The cache of this manager + * @type {Collection} + * @name ApplicationCommandManager#cache + */ + + add(data, cache) { + return super.add(data, cache, { extras: [this.guild] }); + } + + /** + * The APIRouter path to the commands + * @type {Object} + * @readonly + * @private + */ + get commandPath() { + let path = this.client.api.applications(this.client.application.id); + if (this.guild) path = path.guilds(this.guild.id); + return path.commands; + } + + /** + * Data that resolves to give an ApplicationCommand object. This can be: + * * An ApplicationCommand object + * * A Snowflake + * @typedef {ApplicationCommand|Snowflake} ApplicationCommandResolvable + */ + + /** + * Obtains one or multiple application commands from Discord, or the cache if it's already available. + * @param {Snowflake} [id] ID of the application command + * @param {boolean} [cache=true] Whether to cache the new application commands if they weren't already + * @param {boolean} [force=false] Whether to skip the cache check and request the API + * @returns {Promise>} + * @example + * // Fetch a single command + * client.application.commands.fetch('123456789012345678') + * .then(command => console.log(`Fetched command ${command.name}`)) + * .catch(console.error); + * @example + * // Fetch all commands + * guild.commands.fetch() + * .then(commands => console.log(`Fetched ${commands.size} commands`)) + * .catch(console.error); + */ + async fetch(id, cache = true, force = false) { + if (id) { + if (!force) { + const existing = this.cache.get(id); + if (existing) return existing; + } + const command = await this.commandPath(id).get(); + return this.add(command, cache); + } + + const data = await this.commandPath.get(); + return data.reduce((coll, command) => coll.set(command.id, this.add(command, cache)), new Collection()); + } + + /** + * Creates an application command. + * @param {ApplicationCommandData} command The command + * @returns {Promise} + * @example + * // Create a new command + * client.application.commands.create({ + * name: 'test', + * description: 'A test command', + * }) + * .then(console.log) + * .catch(console.error); + */ + async create(command) { + const data = await this.commandPath.post({ + data: this.constructor.transformCommand(command), + }); + return this.add(data); + } + + /** + * Sets all the commands for this application or guild. + * @param {ApplicationCommandData[]} commands The commands + * @returns {Promise>} + * @example + * // Set all commands to just this one + * client.application.commands.set([ + * { + * name: 'test', + * description: 'A test command', + * }, + * ]) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove all commands + * guild.commands.set([]) + * .then(console.log) + * .catch(console.error); + */ + async set(commands) { + const data = await this.commandPath.put({ + data: commands.map(c => this.constructor.transformCommand(c)), + }); + return data.reduce((coll, command) => coll.set(command.id, this.add(command)), new Collection()); + } + + /** + * Edits an application command. + * @param {ApplicationCommandResolvable} command The command to edit + * @param {ApplicationCommandData} data The data to update the command with + * @returns {Promise} + * @example + * // Edit an existing command + * client.application.commands.edit('123456789012345678', { + * description: 'New description', + * }) + * .then(console.log) + * .catch(console.error); + */ + async edit(command, data) { + const id = this.resolveID(command); + if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + + const patched = await this.commandPath(id).patch({ data: this.constructor.transformCommand(data) }); + return this.add(patched); + } + + /** + * Deletes an application command. + * @param {ApplicationCommandResolvable} command The command to delete + * @returns {Promise} + * @example + * // Delete a command + * guild.commands.delete('123456789012345678') + * .then(console.log) + * .catch(console.error); + */ + async delete(command) { + const id = this.resolveID(command); + if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + + await this.commandPath(id).delete(); + + const cached = this.cache.get(id); + this.cache.delete(id); + return cached ?? null; + } + + /** + * Fetches the permissions for one or multiple commands. + * @param {ApplicationCommandResolvable} [command] The command to get the permissions from + * @returns {Promise>} + * @example + * // Fetch permissions for one command + * guild.commands.fetchPermissions('123456789012345678') + * .then(perms => console.log(`Fetched permissions for ${perms.length} users`)) + * .catch(console.error); + * @example + * // Fetch permissions for all commands + * client.application.commands.fetchPermissions() + * .then(perms => console.log(`Fetched permissions for ${perms.size} commands`)) + * .catch(console.error); + */ + async fetchPermissions(command) { + if (command) { + const id = this.resolveID(command); + if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + + const data = await this.commandPath(id).permissions.get(); + return data.permissions.map(perm => this.constructor.transformPermissions(perm, true)); + } + + const data = await this.commandPath.permissions.get(); + return data.reduce( + (coll, perm) => + coll.set( + perm.id, + perm.permissions.map(p => this.constructor.transformPermissions(p, true)), + ), + new Collection(), + ); + } + + /** + * Data used for overwriting the permissions for all application commands in a guild. + * @typedef {object} GuildApplicationCommandPermissionData + * @prop {Snowflake} command The ID of the command + * @prop {ApplicationCommandPermissionData[]} permissions The permissions for this command + */ + + /** + * Sets the permissions for a command. + * @param {ApplicationCommandResolvable|GuildApplicationCommandPermissionData[]} command The command to edit the + * permissions for, or an array of guild application command permissions to set the permissions of all commands + * @param {ApplicationCommandPermissionData[]} permissions The new permissions for the command + * @returns {Promise>} + * @example + * // Set the permissions for one command + * client.commands.setPermissions('123456789012345678', [ + * { + * id: '876543210987654321', + * type: 'USER', + * permission: false, + * }, + * ]) + * .then(console.log) + * .catch(console.error); + * @example + * // Set the permissions for all commands + * guild.commands.setPermissions([ + * { + * id: '123456789012345678', + * permissions: [{ + * id: '876543210987654321', + * type: 'USER', + * permission: false, + * }], + * }, + * ]) + * .then(console.log) + * .catch(console.error); + */ + async setPermissions(command, permissions) { + const id = this.resolveID(command); + + if (id) { + const data = await this.commandPath(id).permissions.put({ + data: { permissions: permissions.map(perm => this.constructor.transformPermissions(perm)) }, + }); + return data.permissions.map(perm => this.constructor.transformPermissions(perm, true)); + } + + const data = await this.commandPath.permissions.put({ + data: command.map(perm => ({ + id: perm.id, + permissions: perm.permissions.map(p => this.constructor.transformPermissions(p)), + })), + }); + return data.reduce( + (coll, perm) => + coll.set( + perm.id, + perm.permissions.map(p => this.constructor.transformPermissions(p, true)), + ), + new Collection(), + ); + } + + /** + * Transforms an {@link ApplicationCommandData} object into something that can be used with the API. + * @param {ApplicationCommandData} command The command to transform + * @returns {Object} + * @private + */ + static transformCommand(command) { + return { + name: command.name, + description: command.description, + options: command.options?.map(o => ApplicationCommand.transformOption(o)), + default_permission: command.defaultPermission, + }; + } + + /** + * Transforms an {@link ApplicationCommandPermissionData} object into something that can be used with the API. + * @param {ApplicationCommandPermissionData} permissions The permissions to transform + * @param {boolean} [received] Whether these permissions have been received from Discord + * @returns {Object} + * @private + */ + static transformPermissions(permissions, received) { + return { + id: permissions.id, + permission: permissions.permission, + type: + typeof permissions.type === 'number' && !received + ? permissions.type + : ApplicationCommandPermissionTypes[permissions.type], + }; + } +} + +module.exports = ApplicationCommandManager; diff --git a/src/managers/GuildApplicationCommandManager.js b/src/managers/GuildApplicationCommandManager.js new file mode 100644 index 000000000000..3a78fbf7f864 --- /dev/null +++ b/src/managers/GuildApplicationCommandManager.js @@ -0,0 +1,21 @@ +'use strict'; + +const ApplicationCommandManager = require('./ApplicationCommandManager'); + +/** + * An extension for guild-specific application commands. + * @extends {ApplicationCommandManager} + */ +class GuildApplicationCommandManager extends ApplicationCommandManager { + constructor(guild, iterable) { + super(guild.client, iterable); + + /** + * The guild that this manager belongs to + * @type {Guild} + */ + this.guild = guild; + } +} + +module.exports = GuildApplicationCommandManager; diff --git a/src/rest/RESTManager.js b/src/rest/RESTManager.js index a9487fd920df..bd54587e1c93 100644 --- a/src/rest/RESTManager.js +++ b/src/rest/RESTManager.js @@ -18,9 +18,10 @@ class RESTManager { this.globalReset = null; this.globalDelay = null; if (client.options.restSweepInterval > 0) { - client.setInterval(() => { + const interval = client.setInterval(() => { this.handlers.sweep(handler => handler._inactive); }, client.options.restSweepInterval * 1000); + interval.unref(); } } diff --git a/src/structures/APIMessage.js b/src/structures/APIMessage.js index 8974952b5083..01695b061619 100644 --- a/src/structures/APIMessage.js +++ b/src/structures/APIMessage.js @@ -73,6 +73,16 @@ class APIMessage { return this.target instanceof Message; } + /** + * Whether or not the target is an interaction + * @type {boolean} + * @readonly + */ + get isInteraction() { + const Interaction = require('./Interaction'); + return this.target instanceof Interaction; + } + /** * Makes the content of this message. * @returns {?(string|string[])} @@ -129,7 +139,7 @@ class APIMessage { } const embedLikes = []; - if (this.isWebhook) { + if (this.isInteraction || this.isWebhook) { if (this.options.embeds) { embedLikes.push(...this.options.embeds); } @@ -149,6 +159,8 @@ class APIMessage { if (this.isMessage) { // eslint-disable-next-line eqeqeq flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags.bitfield; + } else if (this.isInteraction && this.options.ephemeral) { + flags = MessageFlags.FLAGS.EPHEMERAL; } let allowedMentions = @@ -196,7 +208,7 @@ class APIMessage { if (this.files) return this; const embedLikes = []; - if (this.isWebhook) { + if (this.isInteraction || this.isWebhook) { if (this.options.embeds) { embedLikes.push(...this.options.embeds); } @@ -348,10 +360,11 @@ class APIMessage { * @returns {MessageOptions|WebhookMessageOptions} */ static create(target, content, options, extra = {}) { + const Interaction = require('./Interaction'); const Webhook = require('./Webhook'); const WebhookClient = require('../client/WebhookClient'); - const isWebhook = target instanceof Webhook || target instanceof WebhookClient; + const isWebhook = target instanceof Interaction || target instanceof Webhook || target instanceof WebhookClient; const transformed = this.transformOptions(content, options, extra, isWebhook); return new this(target, transformed); } diff --git a/src/structures/ApplicationCommand.js b/src/structures/ApplicationCommand.js new file mode 100644 index 000000000000..2291dc3dee92 --- /dev/null +++ b/src/structures/ApplicationCommand.js @@ -0,0 +1,218 @@ +'use strict'; + +const Base = require('./Base'); +const { ApplicationCommandOptionTypes } = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); + +/** + * Represents an application command. + * @extends {Base} + */ +class ApplicationCommand extends Base { + constructor(client, data, guild) { + super(client); + + /** + * The ID of this command + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The guild this command is part of + * @type {?Guild} + */ + this.guild = guild ?? null; + + this._patch(data); + } + + _patch(data) { + /** + * The name of this command + * @type {string} + */ + this.name = data.name; + + /** + * The description of this command + * @type {string} + */ + this.description = data.description; + + /** + * The options of this command + * @type {ApplicationCommandOption[]} + */ + this.options = data.options?.map(o => this.constructor.transformOption(o, true)) ?? []; + + /** + * Whether the command is enabled by default when the app is added to a guild + * @type {boolean} + */ + this.defaultPermission = data.default_permission; + } + + /** + * The timestamp the command was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return SnowflakeUtil.deconstruct(this.id).timestamp; + } + + /** + * The time the command was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The manager that this command belongs to + * @type {ApplicationCommandManager} + * @readonly + */ + get manager() { + return (this.guild ?? this.client.application).commands; + } + + /** + * Data for creating or editing an application command. + * @typedef {Object} ApplicationCommandData + * @property {string} name The name of the command + * @property {string} description The description of the command + * @property {ApplicationCommandOptionData[]} [options] Options for the command + * @property {boolean} [defaultPermission] Whether the command is enabled by default when the app is added to a guild + */ + + /** + * An option for an application command or subcommand. + * @typedef {Object} ApplicationCommandOptionData + * @property {ApplicationCommandOptionType|number} type The type of the option + * @property {string} name The name of the option + * @property {string} description The description of the option + * @property {boolean} [required] Whether the option is required + * @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from + * @property {ApplicationCommandOptionData[]} [options] Additional options if this option is a subcommand (group) + */ + + /** + * Edits this application command. + * @param {ApplicationCommandData} data The data to update the command with + * @returns {Promise} + * @example + * // Edit the description of this command + * command.edit({ + * description: 'New description', + * }) + * .then(console.log) + * .catch(console.error); + */ + edit(data) { + return this.manager.edit(this, data); + } + + /** + * Deletes this command. + * @returns {Promise} + * @example + * // Delete this command + * command.delete() + * .then(console.log) + * .catch(console.error); + */ + delete() { + return this.manager.delete(this); + } + + /** + * The object returned when fetching permissions for an application command. + * @typedef {object} ApplicationCommandPermissionData + * @property {Snowflake} id The ID of the role or user + * @property {ApplicationCommandPermissionType|number} type Whether this permission if for a role or a user + * @property {boolean} permission Whether the role or user has the permission to use this command + */ + + /** + * The object returned when fetching permissions for an application command. + * @typedef {object} ApplicationCommandPermissions + * @property {Snowflake} id The ID of the role or user + * @property {ApplicationCommandPermissionType} type Whether this permission if for a role or a user + * @property {boolean} permission Whether the role or user has the permission to use this command + */ + + /** + * Fetches the permissions for this command. + * @returns {Promise} + * @example + * // Fetch permissions for this command + * command.fetchPermissions() + * .then(perms => console.log(`Fetched permissions for ${perms.length} users`)) + * .catch(console.error); + */ + fetchPermissions() { + return this.manager.fetchPermissions(this); + } + + /** + * Sets the permissions for this command. + * @param {ApplicationCommandPermissionData[]} permissions The new permissions for the command + * @returns {Promise} + * @example + * // Set the permissions for this command + * command.setPermissions([ + * { + * id: '876543210987654321', + * type: 'USER', + * permission: false, + * }, + * ]) + * .then(console.log) + * .catch(console.error); + */ + setPermissions(permissions) { + return this.manager.setPermissions(this, permissions); + } + + /** + * An option for an application command or subcommand. + * @typedef {Object} ApplicationCommandOption + * @property {ApplicationCommandOptionType} type The type of the option + * @property {string} name The name of the option + * @property {string} description The description of the option + * @property {boolean} [required] Whether the option is required + * @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from + * @property {ApplicationCommandOption[]} [options] Additional options if this option is a subcommand (group) + */ + + /** + * A choice for an application command option. + * @typedef {Object} ApplicationCommandOptionChoice + * @property {string} name The name of the choice + * @property {string|number} value The value of the choice + */ + + /** + * Transforms an {@link ApplicationCommandOptionData} object into something that can be used with the API. + * @param {ApplicationCommandOptionData} option The option to transform + * @param {boolean} [received] Whether this option has been received from Discord + * @returns {Object} + * @private + */ + static transformOption(option, received) { + return { + type: typeof option.type === 'number' && !received ? option.type : ApplicationCommandOptionTypes[option.type], + name: option.name, + description: option.description, + required: option.required, + choices: option.choices, + options: option.options?.map(o => this.transformOption(o)), + }; + } +} + +module.exports = ApplicationCommand; diff --git a/src/structures/ClientApplication.js b/src/structures/ClientApplication.js index 20968d742ae0..aaefe1a2fd48 100644 --- a/src/structures/ClientApplication.js +++ b/src/structures/ClientApplication.js @@ -2,6 +2,7 @@ const Team = require('./Team'); const Application = require('./interfaces/Application'); +const ApplicationCommandManager = require('../managers/ApplicationCommandManager'); const ApplicationFlags = require('../util/ApplicationFlags'); /** @@ -9,6 +10,16 @@ const ApplicationFlags = require('../util/ApplicationFlags'); * @extends {Application} */ class ClientApplication extends Application { + constructor(client, data) { + super(client, data); + + /** + * The application command manager for this application + * @type {ApplicationCommandManager} + */ + this.commands = new ApplicationCommandManager(this.client); + } + _patch(data) { super._patch(data); diff --git a/src/structures/CommandInteraction.js b/src/structures/CommandInteraction.js new file mode 100644 index 000000000000..2cdd9179785a --- /dev/null +++ b/src/structures/CommandInteraction.js @@ -0,0 +1,222 @@ +'use strict'; + +const APIMessage = require('./APIMessage'); +const Interaction = require('./Interaction'); +const WebhookClient = require('../client/WebhookClient'); +const { Error } = require('../errors'); +const { ApplicationCommandOptionTypes, InteractionResponseTypes } = require('../util/Constants'); +const MessageFlags = require('../util/MessageFlags'); + +/** + * Represents a command interaction. + * @extends {Interaction} + */ +class CommandInteraction extends Interaction { + constructor(client, data) { + super(client, data); + + /** + * The ID of the invoked application command + * @type {Snowflake} + */ + this.commandID = data.data.id; + + /** + * The name of the invoked application command + * @type {string} + */ + this.commandName = data.data.name; + + /** + * Whether the reply to this interaction has been deferred + * @type {boolean} + */ + this.deferred = false; + + /** + * The options passed to the command. + * @type {CommandInteractionOption[]} + */ + this.options = data.data.options?.map(o => this.transformOption(o, data.data.resolved)) ?? []; + + /** + * Whether this interaction has already been replied to + * @type {boolean} + */ + this.replied = false; + + /** + * An associated webhook client, can be used to create deferred replies + * @type {WebhookClient} + */ + this.webhook = new WebhookClient(this.applicationID, this.token, this.client.options); + } + + /** + * The invoked application command, if it was fetched before + * @type {?ApplicationCommand} + */ + get command() { + const id = this.commandID; + return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null; + } + + /** + * Defers the reply to this interaction. + * @param {boolean} [ephemeral] Whether the reply should be ephemeral + * @returns {Promise} + * @example + * // Defer the reply to this interaction + * interaction.defer() + * .then(console.log) + * .catch(console.error) + * @example + * // Defer to send an ephemeral reply later + * interaction.defer(true) + * .then(console.log) + * .catch(console.error); + */ + async defer(ephemeral) { + if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); + await this.client.api.interactions(this.id, this.token).callback.post({ + data: { + type: InteractionResponseTypes.DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, + data: { + flags: ephemeral ? MessageFlags.FLAGS.EPHEMERAL : undefined, + }, + }, + }); + this.deferred = true; + } + + /** + * Options for a reply to an interaction. + * @typedef {WebhookMessageOptions} InteractionReplyOptions + * @property {boolean} [ephemeral] Whether the reply should be ephemeral + */ + + /** + * Creates a reply to this interaction. + * @param {string|APIMessage|MessageAdditions} content The content for the reply + * @param {InteractionReplyOptions} [options] Additional options for the reply + * @returns {Promise} + * @example + * // Reply to the interaction with an embed + * const embed = new MessageEmbed().setDescription('Pong!'); + * + * interaction.reply(embed) + * .then(console.log) + * .catch(console.error); + * @example + * // Create an ephemeral reply + * interaction.reply('Pong!', { ephemeral: true }) + * .then(console.log) + * .catch(console.error); + */ + async reply(content, options) { + if (this.deferred || this.replied) throw new Error('INTERACTION_ALREADY_REPLIED'); + const apiMessage = content instanceof APIMessage ? content : APIMessage.create(this, content, options); + const { data, files } = await apiMessage.resolveData().resolveFiles(); + + await this.client.api.interactions(this.id, this.token).callback.post({ + data: { + type: InteractionResponseTypes.CHANNEL_MESSAGE_WITH_SOURCE, + data, + }, + files, + }); + this.replied = true; + } + + /** + * Fetches the initial reply to this interaction. + * @see Webhook#fetchMessage + * @returns {Promise} + * @example + * // Fetch the reply to this interaction + * interaction.fetchReply() + * .then(reply => console.log(`Replied with ${reply.content}`)) + * .catch(console.error); + */ + async fetchReply() { + const raw = await this.webhook.fetchMessage('@original'); + return this.channel?.messages.add(raw) ?? raw; + } + + /** + * Edits the initial reply to this interaction. + * @see Webhook#editMessage + * @param {string|APIMessage|MessageEmbed|MessageEmbed[]} content The new content for the message + * @param {WebhookEditMessageOptions} [options] The options to provide + * @returns {Promise} + * @example + * // Edit the reply to this interaction + * interaction.editReply('New content') + * .then(console.log) + * .catch(console.error); + */ + async editReply(content, options) { + const raw = await this.webhook.editMessage('@original', content, options); + return this.channel?.messages.add(raw) ?? raw; + } + + /** + * Deletes the initial reply to this interaction. + * @see Webhook#deleteMessage + * @returns {Promise} + * @example + * // Delete the reply to this interaction + * interaction.deleteReply() + * .then(console.log) + * .catch(console.error); + */ + async deleteReply() { + await this.webhook.deleteMessage('@original'); + } + + /** + * Represents an option of a received command interaction. + * @typedef {Object} CommandInteractionOption + * @property {string} name The name of the option + * @property {ApplicationCommandOptionType} type The type of the option + * @property {string|number|boolean} [value] The value of the option + * @property {CommandInteractionOption[]} [options] Additional options if this option is a subcommand (group) + * @property {User} [user] The resolved user + * @property {GuildMember|Object} [member] The resolved member + * @property {GuildChannel|Object} [channel] The resolved channel + * @property {Role|Object} [role] The resolved role + */ + + /** + * Transforms an option received from the API. + * @param {Object} option The received option + * @param {Object} resolved The resolved interaction data + * @returns {CommandInteractionOption} + * @private + */ + transformOption(option, resolved) { + const result = { + name: option.name, + type: ApplicationCommandOptionTypes[option.type], + }; + + if ('value' in option) result.value = option.value; + if ('options' in option) result.options = option.options.map(o => this.transformOption(o, resolved)); + + const user = resolved?.users?.[option.value]; + if (user) result.user = this.client.users.add(user); + + const member = resolved?.members?.[option.value]; + if (member) result.member = this.guild?.members.add({ user, ...member }) ?? member; + + const channel = resolved?.channels?.[option.value]; + if (channel) result.channel = this.client.channels.add(channel, this.guild) ?? channel; + + const role = resolved?.roles?.[option.value]; + if (role) result.role = this.guild?.roles.add(role) ?? role; + + return result; + } +} + +module.exports = CommandInteraction; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index 44fabd462d51..6ac55b19e185 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -9,6 +9,7 @@ const Invite = require('./Invite'); const VoiceRegion = require('./VoiceRegion'); const Webhook = require('./Webhook'); const { Error, TypeError } = require('../errors'); +const GuildApplicationCommandManager = require('../managers/GuildApplicationCommandManager'); const GuildChannelManager = require('../managers/GuildChannelManager'); const GuildEmojiManager = require('../managers/GuildEmojiManager'); const GuildMemberManager = require('../managers/GuildMemberManager'); @@ -42,6 +43,12 @@ class Guild extends Base { constructor(client, data) { super(client); + /** + * A manager of the application commands belonging to this guild + * @type {GuildApplicationCommandManager} + */ + this.commands = new GuildApplicationCommandManager(this); + /** * A manager of the members belonging to this guild * @type {GuildMemberManager} diff --git a/src/structures/Interaction.js b/src/structures/Interaction.js new file mode 100644 index 000000000000..87a8c3ddfcf5 --- /dev/null +++ b/src/structures/Interaction.js @@ -0,0 +1,117 @@ +'use strict'; + +const Base = require('./Base'); +const { InteractionTypes } = require('../util/Constants'); +const SnowflakeUtil = require('../util/SnowflakeUtil'); + +/** + * Represents an interaction. + * @extends {Base} + */ +class Interaction extends Base { + constructor(client, data) { + super(client); + + /** + * The type of this interaction + * @type {InteractionType} + */ + this.type = InteractionTypes[data.type]; + + /** + * The ID of this interaction + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The token of this interaction + * @type {string} + * @name Interaction#token + * @readonly + */ + Object.defineProperty(this, 'token', { value: data.token }); + + /** + * The ID of the application + * @type {Snowflake} + */ + this.applicationID = data.application_id; + + /** + * The ID of the channel this interaction was sent in + * @type {?Snowflake} + */ + this.channelID = data.channel_id ?? null; + + /** + * The ID of the guild this interaction was sent in + * @type {?Snowflake} + */ + this.guildID = data.guild_id ?? null; + + /** + * The user which sent this interaction + * @type {User} + */ + this.user = this.client.users.add(data.user ?? data.member.user); + + /** + * If this interaction was sent in a guild, the member which sent it + * @type {?GuildMember|Object} + */ + this.member = data.member ? this.guild?.members.add(data.member) ?? data.member : null; + + /** + * The version + * @type {number} + */ + this.version = data.version; + } + + /** + * The timestamp the interaction was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return SnowflakeUtil.deconstruct(this.id).timestamp; + } + + /** + * The time the interaction was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The channel this interaction was sent in + * @type {?Channel} + * @readonly + */ + get channel() { + return this.client.channels.cache.get(this.channelID) ?? null; + } + + /** + * The guild this interaction was sent in + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.cache.get(this.guildID) ?? null; + } + + /** + * Indicates whether this interaction is a command interaction. + * @returns {boolean} + */ + isCommand() { + return InteractionTypes[this.type] === InteractionTypes.APPLICATION_COMMAND; + } +} + +module.exports = Interaction; diff --git a/src/structures/Message.js b/src/structures/Message.js index a59b1cc22f63..d07f2be88c8e 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -11,7 +11,7 @@ const Sticker = require('./Sticker'); const { Error, TypeError } = require('../errors'); const ReactionManager = require('../managers/ReactionManager'); const Collection = require('../util/Collection'); -const { MessageTypes, SystemMessageTypes } = require('../util/Constants'); +const { InteractionTypes, MessageTypes, SystemMessageTypes } = require('../util/Constants'); const MessageFlags = require('../util/MessageFlags'); const Permissions = require('../util/Permissions'); const SnowflakeUtil = require('../util/SnowflakeUtil'); @@ -232,6 +232,30 @@ class Message extends Base { if (data.referenced_message) { this.channel.messages.add(data.referenced_message); } + + /** + * Partial data of the interaction that a message is a reply to + * @typedef {object} MessageInteraction + * @property {Snowflake} id The ID of the interaction + * @property {InteractionType} type The type of the interaction + * @property {string} commandName The name of the interaction's application command + * @property {User} user The user that invoked the interaction + */ + + if (data.interaction) { + /** + * Partial data of the interaction that this message is a reply to + * @type {?MessageInteraction} + */ + this.interaction = { + id: data.interaction.id, + type: InteractionTypes[data.interaction.type], + commandName: data.interaction.name, + user: this.client.users.add(data.interaction.user), + }; + } else if (!this.interaction) { + this.interaction = null; + } } /** diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index d283035b7e50..a4d469beebd4 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -238,7 +238,7 @@ class Webhook { /** * Gets a message that was sent by this webhook. - * @param {Snowflake} message The ID of the message to fetch + * @param {Snowflake|'@original'} message The ID of the message to fetch * @param {boolean} [cache=true] Whether to cache the message * @returns {Promise} Returns the raw message data if the webhook was instantiated as a * {@link WebhookClient} or if the channel is uncached, otherwise a {@link Message} will be returned @@ -250,7 +250,7 @@ class Webhook { /** * Edits a message that was sent by this webhook. - * @param {MessageResolvable} message The message to edit + * @param {MessageResolvable|'@original'} message The message to edit * @param {StringResolvable|APIMessage} [content] The new content for the message * @param {WebhookEditMessageOptions|MessageEmbed|MessageEmbed[]} [options] The options to provide * @returns {Promise} Returns the raw message data if the webhook was instantiated as a @@ -287,7 +287,7 @@ class Webhook { /** * Delete a message that was sent by this webhook. - * @param {MessageResolvable} message The message to delete + * @param {MessageResolvable|'@original'} message The message to delete * @returns {Promise} */ async deleteMessage(message) { diff --git a/src/util/Constants.js b/src/util/Constants.js index 7516b78ff24c..bf6e00f7c95e 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -271,6 +271,7 @@ exports.Events = { TYPING_START: 'typingStart', TYPING_STOP: 'typingStop', WEBHOOKS_UPDATE: 'webhookUpdate', + INTERACTION_CREATE: 'interaction', ERROR: 'error', WARN: 'warn', DEBUG: 'debug', @@ -343,6 +344,7 @@ exports.PartialTypes = keyMirror(['USER', 'CHANNEL', 'GUILD_MEMBER', 'MESSAGE', * * VOICE_STATE_UPDATE * * VOICE_SERVER_UPDATE * * WEBHOOKS_UPDATE + * * INTERACTION_CREATE * @typedef {string} WSEventType */ exports.WSEvents = keyMirror([ @@ -382,6 +384,7 @@ exports.WSEvents = keyMirror([ 'VOICE_STATE_UPDATE', 'VOICE_SERVER_UPDATE', 'WEBHOOKS_UPDATE', + 'INTERACTION_CREATE', ]); /** @@ -434,6 +437,7 @@ exports.InviteScopes = [ * * GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING * * GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING * * REPLY + * * APPLICATION_COMMAND * @typedef {string} MessageType */ exports.MessageTypes = [ @@ -457,15 +461,19 @@ exports.MessageTypes = [ 'GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING', null, 'REPLY', + 'APPLICATION_COMMAND', ]; /** * The types of messages that are `System`. The available types are `MessageTypes` excluding: * * DEFAULT * * REPLY + * * APPLICATION_COMMAND * @typedef {string} SystemMessageType */ -exports.SystemMessageTypes = exports.MessageTypes.filter(type => type && type !== 'DEFAULT' && type !== 'REPLY'); +exports.SystemMessageTypes = exports.MessageTypes.filter( + type => type && !['DEFAULT', 'REPLY', 'APPLICATION_COMMAND'].includes(type), +); /** * Bots cannot set a `CUSTOM_STATUS`, it is only for custom statuses received from users @@ -742,6 +750,64 @@ exports.StickerFormatTypes = createEnum([null, 'PNG', 'APNG', 'LOTTIE']); */ exports.OverwriteTypes = createEnum(['role', 'member']); +/** + * The type of an {@link ApplicationCommandOption} object: + * * SUB_COMMAND + * * SUB_COMMAND_GROUP + * * STRING + * * INTEGER + * * BOOLEAN + * * USER + * * CHANNEL + * * ROLE + * * MENTIONABLE + * @typedef {string} ApplicationCommandOptionType + */ +exports.ApplicationCommandOptionTypes = createEnum([ + null, + 'SUB_COMMAND', + 'SUB_COMMAND_GROUP', + 'STRING', + 'INTEGER', + 'BOOLEAN', + 'USER', + 'CHANNEL', + 'ROLE', + 'MENTIONABLE', +]); + +/** + * The type of an {@link ApplicationCommandPermissions} object: + * * ROLE + * * USER + * @typedef {string} ApplicationCommandPermissionType + */ +exports.ApplicationCommandPermissionTypes = createEnum([null, 'ROLE', 'USER']); + +/** + * The type of an {@link Interaction} object: + * * PING + * * APPLICATION_COMMAND + * @typedef {string} InteractionType + */ +exports.InteractionTypes = createEnum([null, 'PING', 'APPLICATION_COMMAND']); + +/** + * The type of an interaction response: + * * PONG + * * CHANNEL_MESSAGE_WITH_SOURCE + * * DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE + * @typedef {string} InteractionResponseType + */ +exports.InteractionResponseTypes = createEnum([ + null, + 'PONG', + null, + null, + 'CHANNEL_MESSAGE_WITH_SOURCE', + 'DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE', +]); + function keyMirror(arr) { let tmp = Object.create(null); for (const value of arr) tmp[value] = value; diff --git a/src/util/MessageFlags.js b/src/util/MessageFlags.js index ca4265555f89..36c11ab60625 100644 --- a/src/util/MessageFlags.js +++ b/src/util/MessageFlags.js @@ -28,6 +28,8 @@ class MessageFlags extends BitField {} * * `SUPPRESS_EMBEDS` * * `SOURCE_MESSAGE_DELETED` * * `URGENT` + * * `EPHEMERAL` + * * `LOADING` * @type {Object} * @see {@link https://discord.com/developers/docs/resources/channel#message-object-message-flags} */ @@ -37,6 +39,8 @@ MessageFlags.FLAGS = { SUPPRESS_EMBEDS: 1 << 2, SOURCE_MESSAGE_DELETED: 1 << 3, URGENT: 1 << 4, + EPHEMERAL: 1 << 6, + LOADING: 1 << 7, }; module.exports = MessageFlags; diff --git a/src/util/Permissions.js b/src/util/Permissions.js index 249c8ba767ea..bc6ef20aedea 100644 --- a/src/util/Permissions.js +++ b/src/util/Permissions.js @@ -78,6 +78,7 @@ class Permissions extends BitField { * * `MANAGE_ROLES` * * `MANAGE_WEBHOOKS` * * `MANAGE_EMOJIS` + * * `USE_APPLICATION_COMMANDS` * * `REQUEST_TO_SPEAK` * @type {Object} * @see {@link https://discord.com/developers/docs/topics/permissions} @@ -114,6 +115,7 @@ Permissions.FLAGS = { MANAGE_ROLES: 1n << 28n, MANAGE_WEBHOOKS: 1n << 29n, MANAGE_EMOJIS: 1n << 30n, + USE_APPLICATION_COMMANDS: 1n << 31n, REQUEST_TO_SPEAK: 1n << 32n, }; diff --git a/src/util/Structures.js b/src/util/Structures.js index e850821b4f84..1fcac228313d 100644 --- a/src/util/Structures.js +++ b/src/util/Structures.js @@ -19,6 +19,7 @@ * * **`VoiceState`** * * **`Role`** * * **`User`** + * * **`CommandInteraction`** * @typedef {string} ExtendableStructure */ @@ -109,6 +110,7 @@ const structures = { VoiceState: require('../structures/VoiceState'), Role: require('../structures/Role'), User: require('../structures/User'), + CommandInteraction: require('../structures/CommandInteraction'), }; module.exports = Structures; diff --git a/typings/index.d.ts b/typings/index.d.ts index c217855c8f26..9df7bc58f126 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -21,10 +21,15 @@ declare enum ChannelTypes { STAGE = 13, } -declare enum StickerFormatTypes { - PNG = 1, - APNG = 2, - LOTTIE = 3, +declare enum InteractionResponseTypes { + PONG = 1, + CHANNEL_MESSAGE_WITH_SOURCE = 4, + DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5, +} + +declare enum InteractionTypes { + PING = 1, + APPLICATION_COMMAND = 2, } declare enum OverwriteTypes { @@ -32,10 +37,25 @@ declare enum OverwriteTypes { member = 1, } +declare enum StickerFormatTypes { + PNG = 1, + APNG = 2, + LOTTIE = 3, +} + declare module 'discord.js' { import BaseCollection from '@discordjs/collection'; import { ChildProcess } from 'child_process'; - import { APIMessage as RawMessage, APIOverwrite as RawOverwrite } from 'discord-api-types/v8'; + import { + ApplicationCommandOptionType as ApplicationCommandOptionTypes, + ApplicationCommandPermissionType as ApplicationCommandPermissionTypes, + APIInteractionDataResolvedChannel as RawInteractionDataResolvedChannel, + APIInteractionDataResolvedGuildMember as RawInteractionDataResolvedGuildMember, + APIInteractionGuildMember as RawInteractionGuildMember, + APIMessage as RawMessage, + APIOverwrite as RawOverwrite, + APIRole as RawRole, + } from 'discord-api-types/v8'; import { EventEmitter } from 'events'; import { PathLike } from 'fs'; import { Readable, Stream, Writable } from 'stream'; @@ -85,6 +105,7 @@ declare module 'discord.js' { public readonly isUser: boolean; public readonly isWebhook: boolean; public readonly isMessage: boolean; + public readonly isInteraction: boolean; public files: object[] | null; public options: MessageOptions | WebhookMessageOptions; public target: MessageTarget; @@ -139,6 +160,24 @@ declare module 'discord.js' { public toString(): string | null; } + export class ApplicationCommand extends Base { + constructor(client: Client, data: object, guild?: Guild); + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public defaultPermission: boolean; + public description: string; + public guild: Guild | null; + public readonly manager: ApplicationCommandManager; + public id: Snowflake; + public name: string; + public options: ApplicationCommandOption[]; + public delete(): Promise; + public edit(data: ApplicationCommandData): Promise; + public fetchPermissions(): Promise; + public setPermissions(permissions: ApplicationCommandPermissionData[]): Promise; + private static transformOption(option: ApplicationCommandOptionData, received?: boolean): object; + } + export class ApplicationFlags extends BitField { public static FLAGS: Record; public static resolve(bit?: BitFieldResolvable): number; @@ -300,6 +339,7 @@ declare module 'discord.js' { export class ClientApplication extends Application { public botPublic: boolean | null; public botRequireCodeGrant: boolean | null; + public commands: ApplicationCommandManager; public cover: string | null; public flags: Readonly; public owner: User | Team | null; @@ -363,6 +403,26 @@ declare module 'discord.js' { public once(event: 'end', listener: (collected: Collection, reason: string) => void): this; } + export class CommandInteraction extends Interaction { + public readonly command: ApplicationCommand | null; + public commandID: string; + public commandName: string; + public deferred: boolean; + public options: CommandInteractionOption[]; + public replied: boolean; + public webhook: WebhookClient; + public defer(ephemeral?: boolean): Promise; + public deleteReply(): Promise; + public editReply( + content: string | APIMessage | WebhookEditMessageOptions | MessageEmbed | MessageEmbed[], + ): Promise; + public editReply(content: string, options?: WebhookEditMessageOptions): Promise; + public fetchReply(): Promise; + public reply(content: string | APIMessage | InteractionReplyOptions | MessageAdditions): Promise; + public reply(content: string, options?: InteractionReplyOptions): Promise; + private transformOption(option: object, resolved: object): CommandInteractionOption; + } + type AllowedImageFormat = 'webp' | 'png' | 'jpg' | 'jpeg' | 'gif'; export const Constants: { @@ -457,6 +517,7 @@ declare module 'discord.js' { VOICE_BROADCAST_UNSUBSCRIBE: 'unsubscribe'; TYPING_START: 'typingStart'; WEBHOOKS_UPDATE: 'webhookUpdate'; + INTERACTION_CREATE: 'interaction'; RECONNECTING: 'reconnecting'; ERROR: 'error'; WARN: 'warn'; @@ -569,6 +630,10 @@ declare module 'discord.js' { DefaultMessageNotifications: DefaultMessageNotifications[]; VerificationLevels: VerificationLevel[]; MembershipStates: 'INVITED' | 'ACCEPTED'; + ApplicationCommandOptionTypes: typeof ApplicationCommandOptionTypes; + ApplicationCommandPermissionTypes: typeof ApplicationCommandPermissionTypes; + InteractionTypes: typeof InteractionTypes; + InteractionResponseTypes: typeof InteractionResponseTypes; }; export class DataResolver { @@ -629,6 +694,7 @@ declare module 'discord.js' { public available: boolean; public banner: string | null; public channels: GuildChannelManager; + public commands: GuildApplicationCommandManager; public readonly createdAt: Date; public readonly createdTimestamp: number; public defaultMessageNotifications: DefaultMessageNotifications | number; @@ -967,6 +1033,24 @@ declare module 'discord.js' { public static resolve(bit?: BitFieldResolvable): number; } + export class Interaction extends Base { + constructor(client: Client, data: object); + public applicationID: Snowflake; + public readonly channel: Channel | null; + public channelID: Snowflake | null; + public readonly createdAt: Date; + public readonly createdTimestamp: number; + public readonly guild: Guild | null; + public guildID: Snowflake | null; + public id: Snowflake; + public member: GuildMember | RawInteractionGuildMember | null; + public readonly token: string; + public type: InteractionType; + public user: User; + public version: number; + public isCommand(): this is CommandInteraction; + } + export class Invite extends Base { constructor(client: Client, data: object); public channel: GuildChannel | PartialGroupDMChannel; @@ -1015,6 +1099,7 @@ declare module 'discord.js' { public embeds: MessageEmbed[]; public readonly guild: Guild | null; public id: Snowflake; + public interaction: MessageInteraction | null; public readonly member: GuildMember | null; public mentions: MessageMentions; public nonce: string | number | null; @@ -1378,7 +1463,7 @@ declare module 'discord.js' { public eval(fn: (client: Client) => T): Promise; public fetchClientValue(prop: string): Promise; public kill(): void; - public respawn(options?: { delay?: number, timeout?: number }): Promise; + public respawn(options?: { delay?: number; timeout?: number }): Promise; public send(message: any): Promise; public spawn(timeout?: number): Promise; @@ -1411,7 +1496,7 @@ declare module 'discord.js' { public broadcastEval(fn: (client: Client) => T, shard: number): Promise; public fetchClientValues(prop: string): Promise; public fetchClientValues(prop: string, shard: number): Promise; - public respawnAll(options?: { shardDelay?: number, respawnDelay?: number, timeout?: number }): Promise; + public respawnAll(options?: { shardDelay?: number; respawnDelay?: number; timeout?: number }): Promise; public send(message: any): Promise; public static singleton(client: Client, mode: ShardingManagerMode): ShardClientUtil; @@ -1448,11 +1533,15 @@ declare module 'discord.js' { public fetchClientValues(prop: string): Promise; public fetchClientValues(prop: string, shard: number): Promise; public respawnAll(options?: { - shardDelay?: number, - respawnDelay?: number, - timeout?: number, + shardDelay?: number; + respawnDelay?: number; + timeout?: number; + }): Promise>; + public spawn(options?: { + amount?: number | 'auto'; + delay?: number; + timeout?: number; }): Promise>; - public spawn(options?: { amount?: number | 'auto', delay?: number, timeout?: number }): Promise>; public on(event: 'shardCreate', listener: (shard: Shard) => void): this; @@ -1963,6 +2052,32 @@ declare module 'discord.js' { public valueOf(): Collection; } + export class ApplicationCommandManager extends BaseManager< + Snowflake, + ApplicationCommand, + ApplicationCommandResolvable + > { + constructor(client: Client, iterable?: Iterable); + private readonly commandPath: object; + public create(command: ApplicationCommandData): Promise; + public delete(command: ApplicationCommandResolvable): Promise; + public edit(command: ApplicationCommandResolvable, data: ApplicationCommandData): Promise; + public fetch(id: Snowflake, cache?: boolean, force?: boolean): Promise; + public fetch(id?: Snowflake, cache?: boolean, force?: boolean): Promise>; + public fetchPermissions(): Promise>; + public fetchPermissions(command: ApplicationCommandResolvable): Promise; + public set(commands: ApplicationCommandData[]): Promise>; + public setPermissions( + command: ApplicationCommandResolvable, + permissions: ApplicationCommandPermissionData[], + ): Promise; + public setPermissions( + permissions: GuildApplicationCommandPermissionData[], + ): Promise>; + private static transformCommand(command: ApplicationCommandData): object; + private static transformPermissions(permissions: ApplicationCommandPermissionData, received?: boolean): object; + } + export class BaseGuildEmojiManager extends BaseManager { constructor(client: Client, iterable?: Iterable); public resolveIdentifier(emoji: EmojiIdentifierResolvable): string | null; @@ -1973,6 +2088,11 @@ declare module 'discord.js' { public fetch(id: Snowflake, cache?: boolean, force?: boolean): Promise; } + export class GuildApplicationCommandManager extends ApplicationCommandManager { + constructor(guild: Guild, iterable?: Iterable); + public guild: Guild; + } + export class GuildChannelManager extends BaseManager { constructor(guild: Guild, iterable?: Iterable); public guild: Guild; @@ -2167,15 +2287,18 @@ declare module 'discord.js' { readonly createdTimestamp: number; readonly url: string; delete(reason?: string): Promise; - deleteMessage(message: MessageResolvable): Promise; + deleteMessage(message: MessageResolvable | '@original'): Promise; edit(options: WebhookEditData): Promise; editMessage( - message: MessageResolvable, + message: MessageResolvable | '@original', content: APIMessageContentResolvable | APIMessage | MessageEmbed | MessageEmbed[], options?: WebhookEditMessageOptions, ): Promise; - editMessage(message: MessageResolvable, options: WebhookEditMessageOptions): Promise; - fetchMessage(message: Snowflake, cache?: boolean): Promise; + editMessage( + message: MessageResolvable | '@original', + options: WebhookEditMessageOptions, + ): Promise; + fetchMessage(message: Snowflake | '@original', cache?: boolean): Promise; send( content: APIMessageContentResolvable | (WebhookMessageOptions & { split?: false }) | MessageAdditions, ): Promise; @@ -2299,6 +2422,47 @@ declare module 'discord.js' { type: 'BIG' | 'SMALL'; } + interface ApplicationCommandData { + name: string; + description: string; + options?: ApplicationCommandOptionData[]; + defaultPermission?: boolean; + } + + interface ApplicationCommandOptionData { + type: ApplicationCommandOptionType | ApplicationCommandOptionTypes; + name: string; + description: string; + required?: boolean; + choices?: ApplicationCommandOptionChoice[]; + options?: ApplicationCommandOption[]; + } + + interface ApplicationCommandOption extends ApplicationCommandOptionData { + type: ApplicationCommandOptionType; + } + + interface ApplicationCommandOptionChoice { + name: string; + value: string | number; + } + + type ApplicationCommandOptionType = keyof typeof ApplicationCommandOptionTypes; + + interface ApplicationCommandPermissionData { + id: Snowflake; + type: ApplicationCommandPermissionType | ApplicationCommandPermissionTypes; + permission: boolean; + } + + interface ApplicationCommandPermissions extends ApplicationCommandPermissionData { + type: ApplicationCommandPermissionType; + } + + type ApplicationCommandPermissionType = keyof typeof ApplicationCommandPermissionTypes; + + type ApplicationCommandResolvable = ApplicationCommand | Snowflake; + type ApplicationFlagsString = | 'MANAGED_EMOJI' | 'GROUP_DM_CREATE' @@ -2428,6 +2592,7 @@ declare module 'discord.js' { userUpdate: [oldUser: User | PartialUser, newUser: User]; voiceStateUpdate: [oldState: VoiceState, newState: VoiceState]; webhookUpdate: [channel: TextChannel]; + interaction: [interaction: Interaction]; shardDisconnect: [closeEvent: CloseEvent, shardID: number]; shardError: [error: Error, shardID: number]; shardReady: [shardID: number, unavailableGuilds: Set | undefined]; @@ -2519,6 +2684,17 @@ declare module 'discord.js' { | number | string; + interface CommandInteractionOption { + name: string; + type: ApplicationCommandOptionType; + value?: string | number | boolean; + options?: CommandInteractionOption[]; + user?: User; + member?: GuildMember | RawInteractionDataResolvedGuildMember; + channel?: GuildChannel | RawInteractionDataResolvedChannel; + role?: Role | RawRole; + } + interface CrosspostedChannel { channelID: Snowflake; guildID: Snowflake; @@ -2590,6 +2766,7 @@ declare module 'discord.js' { VoiceState: typeof VoiceState; Role: typeof Role; User: typeof User; + CommandInteraction: typeof CommandInteraction; } interface FetchMemberOptions { @@ -2608,12 +2785,17 @@ declare module 'discord.js' { force?: boolean; } + type FetchOwnerOptions = Omit; + interface FileOptions { attachment: BufferResolvable | Stream; name?: string; } - type FetchOwnerOptions = Omit; + interface GuildApplicationCommandPermissionData { + id: Snowflake; + permissions: ApplicationCommandPermissionData[]; + } type GuildAuditLogsAction = keyof GuildAuditLogsActions; @@ -2834,6 +3016,14 @@ declare module 'discord.js' { name: string; } + interface InteractionReplyOptions extends Omit { + ephemeral?: boolean; + } + + type InteractionResponseType = keyof typeof InteractionResponseTypes; + + type InteractionType = keyof typeof InteractionTypes; + type IntentsString = | 'GUILDS' | 'GUILD_MEMBERS' @@ -2965,7 +3155,21 @@ declare module 'discord.js' { target: WebSocket; } - type MessageFlagsString = 'CROSSPOSTED' | 'IS_CROSSPOST' | 'SUPPRESS_EMBEDS' | 'SOURCE_MESSAGE_DELETED' | 'URGENT'; + type MessageFlagsString = + | 'CROSSPOSTED' + | 'IS_CROSSPOST' + | 'SUPPRESS_EMBEDS' + | 'SOURCE_MESSAGE_DELETED' + | 'URGENT' + | 'EPHEMERAL' + | 'LOADING'; + + interface MessageInteraction { + id: Snowflake; + type: InteractionType; + commandName: string; + user: User; + } interface MessageMentionOptions { parse?: MessageMentionTypes[]; @@ -3018,7 +3222,8 @@ declare module 'discord.js' { | 'GUILD_DISCOVERY_REQUALIFIED' | 'GUILD_DISCOVERY_GRACE_PERIOD_INITIAL_WARNING' | 'GUILD_DISCOVERY_GRACE_PERIOD_FINAL_WARNING' - | 'REPLY'; + | 'REPLY' + | 'APPLICATION_COMMAND'; interface OverwriteData { allow?: PermissionResolvable; @@ -3071,6 +3276,7 @@ declare module 'discord.js' { | 'MANAGE_ROLES' | 'MANAGE_WEBHOOKS' | 'MANAGE_EMOJIS' + | 'USE_APPLICATION_COMMANDS' | 'REQUEST_TO_SPEAK'; interface RecursiveArray extends ReadonlyArray> {} @@ -3309,7 +3515,7 @@ declare module 'discord.js' { type SystemChannelFlagsResolvable = BitFieldResolvable; - type SystemMessageType = Exclude; + type SystemMessageType = Exclude; type TargetUser = number; @@ -3360,11 +3566,11 @@ declare module 'discord.js' { type WebhookEditMessageOptions = Pick; - type WebhookMessageOptions = Omit & { + interface WebhookMessageOptions extends Omit { username?: string; avatarURL?: string; embeds?: (MessageEmbed | object)[]; - }; + } type WebhookTypes = 'Incoming' | 'Channel Follower'; @@ -3416,7 +3622,8 @@ declare module 'discord.js' { | 'TYPING_START' | 'VOICE_STATE_UPDATE' | 'VOICE_SERVER_UPDATE' - | 'WEBHOOKS_UPDATE'; + | 'WEBHOOKS_UPDATE' + | 'INTERACTION_CREATE'; //#endregion }