diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 70f713ddd947..f615639e0f08 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -110,7 +110,8 @@ const Messages = { MEMBER_FETCH_NONCE_LENGTH: 'Nonce length must not exceed 32 characters.', GLOBAL_COMMAND_PERMISSIONS: - "Permissions for global commands may only be fetched or modified from a guild's application command manager.", + 'Permissions for global commands may only be fetched or modified by providing a guildID' + + "or from a guild's application command manager.", INTERACTION_ALREADY_REPLIED: 'This interaction has already been deferred or replied to.', }; diff --git a/src/managers/ApplicationCommandManager.js b/src/managers/ApplicationCommandManager.js index 2bfefc76446a..6a214d817dab 100644 --- a/src/managers/ApplicationCommandManager.js +++ b/src/managers/ApplicationCommandManager.js @@ -1,9 +1,10 @@ 'use strict'; const BaseManager = require('./BaseManager'); -const { TypeError } = require('../errors'); +const { Error, 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. @@ -26,14 +27,16 @@ class ApplicationCommandManager extends BaseManager { /** * The APIRouter path to the commands - * @type {Object} - * @readonly + * @param {Snowflake} [options.id] ID of the application command + * @param {Snowflake} [options.guildID] ID of the guild to use in the path, + * ignored when using a {@link GuildApplicationCommandManager} + * @returns {Object} * @private */ - get commandPath() { + commandPath({ id, guildID } = {}) { let path = this.client.api.applications(this.client.application.id); - if (this.guild) path = path.guilds(this.guild.id); - return path.commands; + if (this.guild || guildID) path = path.guilds(this.guild?.id ?? guildID); + return id ? path.commands(id) : path.commands; } /** @@ -43,11 +46,23 @@ class ApplicationCommandManager extends BaseManager { * @typedef {ApplicationCommand|Snowflake} ApplicationCommandResolvable */ + /** + * Options used to fetch data from discord + * @typedef {Object} BaseFetchOptions + * @property {boolean} [cache=true] Whether to cache the fetched data if it wasn't already + * @property {boolean} [force=false] Whether to skip the cache check and request the API + */ + + /** + * Options used to fetch Application Commands from discord + * @typedef {BaseFetchOptions} FetchApplicationCommandOptions + * @property {Snowflake} [guildID] ID of the guild to fetch commands for, for when the guild is not cached + */ + /** * 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 + * @param {FetchApplicationCommandOptions} [options] Additional options for this fetch * @returns {Promise>} * @example * // Fetch a single command @@ -60,23 +75,29 @@ class ApplicationCommandManager extends BaseManager { * .then(commands => console.log(`Fetched ${commands.size} commands`)) * .catch(console.error); */ - async fetch(id, cache = true, force = false) { + async fetch(id, { guildID, cache = true, force = false } = {}) { + if (typeof id === 'object') { + ({ guildID, cache = true, force = false } = id); + id = undefined; + } if (id) { if (!force) { const existing = this.cache.get(id); if (existing) return existing; } - const command = await this.commandPath(id).get(); + const command = await this.commandPath({ id, guildID }).get(); return this.add(command, cache); } - const data = await this.commandPath.get(); + const data = await this.commandPath({ guildID }).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 + * @param {Snowflake} [guildID] ID of the guild to create this command in, + * ignored when using a {@link GuildApplicationCommandManager} * @returns {Promise} * @example * // Create a new command @@ -87,8 +108,8 @@ class ApplicationCommandManager extends BaseManager { * .then(console.log) * .catch(console.error); */ - async create(command) { - const data = await this.commandPath.post({ + async create(command, guildID) { + const data = await this.commandPath({ guildID }).post({ data: this.constructor.transformCommand(command), }); return this.add(data); @@ -97,6 +118,8 @@ class ApplicationCommandManager extends BaseManager { /** * Sets all the commands for this application or guild. * @param {ApplicationCommandData[]} commands The commands + * @param {Snowflake} [guildID] ID of the guild to create the commands in, + * ignored when using a {@link GuildApplicationCommandManager} * @returns {Promise>} * @example * // Set all commands to just this one @@ -114,8 +137,8 @@ class ApplicationCommandManager extends BaseManager { * .then(console.log) * .catch(console.error); */ - async set(commands) { - const data = await this.commandPath.put({ + async set(commands, guildID) { + const data = await this.commandPath({ guildID }).put({ data: commands.map(c => this.constructor.transformCommand(c)), }); return data.reduce((coll, command) => coll.set(command.id, this.add(command)), new Collection()); @@ -125,6 +148,8 @@ class ApplicationCommandManager extends BaseManager { * Edits an application command. * @param {ApplicationCommandResolvable} command The command to edit * @param {ApplicationCommandData} data The data to update the command with + * @param {Snowflake} [guildID] ID of the guild where the command registered, + * ignored when using a {@link GuildApplicationCommandManager} * @returns {Promise} * @example * // Edit an existing command @@ -134,17 +159,19 @@ class ApplicationCommandManager extends BaseManager { * .then(console.log) * .catch(console.error); */ - async edit(command, data) { + async edit(command, data, guildID) { 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) }); + const patched = await this.commandPath({ id, guildID }).patch({ data: this.constructor.transformCommand(data) }); return this.add(patched); } /** * Deletes an application command. * @param {ApplicationCommandResolvable} command The command to delete + * @param {Snowflake} [guildID] ID of the guild where the command is registered, + * ignored when using a {@link GuildApplicationCommandManager} * @returns {Promise} * @example * // Delete a command @@ -152,11 +179,11 @@ class ApplicationCommandManager extends BaseManager { * .then(console.log) * .catch(console.error); */ - async delete(command) { + async delete(command, guildID) { const id = this.resolveID(command); if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); - await this.commandPath(id).delete(); + await this.commandPath({ id, guildID }).delete(); const cached = this.cache.get(id); this.cache.delete(id); @@ -177,6 +204,141 @@ class ApplicationCommandManager extends BaseManager { default_permission: command.defaultPermission, }; } + + /** + * Fetches the permissions for one or multiple commands. + * When calling this on ApplicationCommandManager, guildID is required. + * To fetch all permissions for an uncached guild use `fetchPermissions(undefined, '123456789012345678')` + * @param {ApplicationCommandResolvable} [command] The command to get the permissions from + * @param {Snowflake} [guildID] ID of the guild to get the permissions for, + * ignored when using a {@link GuildApplicationCommandManager} + * @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, guildID) { + if (!this.guild && !guildID) throw new Error('GLOBAL_COMMAND_PERMISSIONS'); + if (command) { + const id = this.resolveID(command); + if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + + const data = await this.commandPath({ id, guildID }).permissions.get(); + return data.permissions.map(perm => this.constructor.transformPermissions(perm, true)); + } + + const data = await this.commandPath({ guildID }).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} id The ID of the command + * @prop {ApplicationCommandPermissionData[]} permissions The permissions for this command + */ + + /** + * Sets the permissions for a command. + * When calling this on ApplicationCommandManager, guildID is required. + * To set multiple permissions for an uncached guild use `setPermissions(permissions, '123456789012345678')` + * @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 + * @param {Snowflake} [guildID] ID of the guild to get the permissions for, + * ignored when using a {@link GuildApplicationCommandManager} + * @returns {Promise>} + * @example + * // Set the permissions for one command + * client.application.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, guildID) { + const id = this.resolveID(command); + + if (id) { + if (!this.guild && !guildID) throw new Error('GLOBAL_COMMAND_PERMISSIONS'); + const data = await this.commandPath({ id, guildID }).permissions.put({ + data: { permissions: permissions.map(perm => this.constructor.transformPermissions(perm)) }, + }); + return data.permissions.map(perm => this.constructor.transformPermissions(perm, true)); + } + + if (typeof permissions === 'string') { + guildID = permissions; + permissions = undefined; + } + + if (!this.guild && !guildID) throw new Error('GLOBAL_COMMAND_PERMISSIONS'); + + const data = await this.commandPath({ guildID }).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 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 index 910cbfd7680d..3a78fbf7f864 100644 --- a/src/managers/GuildApplicationCommandManager.js +++ b/src/managers/GuildApplicationCommandManager.js @@ -1,9 +1,6 @@ 'use strict'; const ApplicationCommandManager = require('./ApplicationCommandManager'); -const { TypeError } = require('../errors'); -const Collection = require('../util/Collection'); -const { ApplicationCommandPermissionTypes } = require('../util/Constants'); /** * An extension for guild-specific application commands. @@ -19,124 +16,6 @@ class GuildApplicationCommandManager extends ApplicationCommandManager { */ this.guild = guild; } - - /** - * 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.application.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 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 = GuildApplicationCommandManager; diff --git a/src/structures/ApplicationCommand.js b/src/structures/ApplicationCommand.js index 652de0043937..88159d127c88 100644 --- a/src/structures/ApplicationCommand.js +++ b/src/structures/ApplicationCommand.js @@ -1,7 +1,6 @@ 'use strict'; const Base = require('./Base'); -const { Error } = require('../errors'); const { ApplicationCommandOptionTypes } = require('../util/Constants'); const SnowflakeUtil = require('../util/SnowflakeUtil'); @@ -148,7 +147,9 @@ class ApplicationCommand extends Base { /** * Fetches the permissions for this command. - * This is only available for guild application commands. + * You must specify guildID if this command is handled by a {@link ApplicationCommandManager}, + * including commands fetched for arbitrary guilds from it, otherwise it is ignored. + * @param {Snowflake} [guildID] ID for the guild to fetch permissions for if this is a global command * @returns {Promise} * @example * // Fetch permissions for this command @@ -156,15 +157,16 @@ class ApplicationCommand extends Base { * .then(perms => console.log(`Fetched permissions for ${perms.length} users`)) * .catch(console.error); */ - fetchPermissions() { - if (!this.guild) throw new Error('GLOBAL_COMMAND_PERMISSIONS'); - return this.manager.fetchPermissions(this); + fetchPermissions(guildID) { + return this.manager.fetchPermissions(this, guildID); } /** * Sets the permissions for this command. - * This is only available for guild application commands. + * You must specify guildID if this command is handled by a {@link ApplicationCommandManager}, + * including commands fetched for arbitrary guilds from it, otherwise it is ignored. * @param {ApplicationCommandPermissionData[]} permissions The new permissions for the command + * @param {Snowflake} [guildID] ID for the guild to fetch permissions for if this is a global command * @returns {Promise} * @example * // Set the permissions for this command @@ -178,9 +180,8 @@ class ApplicationCommand extends Base { * .then(console.log) * .catch(console.error); */ - setPermissions(permissions) { - if (!this.guild) throw new Error('GLOBAL_COMMAND_PERMISSIONS'); - return this.manager.setPermissions(this, permissions); + setPermissions(permissions, guildID) { + return this.manager.setPermissions(this, permissions, guildID); } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index f57de118e2f6..0a72b621a612 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -189,8 +189,11 @@ declare module 'discord.js' { public options: ApplicationCommandOption[]; public delete(): Promise; public edit(data: ApplicationCommandData): Promise; - public fetchPermissions(): Promise; - public setPermissions(permissions: ApplicationCommandPermissionData[]): Promise; + public fetchPermissions(guildID?: Snowflake): Promise; + public setPermissions( + permissions: ApplicationCommandPermissionData[], + guildID?: Snowflake, + ): Promise; private static transformOption(option: ApplicationCommandOptionData, received?: boolean): unknown; } @@ -2059,14 +2062,42 @@ declare module 'discord.js' { ApplicationCommandResolvable > { constructor(client: Client, iterable?: Iterable); - private readonly commandPath: unknown; - 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 set(commands: ApplicationCommandData[]): Promise>; + private commandPath({ id, guildID }: { id?: Snowflake; guildID?: Snowflake }): unknown; + public create(command: ApplicationCommandData, guildID?: Snowflake): Promise; + public delete(command: ApplicationCommandResolvable, guildID?: Snowflake): Promise; + public edit( + command: ApplicationCommandResolvable, + data: ApplicationCommandData, + guildID?: Snowflake, + ): Promise; + public fetch(id: Snowflake, options?: FetchApplicationCommandOptions): Promise; + public fetch( + id?: Snowflake, + options?: FetchApplicationCommandOptions, + ): Promise>; + public fetchPermissions( + command: undefined, + guildID: Snowflake, + ): Promise>; + public fetchPermissions( + command: ApplicationCommandResolvable, + guildID: Snowflake, + ): Promise; + public set( + commands: ApplicationCommandData[], + guildID?: Snowflake, + ): Promise>; + public setPermissions( + command: ApplicationCommandResolvable, + permissions: ApplicationCommandPermissionData[], + guildID: Snowflake, + ): Promise; + public setPermissions( + permissions: GuildApplicationCommandPermissionData[], + guildID: Snowflake, + ): Promise>; private static transformCommand(command: ApplicationCommandData): unknown; + private static transformPermissions(permissions: ApplicationCommandPermissionData, received?: boolean): unknown; } export class BaseGuildEmojiManager extends BaseManager { @@ -2082,8 +2113,14 @@ declare module 'discord.js' { export class GuildApplicationCommandManager extends ApplicationCommandManager { constructor(guild: Guild, iterable?: Iterable); public guild: Guild; - public fetchPermissions(): Promise>; + public create(command: ApplicationCommandData): Promise; + public delete(command: ApplicationCommandResolvable): Promise; + public edit(command: ApplicationCommandResolvable, data: ApplicationCommandData): Promise; + public fetch(id: Snowflake, options?: BaseFetchOptions): Promise; + public fetch(id?: Snowflake, options?: BaseFetchOptions): Promise>; + public fetchPermissions(command: undefined): Promise>; public fetchPermissions(command: ApplicationCommandResolvable): Promise; + public set(commands: ApplicationCommandData[]): Promise>; public setPermissions( command: ApplicationCommandResolvable, permissions: ApplicationCommandPermissionData[], @@ -2091,7 +2128,6 @@ declare module 'discord.js' { public setPermissions( permissions: GuildApplicationCommandPermissionData[], ): Promise>; - private static transformPermissions(permissions: ApplicationCommandPermissionData, received?: boolean): unknown; } export class GuildChannelManager extends BaseManager { @@ -2516,6 +2552,11 @@ declare module 'discord.js' { type Base64String = string; + interface BaseFetchOptions { + cache?: boolean; + force?: boolean; + } + interface BaseMessageComponentOptions { type?: MessageComponentType | MessageComponentTypes; } @@ -2807,6 +2848,10 @@ declare module 'discord.js' { ButtonInteraction: typeof ButtonInteraction; } + interface FetchApplicationCommandOptions extends BaseFetchOptions { + guildID?: Snowflake; + } + interface FetchBanOptions { user: UserResolvable; cache?: boolean;