'use strict';
const { Collection } = require('@discordjs/collection');
const { ApplicationCommandPermissionType, RESTJSONErrorCodes, Routes } = require('discord-api-types/v10');
const BaseManager = require('./BaseManager');
const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors');
/**
* Manages API methods for permissions of Application Commands.
* @extends {BaseManager}
*/
class ApplicationCommandPermissionsManager extends BaseManager {
constructor(manager) {
super(manager.client);
/**
* The manager or command that this manager belongs to
* @type {ApplicationCommandManager|ApplicationCommand}
* @private
*/
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 APIRouter path to the commands
* @param {Snowflake} guildId The guild's id to use in the path,
* @param {Snowflake} [commandId] The application command's id
* @returns {string}
* @private
*/
permissionsPath(guildId, commandId) {
if (commandId) {
return Routes.applicationCommandPermissions(this.client.application.id, guildId, commandId);
}
return Routes.guildApplicationCommandsPermissions(this.client.application.id, guildId);
}
/* eslint-disable max-len */
/**
* The object returned when fetching permissions for an application command.
* @typedef {Object} ApplicationCommandPermissions
* @property {Snowflake} id The role, user, or channel's id. Can also be a
* {@link https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-constants permission constant}.
* @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
*/
/* eslint-enable max-len */
/**
* 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
* @property {GuildResolvable} [guild] The guild to modify / check permissions for
* Ignored when the manager has a non-null `guildId` property
* @property {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. Providing the client's id as the "command id" will fetch
* *only* the guild level permissions
* @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 ${perms.length} overwrites`))
* .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);
* @example
* // Fetch guild level permissions
* guild.commands.permissions.fetch({ command: client.user.id })
* .then(perms => console.log(`Fetched ${perms.length} guild level permissions`))
* .catch(console.error);
*/
async fetch({ guild, command } = {}) {
const { guildId, commandId } = this._validateOptions(guild, command);
if (commandId) {
const data = await this.client.rest.get(this.permissionsPath(guildId, commandId));
return data.permissions;
}
const data = await this.client.rest.get(this.permissionsPath(guildId));
return data.reduce((coll, perm) => coll.set(perm.id, perm.permissions), new Collection());
}
/**
* Options used to set permissions for one or more Application Commands in a guild
* Omitting the `command` parameter edits the guild wide permissions
* when the manager's `commandId` is `null`
* @typedef {BaseApplicationCommandPermissionsOptions} ApplicationCommandPermissionsEditOptions
* @property {ApplicationCommandPermissions[]} permissions The new permissions for the guild or overwrite
* @property {string} token The bearer token to use that authorizes the permission edit
*/
/**
* Sets the permissions for the guild or a command overwrite.
* @param {ApplicationCommandPermissionsEditOptions} options Options used to set permissions
* @returns {Promise>}
* @example
* // Set a permission overwrite for a command
* client.application.commands.permissions.set({
* guild: '892455839386304532',
* command: '123456789012345678',
* token: 'TotallyRealToken',
* permissions: [
* {
* id: '876543210987654321',
* type: ApplicationCommandPermissionType.User,
* permission: false,
* },
* ]})
* .then(console.log)
* .catch(console.error);
* @example
* // Set the permissions used for the guild (commands without overwrites)
* guild.commands.permissions.set({ token: 'TotallyRealToken', permissions: [
* {
* id: '123456789012345678',
* permissions: [{
* id: '876543210987654321',
* type: ApplicationCommandPermissionType.User,
* permission: false,
* }],
* },
* ]})
* .then(console.log)
* .catch(console.error);
*/
async set({ guild, command, permissions, token } = {}) {
if (!token) {
throw new DiscordjsError(ErrorCodes.ApplicationCommandPermissionsTokenMissing);
}
let { guildId, commandId } = this._validateOptions(guild, command);
if (!Array.isArray(permissions)) {
throw new DiscordjsTypeError(
ErrorCodes.InvalidType,
'permissions',
'Array of ApplicationCommandPermissions',
true,
);
}
if (!commandId) {
commandId = this.client.user.id;
}
const data = await this.client.rest.put(this.permissionsPath(guildId, commandId), {
body: { permissions },
auth: false,
headers: { Authorization: `Bearer ${token}` },
});
return data.permissions;
}
/**
* Add permissions to a command.
* @param {ApplicationCommandPermissionsEditOptions} options Options used to add permissions
* @returns {Promise}
* @example
* // Add a rule to block a role from using a command
* guild.commands.permissions.add({ command: '123456789012345678', token: 'TotallyRealToken', permissions: [
* {
* id: '876543211234567890',
* type: ApplicationCommandPermissionType.Role,
* permission: false
* },
* ]})
* .then(console.log)
* .catch(console.error);
*/
async add({ guild, command, permissions, token } = {}) {
if (!token) {
throw new DiscordjsError(ErrorCodes.ApplicationCommandPermissionsTokenMissing);
}
let { guildId, commandId } = this._validateOptions(guild, command);
if (!commandId) {
commandId = this.client.user.id;
}
if (!Array.isArray(permissions)) {
throw new DiscordjsTypeError(
ErrorCodes.InvalidType,
'permissions',
'Array of ApplicationCommandPermissions',
true,
);
}
let existing = [];
try {
existing = await this.fetch({ guild: guildId, command: commandId });
} catch (error) {
if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) 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, token });
}
/**
* A static snowflake that identifies the everyone role for application command permissions.
* It is the same as the guild id
* @typedef {Snowflake} RolePermissionConstant
*/
/**
* A static snowflake that identifies the "all channels" entity for application command permissions.
* It will be the result of the calculation `guildId - 1`
* @typedef {Snowflake} ChannelPermissionConstant
*/
/**
* Options used to remove permissions from a command
* Omitting the `command` parameter removes from the guild wide permissions
* when the managers `commandId` is `null`
* At least one of `users`, `roles`, and `channels` is required
* @typedef {BaseApplicationCommandPermissionsOptions} RemoveApplicationCommandPermissionsOptions
* @property {string} token The bearer token to use that authorizes the permission removal
* @property {UserResolvable[]} [users] The user(s) to remove
* @property {Array} [roles] The role(s) to remove
* @property {Array} [channels] The channel(s) to remove
*/
/**
* 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', token: 'TotallyRealToken',
* })
* .then(console.log)
* .catch(console.error);
* @example
* // Remove multiple roles from this command
* guild.commands.permissions.remove({
* command: '123456789012345678', roles: ['876543210123456789', '765432101234567890'], token: 'TotallyRealToken',
* })
* .then(console.log)
* .catch(console.error);
*/
async remove({ guild, command, users, roles, channels, token } = {}) {
if (!token) {
throw new DiscordjsError(ErrorCodes.ApplicationCommandPermissionsTokenMissing);
}
let { guildId, commandId } = this._validateOptions(guild, command);
if (!commandId) {
commandId = this.client.user.id;
}
if (!users && !roles && !channels) {
throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'users OR roles OR channels', 'Array or Resolvable', true);
}
let resolvedUserIds = [];
if (Array.isArray(users)) {
for (const user of users) {
const userId = this.client.users.resolveId(user);
if (!userId) throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array', 'users', user);
resolvedUserIds.push(userId);
}
}
let resolvedRoleIds = [];
if (Array.isArray(roles)) {
for (const role of roles) {
if (typeof role === 'string') {
resolvedRoleIds.push(role);
continue;
}
if (!this.guild) throw new DiscordjsError(ErrorCodes.GuildUncachedEntityResolve, 'roles');
const roleId = this.guild.roles.resolveId(role);
if (!roleId) throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array', 'users', role);
resolvedRoleIds.push(roleId);
}
}
let resolvedChannelIds = [];
if (Array.isArray(channels)) {
for (const channel of channels) {
if (typeof channel === 'string') {
resolvedChannelIds.push(channel);
continue;
}
if (!this.guild) throw new DiscordjsError(ErrorCodes.GuildUncachedEntityResolve, 'channels');
const channelId = this.guild.channels.resolveId(channel);
if (!channelId) throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array', 'channels', channel);
resolvedChannelIds.push(channelId);
}
}
let existing = [];
try {
existing = await this.fetch({ guild: guildId, command: commandId });
} catch (error) {
if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error;
}
const permissions = existing.filter(perm => {
switch (perm.type) {
case ApplicationCommandPermissionType.Role:
return !resolvedRoleIds.includes(perm.id);
case ApplicationCommandPermissionType.User:
return !resolvedUserIds.includes(perm.id);
case ApplicationCommandPermissionType.Channel:
return !resolvedChannelIds.includes(perm.id);
}
return true;
});
return this.set({ guild: guildId, command: commandId, permissions, token });
}
/**
* Options used to check the existence of permissions on a command
* The `command` parameter is not optional when the managers `commandId` is `null`
* @typedef {BaseApplicationCommandPermissionsOptions} HasApplicationCommandPermissionsOptions
* @property {ApplicationCommandPermissionIdResolvable} permissionId The entity to check if a permission exists for
* on this command.
* @property {ApplicationCommandPermissionType} [permissionType] Check for a specific type of permission
*/
/**
* Check whether a permission exists for a user, role, or channel
* @param {HasApplicationCommandPermissionsOptions} 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, permissionType }) {
const { guildId, commandId } = this._validateOptions(guild, command);
if (!commandId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'command', 'ApplicationCommandResolvable');
if (!permissionId) {
throw new DiscordjsTypeError(
ErrorCodes.InvalidType,
'permissionId',
'UserResolvable, RoleResolvable, ChannelResolvable, or Permission Constant',
);
}
let resolvedId = permissionId;
if (typeof permissionId !== 'string') {
resolvedId = this.client.users.resolveId(permissionId);
if (!resolvedId) {
if (!this.guild) throw new DiscordjsError(ErrorCodes.GuildUncachedEntityResolve, 'roles');
resolvedId = this.guild.roles.resolveId(permissionId);
}
if (!resolvedId) {
resolvedId = this.guild.channels.resolveId(permissionId);
}
if (!resolvedId) {
throw new DiscordjsTypeError(
ErrorCodes.InvalidType,
'permissionId',
'UserResolvable, RoleResolvable, ChannelResolvable, or Permission Constant',
);
}
}
let existing = [];
try {
existing = await this.fetch({ guild: guildId, command: commandId });
} catch (error) {
if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error;
}
// Check permission type if provided for the single edge case where a channel id is the same as the everyone role id
return existing.some(perm => perm.id === resolvedId && (permissionType ?? perm.type) === perm.type);
}
_validateOptions(guild, command) {
const guildId = this.guildId ?? this.client.guilds.resolveId(guild);
if (!guildId) throw new DiscordjsError(ErrorCodes.GlobalCommandPermissions);
let commandId = this.commandId;
if (command && !commandId) {
commandId = this.manager.resolveId?.(command);
if (!commandId && this.guild) {
commandId = this.guild.commands.resolveId(command);
}
commandId ??= this.client.application?.commands.resolveId(command);
if (!commandId) {
throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'command', 'ApplicationCommandResolvable', true);
}
}
return { guildId, commandId };
}
}
module.exports = ApplicationCommandPermissionsManager;
/* eslint-disable max-len */
/**
* @external APIApplicationCommandPermissions
* @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure}
*/
/**
* Data that resolves to an id used for an application command permission
* @typedef {UserResolvable|RoleResolvable|GuildChannelResolvable|RolePermissionConstant|ChannelPermissionConstant} ApplicationCommandPermissionIdResolvable
*/