From 6264c60e97da93b311a7a9fd92e16e59de94104a Mon Sep 17 00:00:00 2001 From: ckohen Date: Tue, 29 Jun 2021 11:19:48 -0700 Subject: [PATCH] feat(Managers): new ApplicationCommandPermissionsManager (#5897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: SpaceEEC <24881032+SpaceEEC@users.noreply.github.com> Co-authored-by: Yoshida Tomio Co-authored-by: Antonio Román Co-authored-by: Vlad Frangu --- src/errors/Messages.js | 3 +- src/managers/ApplicationCommandManager.js | 165 +------ .../ApplicationCommandPermissionsManager.js | 420 ++++++++++++++++++ .../GuildApplicationCommandManager.js | 7 + src/structures/ApplicationCommand.js | 75 +--- typings/index.d.ts | 115 +++-- 6 files changed, 542 insertions(+), 243 deletions(-) create mode 100644 src/managers/ApplicationCommandPermissionsManager.js diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 2e04d168094d..2bdec03e3b92 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -119,8 +119,9 @@ 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 by providing a guildID' + + 'Permissions for global commands may only be fetched or modified by providing a GuildResolvable ' + "or from a guild's application command manager.", + GUILD_UNCACHED_ROLE_RESOLVE: 'Cannot resolve roles from an arbitrary guild, provide an ID instead', INTERACTION_ALREADY_REPLIED: 'This interaction has already been deferred or replied to.', INTERACTION_NOT_REPLIED: 'This interaction has not been deferred or replied to.', diff --git a/src/managers/ApplicationCommandManager.js b/src/managers/ApplicationCommandManager.js index 8b5101473a27..63b4715c7d37 100644 --- a/src/managers/ApplicationCommandManager.js +++ b/src/managers/ApplicationCommandManager.js @@ -1,10 +1,10 @@ 'use strict'; +const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager'); const BaseManager = require('./BaseManager'); -const { Error, TypeError } = require('../errors'); +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. @@ -13,6 +13,12 @@ const { ApplicationCommandPermissionTypes } = require('../util/Constants'); class ApplicationCommandManager extends BaseManager { constructor(client, iterable) { super(client, iterable, ApplicationCommand); + + /** + * The manager for permissions of arbitrary commands on arbitrary guilds + * @type {ApplicationCommandPermissionsManager} + */ + this.permissions = new ApplicationCommandPermissionsManager(this); } /** @@ -21,8 +27,8 @@ class ApplicationCommandManager extends BaseManager { * @name ApplicationCommandManager#cache */ - add(data, cache) { - return super.add(data, cache, { extras: [this.guild] }); + add(data, cache, guildID) { + return super.add(data, cache, { extras: [this.guild, guildID] }); } /** @@ -90,7 +96,7 @@ class ApplicationCommandManager extends BaseManager { } const data = await this.commandPath({ guildID }).get(); - return data.reduce((coll, command) => coll.set(command.id, this.add(command, cache)), new Collection()); + return data.reduce((coll, command) => coll.set(command.id, this.add(command, cache, guildID)), new Collection()); } /** @@ -112,7 +118,7 @@ class ApplicationCommandManager extends BaseManager { const data = await this.commandPath({ guildID }).post({ data: this.constructor.transformCommand(command), }); - return this.add(data); + return this.add(data, undefined, guildID); } /** @@ -141,7 +147,10 @@ class ApplicationCommandManager extends BaseManager { 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()); + return data.reduce( + (coll, command) => coll.set(command.id, this.add(command, undefined, guildID)), + new Collection(), + ); } /** @@ -164,7 +173,7 @@ class ApplicationCommandManager extends BaseManager { if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); const patched = await this.commandPath({ id, guildID }).patch({ data: this.constructor.transformCommand(data) }); - return this.add(patched); + return this.add(patched, undefined, guildID); } /** @@ -204,146 +213,6 @@ 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 {APIApplicationCommandPermissions} - * @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; - -/** - * @external APIApplicationCommandPermissions - * @see {@link https://discord.com/developers/docs/interactions/slash-commands#applicationcommandpermissions} - */ diff --git a/src/managers/ApplicationCommandPermissionsManager.js b/src/managers/ApplicationCommandPermissionsManager.js new file mode 100644 index 000000000000..bf10454efb31 --- /dev/null +++ b/src/managers/ApplicationCommandPermissionsManager.js @@ -0,0 +1,420 @@ +'use strict'; + +const { Error, TypeError } = require('../errors'); +const Collection = require('../util/Collection'); +const { ApplicationCommandPermissionTypes, APIErrors } = require('../util/Constants'); + +/** + * Manages API methods for permissions of Application Commands. + */ +class ApplicationCommandPermissionsManager { + constructor(manager) { + /** + * The manager or command that this manager belongs to + * @type {ApplicationCommandManager|ApplicationCommand} + */ + this.manager = manager; + + /** + * The guild that this manager acts on + * @type {?Guild} + */ + this.guild = manager.guild ?? null; + + /** + * The id of the guild that this manager acts on + * @type {?Snowflake} + */ + this.guildID = manager.guildID ?? manager.guild?.id ?? null; + + /** + * The id of the command this manager acts on + * @type {?Snowflake} + */ + this.commandID = manager.id ?? null; + + /** + * The client that instantiated this Manager + * @name ApplicationCommandPermissionsManager#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: manager.client }); + } + + /** + * The APIRouter path to the commands + * @param {Snowflake} guildID ID of the guild to use in the path, + * @param {Snowflake} [commandID] ID of the application command + * @returns {Object} + * @private + */ + permissionsPath(guildID, commandID) { + return this.client.api.applications(this.client.application.id).guilds(guildID).commands(commandID).permissions; + } + + /** + * Data for setting the permissions of an application command. + * @typedef {Object} ApplicationCommandPermissionData + * @property {Snowflake} id The ID of the role or user + * @property {ApplicationCommandPermissionType|number} type Whether this permission is 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 is for a role or a user + * @property {boolean} permission Whether the role or user has the permission to use this command + */ + + /** + * Options for managing permissions for one or more Application Commands + * When passing these options to a manager where `guildID` is `null`, + * `guild` is a required parameter + * @typedef {Object} BaseApplicationCommandPermissionsOptions + * @param {GuildResolvable} [guild] The guild to modify / check permissions for + * Ignored when the manager has a non-null `guildID` property + * @param {ApplicationCommandResolvable} [command] The command to modify / check permissions for + * Ignored when the manager has a non-null `commandID` property + */ + + /** + * Fetches the permissions for one or multiple commands. + * @param {BaseApplicationCommandPermissionsOptions} [options] Options used to fetch permissions + * @returns {Promise>} + * @example + * // Fetch permissions for one command + * guild.commands.permissions.fetch({ command: '123456789012345678' }) + * .then(perms => console.log(`Fetched permissions for ${perms.length} users`)) + * .catch(console.error); + * @example + * // Fetch permissions for all commands in a guild + * client.application.commands.permissions.fetch({ guild: '123456789012345678' }) + * .then(perms => console.log(`Fetched permissions for ${perms.size} commands`)) + * .catch(console.error); + */ + async fetch({ guild, command } = {}) { + const { guildID, commandID } = this._validateOptions(guild, command); + if (commandID) { + const data = await this.permissionsPath(guildID, commandID).get(); + return data.permissions.map(perm => this.constructor.transformPermissions(perm, true)); + } + + const data = await this.permissionsPath(guildID).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 + * @property {Snowflake} id The ID of the command + * @property {ApplicationCommandPermissionData[]} permissions The permissions for this command + */ + + /** + * Options used to set permissions for one or more Application Commands in a guild + * One of `command` AND `permissions`, OR `fullPermissions` is required. + * `fullPermissions` is not a valid option when passing to a manager where `commandID` is non-null + * @typedef {BaseApplicationCommandPermissionsOptions} SetApplicationCommandPermissionsOptions + * @param {ApplicationCommandPermissionData[]} [permissions] The new permissions for the command + * @param {GuildApplicationCommandPermissionData[]} [fullPermissions] The new permissions for all commands + * in a guild When this parameter is set, permissions and command are ignored + */ + + /** + * Sets the permissions for one or more commands. + * @param {SetApplicationCommandPermissionsOptions} options Options used to set permissions + * @returns {Promise>} + * @example + * // Set the permissions for one command + * client.application.commands.permissions.set({ command: '123456789012345678', permissions: [ + * { + * id: '876543210987654321', + * type: 'USER', + * permission: false, + * }, + * ]}) + * .then(console.log) + * .catch(console.error); + * @example + * // Set the permissions for all commands + * guild.commands.permissions.set({ fullPermissions: [ + * { + * id: '123456789012345678', + * permissions: [{ + * id: '876543210987654321', + * type: 'USER', + * permission: false, + * }], + * }, + * ]}) + * .then(console.log) + * .catch(console.error); + */ + async set({ guild, command, permissions, fullPermissions } = {}) { + const { guildID, commandID } = this._validateOptions(guild, command); + + if (commandID) { + if (!Array.isArray(permissions)) { + throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true); + } + const data = await this.permissionsPath(guildID, commandID).put({ + data: { permissions: permissions.map(perm => this.constructor.transformPermissions(perm)) }, + }); + return data.permissions.map(perm => this.constructor.transformPermissions(perm, true)); + } + + if (!Array.isArray(fullPermissions)) { + throw new TypeError('INVALID_TYPE', 'fullPermissions', 'Array of GuildApplicationCommandPermissionData', true); + } + + const APIPermissions = []; + for (const perm of fullPermissions) { + if (!Array.isArray(perm.permissions)) throw new TypeError('INVALID_ELEMENT', 'Array', 'fullPermissions', perm); + APIPermissions.push({ + id: perm.id, + permissions: perm.permissions.map(p => this.constructor.transformPermissions(p)), + }); + } + const data = await this.permissionsPath(guildID).put({ + data: APIPermissions, + }); + return data.reduce( + (coll, perm) => + coll.set( + perm.id, + perm.permissions.map(p => this.constructor.transformPermissions(p, true)), + ), + new Collection(), + ); + } + + /** + * Options used to add permissions to a command + * The `command` parameter is not optional when the managers `commandID` is `null` + * @typedef {BaseApplicationCommandPermissionsOptions} AddApplicationCommandPermissionsOptions + * @param {ApplicationCommandPermissionData[]} permissions The permissions to add to the command + */ + + /** + * Add permissions to a command. + * @param {AddApplicationCommandPermissionsOptions} options Options used to add permissions + * @returns {Promise} + * @example + * // Block a role from the command permissions + * guild.commands.permissions.add({ command: '123456789012345678', permissions: [ + * { + * id: '876543211234567890', + * type: 'ROLE', + * permission: false + * }, + * ]}) + * .then(console.log) + * .catch(console.error); + */ + async add({ guild, command, permissions }) { + const { guildID, commandID } = this._validateOptions(guild, command); + if (!commandID) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + if (!Array.isArray(permissions)) { + throw new TypeError('INVALID_TYPE', 'permissions', 'Array of ApplicationCommandPermissionData', true); + } + + let existing = []; + try { + existing = await this.fetch({ guild: guildID, command: commandID }); + } catch (error) { + if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; + } + + const newPermissions = permissions.slice(); + for (const perm of existing) { + if (!newPermissions.some(x => x.id === perm.id)) { + newPermissions.push(perm); + } + } + + return this.set({ guild: guildID, command: commandID, permissions: newPermissions }); + } + + /** + * Options used to remove permissions from a command + * The `command` parameter is not optional when the managers `commandID` is `null` + * @typedef {BaseApplicationCommandPermissionsOptions} RemoveApplicationCommandPermissionsOptions + * @param {UserResolvable|UserResolvable[]} [users] The user(s) to remove from the command permissions + * One of `users` or `roles` is required + * @param {RoleResolvable|RoleResolvable[]} [roles] The role(s) to remove from the command permissions + * One of `users` or `roles` is required + */ + + /** + * Remove permissions from a command. + * @param {RemoveApplicationCommandPermissionsOptions} options Options used to remove permissions + * @returns {Promise} + * @example + * // Remove a user permission from this command + * guild.commands.permissions.remove({ command: '123456789012345678', users: '876543210123456789' }) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove multiple roles from this command + * guild.commands.permissions.remove({ + * command: '123456789012345678', roles: ['876543210123456789', '765432101234567890'] + * }) + * .then(console.log) + * .catch(console.error); + */ + async remove({ guild, command, users, roles }) { + const { guildID, commandID } = this._validateOptions(guild, command); + if (!commandID) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + + if (!users && !roles) throw new TypeError('INVALID_TYPE', 'users OR roles', 'Array or Resolvable', true); + + let resolvedIDs = []; + if (Array.isArray(users)) { + users.forEach(user => { + const userID = this.client.users.resolveID(user); + if (!userID) throw new TypeError('INVALID_ELEMENT', 'Array', 'users', user); + resolvedIDs.push(userID); + }); + } else if (users) { + const userID = this.client.users.resolveID(users); + if (!userID) { + throw new TypeError('INVALID_TYPE', 'users', 'Array or UserResolvable'); + } + resolvedIDs.push(userID); + } + + if (Array.isArray(roles)) { + roles.forEach(role => { + if (typeof role === 'string') { + resolvedIDs.push(role); + return; + } + if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE'); + const roleID = this.guild.roles.resolveID(role); + if (!roleID) throw new TypeError('INVALID_ELEMENT', 'Array', 'users', role); + resolvedIDs.push(roleID); + }); + } else if (roles) { + if (typeof roles === 'string') { + resolvedIDs.push(roles); + } else { + if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE'); + const roleID = this.guild.roles.resolveID(roles); + if (!roleID) { + throw new TypeError('INVALID_TYPE', 'users', 'Array or RoleResolvable'); + } + resolvedIDs.push(roleID); + } + } + + let existing = []; + try { + existing = await this.fetch({ guild: guildID, command: commandID }); + } catch (error) { + if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; + } + + const permissions = existing.filter(perm => !resolvedIDs.includes(perm.id)); + + return this.set({ guild: guildID, command: commandID, permissions }); + } + + /** + * Options used to check existance of permissions on a command + * The `command` parameter is not optional when the managers `commandID` is `null` + * @typedef {BaseApplicationCommandPermissionsOptions} HasApplicationCommandPermissionsOptions + * @param {UserResolvable|RoleResolvable} permissionID The user or role to check if a permission exists for + * on this command. + */ + + /** + * Check whether a permission exists for a user or role + * @param {AddApplicationCommandPermissionsOptions} options Options used to check permissions + * @returns {Promise} + * @example + * // Check whether a user has permission to use a command + * guild.commands.permissions.has({ command: '123456789012345678', permissionID: '876543210123456789' }) + * .then(console.log) + * .catch(console.error); + */ + async has({ guild, command, permissionID }) { + const { guildID, commandID } = this._validateOptions(guild, command); + if (!commandID) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + + if (!permissionID) throw new TypeError('INVALID_TYPE', 'permissionsID', 'UserResolvable or RoleResolvable'); + let resolvedID = permissionID; + if (typeof permissionID !== 'string') { + resolvedID = this.client.users.resolveID(permissionID); + if (!resolvedID) { + if (!this.guild) throw new Error('GUILD_UNCACHED_ROLE_RESOLVE'); + resolvedID = this.guild.roles.resolveID(permissionID); + } + if (!resolvedID) { + throw new TypeError('INVALID_TYPE', 'permissionID', 'UserResolvable or RoleResolvable'); + } + } + + let existing = []; + try { + existing = await this.fetch({ guild: guildID, command: commandID }); + } catch (error) { + if (error.code !== APIErrors.UNKNOWN_APPLICATION_COMMAND_PERMISSIONS) throw error; + } + + return existing.some(perm => perm.id === resolvedID); + } + + _validateOptions(guild, command) { + const guildID = this.guildID ?? this.client.guilds.resolveID(guild); + if (!guildID) throw new Error('GLOBAL_COMMAND_PERMISSIONS'); + let commandID = this.commandID; + if (command && !commandID) { + commandID = this.manager.resolveID?.(command); + if (!commandID && this.guild) { + commandID = this.guild.commands.resolveID(command); + } + if (!commandID) { + commandID = this.client.application?.commands.resolveID(command); + } + if (!commandID) { + throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable', true); + } + } + return { guildID, commandID }; + } + + /** + * 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 {APIApplicationCommandPermissions} + * @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 = ApplicationCommandPermissionsManager; + +/** + * @external APIApplicationCommandPermissions + * @see {@link https://discord.com/developers/docs/interactions/slash-commands#applicationcommandpermissions} + */ diff --git a/src/managers/GuildApplicationCommandManager.js b/src/managers/GuildApplicationCommandManager.js index 3a78fbf7f864..97fea5e45bdd 100644 --- a/src/managers/GuildApplicationCommandManager.js +++ b/src/managers/GuildApplicationCommandManager.js @@ -1,6 +1,7 @@ 'use strict'; const ApplicationCommandManager = require('./ApplicationCommandManager'); +const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager'); /** * An extension for guild-specific application commands. @@ -15,6 +16,12 @@ class GuildApplicationCommandManager extends ApplicationCommandManager { * @type {Guild} */ this.guild = guild; + + /** + * The manager for permissions of arbitrary commands on this guild + * @type {ApplicationCommandPermissionsManager} + */ + this.permissions = new ApplicationCommandPermissionsManager(this); } } diff --git a/src/structures/ApplicationCommand.js b/src/structures/ApplicationCommand.js index 9b7be236a3a3..2b81b0e48355 100644 --- a/src/structures/ApplicationCommand.js +++ b/src/structures/ApplicationCommand.js @@ -1,6 +1,7 @@ 'use strict'; const Base = require('./Base'); +const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager'); const { ApplicationCommandOptionTypes } = require('../util/Constants'); const SnowflakeUtil = require('../util/SnowflakeUtil'); @@ -9,7 +10,7 @@ const SnowflakeUtil = require('../util/SnowflakeUtil'); * @extends {Base} */ class ApplicationCommand extends Base { - constructor(client, data, guild) { + constructor(client, data, guild, guildID) { super(client); /** @@ -24,6 +25,19 @@ class ApplicationCommand extends Base { */ this.guild = guild ?? null; + /** + * The guild ID this command is part of, this may be non-null when `guild` is `null` if the command + * was fetched from the `ApplicationCommandManager` + * @type {?Snowflake} + */ + this.guildID = guild?.id ?? guildID ?? null; + + /** + * The manager for permissions of this command on its guild or arbitrary gulds when the command is global + * @type {ApplicationCommandPermissionsManager} + */ + this.permissions = new ApplicationCommandPermissionsManager(this); + this._patch(data); } @@ -113,7 +127,7 @@ class ApplicationCommand extends Base { * .catch(console.error); */ edit(data) { - return this.manager.edit(this, data); + return this.manager.edit(this, data, this.guildID); } /** @@ -126,62 +140,7 @@ class ApplicationCommand extends Base { * .catch(console.error); */ delete() { - return this.manager.delete(this); - } - - /** - * Data for setting the permissions of an application command. - * @typedef {Object} ApplicationCommandPermissionData - * @property {Snowflake} id The ID of the role or user - * @property {ApplicationCommandPermissionType|number} type Whether this permission is 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 is 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. - * 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 - * command.fetchPermissions() - * .then(perms => console.log(`Fetched permissions for ${perms.length} users`)) - * .catch(console.error); - */ - fetchPermissions(guildID) { - return this.manager.fetchPermissions(this, guildID); - } - - /** - * Sets the permissions for this command. - * 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 - * command.setPermissions([ - * { - * id: '876543210987654321', - * type: 'USER', - * permission: false, - * }, - * ]) - * .then(console.log) - * .catch(console.error); - */ - setPermissions(permissions, guildID) { - return this.manager.setPermissions(this, permissions, guildID); + return this.manager.delete(this, this.guildID); } /** diff --git a/typings/index.d.ts b/typings/index.d.ts index 9b1cfd8eb736..d4e35c04345f 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -282,24 +282,26 @@ declare module 'discord.js' { public toString(): string | null; } - export class ApplicationCommand extends Base { - constructor(client: Client, data: unknown, guild?: Guild); + export class ApplicationCommand extends Base { + constructor(client: Client, data: unknown, guild?: Guild, guildID?: Snowflake); public readonly createdAt: Date; public readonly createdTimestamp: number; public defaultPermission: boolean; public description: string; public guild: Guild | null; + public guildID: Snowflake | null; public readonly manager: ApplicationCommandManager; public id: Snowflake; public name: string; public options: ApplicationCommandOption[]; - public delete(): Promise; - public edit(data: ApplicationCommandData): Promise; - public fetchPermissions(guildID?: Snowflake): Promise; - public setPermissions( - permissions: ApplicationCommandPermissionData[], - guildID?: Snowflake, - ): Promise; + public permissions: ApplicationCommandPermissionsManager< + PermissionsFetchType, + PermissionsFetchType, + Guild | null, + Snowflake + >; + public delete(): Promise>; + public edit(data: ApplicationCommandData): Promise>; private static transformOption(option: ApplicationCommandOptionData, received?: boolean): unknown; } @@ -546,7 +548,7 @@ declare module 'discord.js' { } export class CommandInteraction extends Interaction { - public readonly command: ApplicationCommand | null; + public readonly command: ApplicationCommand | ApplicationCommand<{ guild: GuildResolvable }> | null; public readonly channel: TextChannel | DMChannel | NewsChannel | PartialDMChannel | null; public channelID: Snowflake; public commandID: Snowflake; @@ -2326,47 +2328,88 @@ declare module 'discord.js' { public valueOf(): Collection; } - export class ApplicationCommandManager extends BaseManager< - Snowflake, - ApplicationCommand, - ApplicationCommandResolvable - > { + export class ApplicationCommandManager< + ApplicationCommandType = ApplicationCommand<{ guild: GuildResolvable }>, + PermissionsOptionsExtras = { guild: GuildResolvable }, + PermissionsGuildType = null, + > extends BaseManager { constructor(client: Client, iterable?: Iterable); + public permissions: ApplicationCommandPermissionsManager< + { command?: ApplicationCommandResolvable } & PermissionsOptionsExtras, + { command: ApplicationCommandResolvable } & PermissionsOptionsExtras, + PermissionsGuildType, + null + >; private commandPath({ id, guildID }: { id?: Snowflake; guildID?: Snowflake }): unknown; - public create(command: ApplicationCommandData, guildID?: Snowflake): Promise; - public delete(command: ApplicationCommandResolvable, guildID?: Snowflake): Promise; + public create(command: ApplicationCommandData, guildID: Snowflake): Promise; + public create(command: ApplicationCommandData, guildID?: Snowflake): Promise; + public delete(command: ApplicationCommandResolvable, guildID?: Snowflake): Promise; + public edit( + command: ApplicationCommandResolvable, + data: ApplicationCommandData, + guildID: Snowflake, + ): Promise; public edit( command: ApplicationCommandResolvable, data: ApplicationCommandData, guildID?: Snowflake, + ): Promise; + public fetch( + id: Snowflake, + options: FetchApplicationCommandOptions & { guild: GuildResolvable }, ): Promise; - public fetch(id: Snowflake, options?: FetchApplicationCommandOptions): 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; + ): Promise>; public set( commands: ApplicationCommandData[], guildID?: Snowflake, ): Promise>; - public setPermissions( - command: ApplicationCommandResolvable, - permissions: ApplicationCommandPermissionData[], - guildID: Snowflake, + public set( + commands: ApplicationCommandData[], + guildID?: Snowflake, + ): Promise>; + private static transformCommand(command: ApplicationCommandData): unknown; + } + + export class ApplicationCommandPermissionsManager { + constructor(manager: ApplicationCommandManager | GuildApplicationCommandManager | ApplicationCommand); + public client: Client; + public commandID: CommandIDType; + public guild: GuildType; + public guildID: Snowflake | null; + public manager: ApplicationCommandManager | GuildApplicationCommandManager | ApplicationCommand; + public add( + options: BaseOptions & { permissions: ApplicationCommandPermissionData[] }, ): Promise; - public setPermissions( - permissions: GuildApplicationCommandPermissionData[], - guildID: Snowflake, + public has(options: BaseOptions & { permissionsID: UserResolvable | RoleResolvable }): Promise; + public fetch(options: FetchSingleOptions): Promise; + public fetch(options: BaseOptions): Promise>; + public remove( + options: + | (BaseOptions & { + users: UserResolvable | UserResolvable[]; + roles?: RoleResolvable | RoleResolvable[]; + }) + | (BaseOptions & { + users?: UserResolvable | UserResolvable[]; + roles: RoleResolvable | RoleResolvable[]; + }), + ): Promise; + public set( + options: BaseOptions & { + command: ApplicationCommandResolvable; + permissions: ApplicationCommandPermissionData[]; + }, + ): Promise; + public set( + options: BaseOptions & { + fullPermissions: GuildApplicationCommandPermissionData[]; + }, ): Promise>; - private static transformCommand(command: ApplicationCommandData): unknown; + private permissionsPath(guildID: Snowflake, commandID?: Snowflake): unknown; private static transformPermissions(permissions: ApplicationCommandPermissionData, received?: boolean): unknown; } @@ -2380,7 +2423,7 @@ declare module 'discord.js' { public fetch(id: Snowflake, options?: BaseFetchOptions): Promise; } - export class GuildApplicationCommandManager extends ApplicationCommandManager { + export class GuildApplicationCommandManager extends ApplicationCommandManager { constructor(guild: Guild, iterable?: Iterable); public guild: Guild; public create(command: ApplicationCommandData): Promise;