diff --git a/src/managers/ApplicationCommandManager.js b/src/managers/ApplicationCommandManager.js index 183478f..6aeeedc 100644 --- a/src/managers/ApplicationCommandManager.js +++ b/src/managers/ApplicationCommandManager.js @@ -1,223 +1,223 @@ -'use strict'; - -const { Collection } = require('@discordjs/collection'); -const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager'); -const CachedManager = require('./CachedManager'); -const { TypeError } = require('../errors'); -const ApplicationCommand = require('../structures/ApplicationCommand'); -const { ApplicationCommandTypes } = require('../util/Constants'); - -/** - * Manages API methods for application commands and stores their cache. - * @extends {CachedManager} - */ -class ApplicationCommandManager extends CachedManager { - constructor(client, iterable, user) { - super(client, ApplicationCommand, iterable); - - /** - * The manager for permissions of arbitrary commands on arbitrary guilds - * @type {ApplicationCommandPermissionsManager} - */ - this.permissions = new ApplicationCommandPermissionsManager(this, user); - this.user = user; - } - - /** - * The cache of this manager - * @type {Collection} - * @name ApplicationCommandManager#cache - */ - - _add(data, cache, guildId) { - return super._add(data, cache, { extras: [this.guild, guildId] }); - } - - /** - * The APIRouter path to the commands - * @param {Snowflake} [options.id] The application command's id - * @param {Snowflake} [options.guildId] The guild's id to use in the path, - * ignored when using a {@link GuildApplicationCommandManager} - * @returns {Object} - * @private - */ - commandPath({ id, guildId } = {}) { - let path = this.client.api.applications(this.user.id); - if (this.guild ?? guildId) path = path.guilds(this.guild?.id ?? guildId); - return id ? path.commands(id) : path.commands; - } - - /** - * Data that resolves to give an ApplicationCommand object. This can be: - * * An ApplicationCommand object - * * A Snowflake - * @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] The guild's id 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] The application command's id - * @param {FetchApplicationCommandOptions} [options] Additional options for this fetch - * @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, { guildId, cache = true, force = false } = {}) { - await this.user.createDM().catch(() => {}); - if (typeof id === 'object') { - ({ guildId, cache = true } = id); - } else if (id) { - if (!force) { - const existing = this.cache.get(id); - if (existing) return existing; - } - const command = await this.commandPath({ id, guildId }).get(); - return this._add(command, cache); - } - - const data = await this.commandPath({ guildId }).get(); - return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection()); - } - - /** - * Creates an application command. - * @param {ApplicationCommandData|APIApplicationCommand} command The command - * @param {Snowflake} [guildId] The guild's id to create this command in, - * ignored when using a {@link GuildApplicationCommandManager} - * @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, guildId) { - if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); - const data = await this.commandPath({ guildId }).post({ - data: this.constructor.transformCommand(command), - }); - return this._add(data, true, guildId); - } - - /** - * Sets all the commands for this application or guild. - * @param {ApplicationCommandData[]|APIApplicationCommand[]} commands The commands - * @param {Snowflake} [guildId] The guild's id to create the commands in, - * ignored when using a {@link GuildApplicationCommandManager} - * @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, guildId) { - if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); - 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, true, guildId)), new Collection()); - } - - /** - * Edits an application command. - * @param {ApplicationCommandResolvable} command The command to edit - * @param {ApplicationCommandData|APIApplicationCommand} data The data to update the command with - * @param {Snowflake} [guildId] The guild's id where the command registered, - * ignored when using a {@link GuildApplicationCommandManager} - * @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, guildId) { - if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); - const id = this.resolveId(command); - 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, true, guildId); - } - - /** - * Deletes an application command. - * @param {ApplicationCommandResolvable} command The command to delete - * @param {Snowflake} [guildId] The guild's id where the command is registered, - * ignored when using a {@link GuildApplicationCommandManager} - * @returns {Promise} - * @example - * // Delete a command - * guild.commands.delete('123456789012345678') - * .then(console.log) - * .catch(console.error); - */ - async delete(command, guildId) { - if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); - const id = this.resolveId(command); - if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); - - await this.commandPath({ id, guildId }).delete(); - - const cached = this.cache.get(id); - this.cache.delete(id); - return cached ?? null; - } - - /** - * Transforms an {@link ApplicationCommandData} object into something that can be used with the API. - * @param {ApplicationCommandData|APIApplicationCommand} command The command to transform - * @returns {APIApplicationCommand} - * @private - */ - static transformCommand(command) { - return { - name: command.name, - description: command.description, - type: typeof command.type === 'number' ? command.type : ApplicationCommandTypes[command.type], - options: command.options?.map(o => ApplicationCommand.transformOption(o)), - default_permission: command.defaultPermission ?? command.default_permission, - }; - } -} - -module.exports = ApplicationCommandManager; +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager'); +const CachedManager = require('./CachedManager'); +const { TypeError } = require('../errors'); +const ApplicationCommand = require('../structures/ApplicationCommand'); +const { ApplicationCommandTypes } = require('../util/Constants'); + +/** + * Manages API methods for application commands and stores their cache. + * @extends {CachedManager} + */ +class ApplicationCommandManager extends CachedManager { + constructor(client, iterable, user) { + super(client, ApplicationCommand, iterable); + + /** + * The manager for permissions of arbitrary commands on arbitrary guilds + * @type {ApplicationCommandPermissionsManager} + */ + this.permissions = new ApplicationCommandPermissionsManager(this, user); + this.user = user; + } + + /** + * The cache of this manager + * @type {Collection} + * @name ApplicationCommandManager#cache + */ + + _add(data, cache, guildId) { + return super._add(data, cache, { extras: [this.guild, guildId] }); + } + + /** + * The APIRouter path to the commands + * @param {Snowflake} [options.id] The application command's id + * @param {Snowflake} [options.guildId] The guild's id to use in the path, + * ignored when using a {@link GuildApplicationCommandManager} + * @returns {Object} + * @private + */ + commandPath({ id, guildId } = {}) { + let path = this.client.api.applications(this.user.id); + if (this.guild ?? guildId) path = path.guilds(this.guild?.id ?? guildId); + return id ? path.commands(id) : path.commands; + } + + /** + * Data that resolves to give an ApplicationCommand object. This can be: + * * An ApplicationCommand object + * * A Snowflake + * @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] The guild's id 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] The application command's id + * @param {FetchApplicationCommandOptions} [options] Additional options for this fetch + * @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, { guildId, cache = true, force = false } = {}) { + await this.user.createDM().catch(() => {}); + if (typeof id === 'object') { + ({ guildId, cache = true } = id); + } else if (id) { + if (!force) { + const existing = this.cache.get(id); + if (existing) return existing; + } + const command = await this.commandPath({ id, guildId }).get(); + return this._add(command, cache); + } + + const data = await this.commandPath({ guildId }).get(); + return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection()); + } + + /** + * Creates an application command. + * @param {ApplicationCommandData|APIApplicationCommand} command The command + * @param {Snowflake} [guildId] The guild's id to create this command in, + * ignored when using a {@link GuildApplicationCommandManager} + * @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, guildId) { + if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); + const data = await this.commandPath({ guildId }).post({ + data: this.constructor.transformCommand(command), + }); + return this._add(data, true, guildId); + } + + /** + * Sets all the commands for this application or guild. + * @param {ApplicationCommandData[]|APIApplicationCommand[]} commands The commands + * @param {Snowflake} [guildId] The guild's id to create the commands in, + * ignored when using a {@link GuildApplicationCommandManager} + * @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, guildId) { + if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); + 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, true, guildId)), new Collection()); + } + + /** + * Edits an application command. + * @param {ApplicationCommandResolvable} command The command to edit + * @param {ApplicationCommandData|APIApplicationCommand} data The data to update the command with + * @param {Snowflake} [guildId] The guild's id where the command registered, + * ignored when using a {@link GuildApplicationCommandManager} + * @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, guildId) { + if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); + const id = this.resolveId(command); + 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, true, guildId); + } + + /** + * Deletes an application command. + * @param {ApplicationCommandResolvable} command The command to delete + * @param {Snowflake} [guildId] The guild's id where the command is registered, + * ignored when using a {@link GuildApplicationCommandManager} + * @returns {Promise} + * @example + * // Delete a command + * guild.commands.delete('123456789012345678') + * .then(console.log) + * .catch(console.error); + */ + async delete(command, guildId) { + if(!this.client.user.bot) throw new Error("INVALID_USER_METHOD"); + const id = this.resolveId(command); + if (!id) throw new TypeError('INVALID_TYPE', 'command', 'ApplicationCommandResolvable'); + + await this.commandPath({ id, guildId }).delete(); + + const cached = this.cache.get(id); + this.cache.delete(id); + return cached ?? null; + } + + /** + * Transforms an {@link ApplicationCommandData} object into something that can be used with the API. + * @param {ApplicationCommandData|APIApplicationCommand} command The command to transform + * @returns {APIApplicationCommand} + * @private + */ + static transformCommand(command) { + return { + name: command.name, + description: command.description, + type: typeof command.type === 'number' ? command.type : ApplicationCommandTypes[command.type], + options: command.options?.map(o => ApplicationCommand.transformOption(o)), + default_permission: command.defaultPermission ?? command.default_permission, + }; + } +} + +module.exports = ApplicationCommandManager; diff --git a/src/managers/ApplicationCommandPermissionsManager.js b/src/managers/ApplicationCommandPermissionsManager.js index b9cbb20..cf2048a 100644 --- a/src/managers/ApplicationCommandPermissionsManager.js +++ b/src/managers/ApplicationCommandPermissionsManager.js @@ -1,422 +1,422 @@ -'use strict'; - -const { Collection } = require('@discordjs/collection'); -const BaseManager = require('./BaseManager'); -const { Error, TypeError } = require('../errors'); -const { ApplicationCommandPermissionTypes, APIErrors } = require('../util/Constants'); - -/** - * Manages API methods for permissions of Application Commands. - * @extends {BaseManager} - */ -class ApplicationCommandPermissionsManager extends BaseManager { - constructor(manager, user) { - 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; - - this.user = user; - } - - /** - * 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 {Object} - * @private - */ - permissionsPath(guildId, commandId) { - return this.client.api.applications(typeof this.user == 'string' ? this.user : this.user.id).guilds(guildId).commands(commandId).permissions; - } - - /** - * Data for setting the permissions of an application command. - * @typedef {Object} ApplicationCommandPermissionData - * @property {Snowflake} id The role or user's id - * @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 role or user's id - * @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 - * @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. - * @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 command's id - * @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 - * @property {ApplicationCommandPermissionData[]} [permissions] The new permissions for the command - * @property {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({ guild: '892455839386304532', 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 } = {}) { - if(!this.manager.client.user.bot) throw new Error("INVALID_USER_METHOD"); - 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 - * @property {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 }) { - if(!this.manager.client.user.bot) throw new Error("INVALID_USER_METHOD"); - 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 - * @property {UserResolvable|UserResolvable[]} [users] The user(s) to remove from the command permissions - * One of `users` or `roles` is required - * @property {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 }) { - if(!this.manager.client.user.bot) throw new Error("INVALID_USER_METHOD"); - 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 the existence of permissions on a command - * The `command` parameter is not optional when the managers `commandId` is `null` - * @typedef {BaseApplicationCommandPermissionsOptions} HasApplicationCommandPermissionsOptions - * @property {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', 'permissionId', '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); - } - 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; - -/* eslint-disable max-len */ -/** - * @external APIApplicationCommandPermissions - * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure} - */ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const BaseManager = require('./BaseManager'); +const { Error, TypeError } = require('../errors'); +const { ApplicationCommandPermissionTypes, APIErrors } = require('../util/Constants'); + +/** + * Manages API methods for permissions of Application Commands. + * @extends {BaseManager} + */ +class ApplicationCommandPermissionsManager extends BaseManager { + constructor(manager, user) { + 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; + + this.user = user; + } + + /** + * 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 {Object} + * @private + */ + permissionsPath(guildId, commandId) { + return this.client.api.applications(typeof this.user == 'string' ? this.user : this.user.id).guilds(guildId).commands(commandId).permissions; + } + + /** + * Data for setting the permissions of an application command. + * @typedef {Object} ApplicationCommandPermissionData + * @property {Snowflake} id The role or user's id + * @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 role or user's id + * @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 + * @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. + * @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 command's id + * @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 + * @property {ApplicationCommandPermissionData[]} [permissions] The new permissions for the command + * @property {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({ guild: '892455839386304532', 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 } = {}) { + if(!this.manager.client.user.bot) throw new Error("INVALID_USER_METHOD"); + 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 + * @property {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 }) { + if(!this.manager.client.user.bot) throw new Error("INVALID_USER_METHOD"); + 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 + * @property {UserResolvable|UserResolvable[]} [users] The user(s) to remove from the command permissions + * One of `users` or `roles` is required + * @property {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 }) { + if(!this.manager.client.user.bot) throw new Error("INVALID_USER_METHOD"); + 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 the existence of permissions on a command + * The `command` parameter is not optional when the managers `commandId` is `null` + * @typedef {BaseApplicationCommandPermissionsOptions} HasApplicationCommandPermissionsOptions + * @property {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', 'permissionId', '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); + } + 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; + +/* eslint-disable max-len */ +/** + * @external APIApplicationCommandPermissions + * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure} + */ diff --git a/src/managers/BlockedManager.js b/src/managers/BlockedManager.js index fac1b7a..32cadcf 100644 --- a/src/managers/BlockedManager.js +++ b/src/managers/BlockedManager.js @@ -1,75 +1,75 @@ -'use strict'; - -const CachedManager = require('./CachedManager'); -const GuildMember = require('../structures/GuildMember'); -const Message = require('../structures/Message'); -const ThreadMember = require('../structures/ThreadMember'); -const User = require('../structures/User'); - -/** - * Manages API methods for users and stores their cache. - * @extends {CachedManager} - */ -class BlockedManager extends CachedManager { - constructor(client, iterable) { - super(client, User, iterable); - } - - /** - * The cache of this manager - * @type {Collection} - * @name BlockedManager#cache - */ - - /** - * Data that resolves to give a User object. This can be: - * * A User object - * * A Snowflake - * * A Message object (resolves to the message author) - * * A GuildMember object - * * A ThreadMember object - * @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable - */ - - /** - * Resolves a {@link UserResolvable} to a {@link User} object. - * @param {UserResolvable} user The UserResolvable to identify - * @returns {?User} - */ - resolve(user) { - if (user instanceof GuildMember || user instanceof ThreadMember) return user.user; - if (user instanceof Message) return user.author; - return super.resolve(user); - } - - /** - * Resolves a {@link UserResolvable} to a {@link User} id. - * @param {UserResolvable} user The UserResolvable to identify - * @returns {?Snowflake} - */ - resolveId(user) { - if (user instanceof ThreadMember) return user.id; - if (user instanceof GuildMember) return user.user.id; - if (user instanceof Message) return user.author.id; - return super.resolveId(user); - } - - /** - * Obtains a user from Discord, or the user cache if it's already available. - * @param {UserResolvable} user The user to fetch - * @param {BaseFetchOptions} [options] Additional options for this fetch - * @returns {Promise} - */ - async fetch(user, { cache = true, force = false } = {}) { - const id = this.resolveId(user); - if (!force) { - const existing = this.cache.get(id); - if (existing && !existing.partial) return existing; - } - - const data = await this.client.api.users(id).get(); - return this._add(data, cache); - } -} - -module.exports = BlockedManager; +'use strict'; + +const CachedManager = require('./CachedManager'); +const GuildMember = require('../structures/GuildMember'); +const Message = require('../structures/Message'); +const ThreadMember = require('../structures/ThreadMember'); +const User = require('../structures/User'); + +/** + * Manages API methods for users and stores their cache. + * @extends {CachedManager} + */ +class BlockedManager extends CachedManager { + constructor(client, iterable) { + super(client, User, iterable); + } + + /** + * The cache of this manager + * @type {Collection} + * @name BlockedManager#cache + */ + + /** + * Data that resolves to give a User object. This can be: + * * A User object + * * A Snowflake + * * A Message object (resolves to the message author) + * * A GuildMember object + * * A ThreadMember object + * @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable + */ + + /** + * Resolves a {@link UserResolvable} to a {@link User} object. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?User} + */ + resolve(user) { + if (user instanceof GuildMember || user instanceof ThreadMember) return user.user; + if (user instanceof Message) return user.author; + return super.resolve(user); + } + + /** + * Resolves a {@link UserResolvable} to a {@link User} id. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?Snowflake} + */ + resolveId(user) { + if (user instanceof ThreadMember) return user.id; + if (user instanceof GuildMember) return user.user.id; + if (user instanceof Message) return user.author.id; + return super.resolveId(user); + } + + /** + * Obtains a user from Discord, or the user cache if it's already available. + * @param {UserResolvable} user The user to fetch + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise} + */ + async fetch(user, { cache = true, force = false } = {}) { + const id = this.resolveId(user); + if (!force) { + const existing = this.cache.get(id); + if (existing && !existing.partial) return existing; + } + + const data = await this.client.api.users(id).get(); + return this._add(data, cache); + } +} + +module.exports = BlockedManager; diff --git a/src/managers/ClientUserSettingManager.js b/src/managers/ClientUserSettingManager.js index ebd3df8..ca42cd1 100644 --- a/src/managers/ClientUserSettingManager.js +++ b/src/managers/ClientUserSettingManager.js @@ -1,364 +1,367 @@ -'use strict'; -const { default: Collection } = require('@discordjs/collection'); -const { Error, TypeError } = require('../errors/DJSError'); -const { remove } = require('lodash'); -const { localeObject, DMScanLevel, stickerAnimationMode } = require('../util/Constants') -/** - * Manages API methods for users. - */ -class ClientUserSettingManager { - constructor(client, iterable) { - this.client = client; - // Raw data - this.rawSetting = {}; - // Language - this.locale = null; - // Setting => ACTIVITY SETTINGS => Activity Status => Display current activity as a status message - this.activityDisplay = null; - // - this.disableDMfromServer = new Collection(); - // Allow direct messages from server members - this.DMfromServerMode = null; - // - this.displayImage = null; - // - this.linkedImageDisplay = null; - // Setting => APP SETTINGS => Accessibility => Automatically play GIFs when Discord is focused. - this.autoplayGIF = null; - // Show embeds and preview website links pasted into chat - this.previewLink = null; - // Setting => APP SETTINGS => Accessibility => Play Animated Emojis - this.animatedEmojis = null; - // Setting => APP SETTINGS => Accessibility => Text-to-speech => Allow playback - this.allowTTS = null; - // Setting => APP SETTINGS => Appearance => Message Display => Compact Mode [OK] - this.compactMode = null; - // Setting => APP SETTINGS => Text & Images => Emoji => Convert Emoticons - this.convertEmoticons = null; - // SAFE DIRECT MESSAGING - this.DMScanLevel = null; - // Setting => APP SETTINGS => Appearance => Theme [OK] - this.theme = ''; - // - this.developerMode = null; - // - this.afkTimeout = null; - // - this.stickerAnimationMode = null; - // WHO CAN ADD YOU AS A FRIEND ? - this.addFriendFrom = { - all: null, - mutual_friends: null, - mutual_guilds: null, - }; - // Setting => APP SETTINGS => Text & Images => Emoji => Show emoji reactions - this.showEmojiReactions = null; - // Custom Stauts [It's not working now] - this.customStatus = null; - // Guild folder and position - this.guildMetadata = new Collection(); - // Todo: add new method from Discum - } - /** - * - * @param {Object} data Raw Data to patch - * @extends https://github.com/Merubokkusu/Discord-S.C.U.M/blob/master/discum/user/user.py - * @private - */ - _patch(data) { - this.rawSetting = data; - if ('locale' in data) { - this.locale = localeObject[data.locale]; - } - if ('show_current_game' in data) { - this.activityDisplay = data.show_current_game; - } - if ('default_guilds_restricted' in data) { - this.DMfromServerMode = data.default_guilds_restricted; - } - if ('inline_attachment_media' in data) { - this.displayImage = data.inline_attachment_media; - } - if ('inline_embed_media' in data) { - this.linkedImageDisplay = data.inline_embed_media; - } - if ('gif_auto_play' in data) { - this.autoplayGIF = data.gif_auto_play; - } - if ('render_embeds' in data) { - this.previewLink = data.render_embeds; - } - if ('animate_emoji' in data) { - this.animatedEmojis = data.animate_emoji; - } - if ('enable_tts_command' in data) { - this.allowTTS = data.enable_tts_command; - } - if ('message_display_compact' in data) { - this.compactMode = data.message_display_compact; - } - if ('convert_emoticons' in data) { - this.convertEmoticons = data.convert_emoticons; - } - if ('explicit_content_filter' in data) { - this.DMScanLevel = DMScanLevel[data.explicit_content_filter]; - } - if ('theme' in data) { - this.theme = data.theme; - } - if ('developer_mode' in data) { - this.developerMode = data.developer_mode; - } - if ('afk_timeout' in data) { - this.afkTimeout = data.afk_timeout * 1000; // second => milisecond - } - if ('animate_stickers' in data) { - this.stickerAnimationMode = stickerAnimationMode[data.animate_stickers]; - } - if ('render_reactions' in data) { - this.showEmojiReactions = data.render_reactions; - } - if ('custom_status' in data) { - this.customStatus = data.custom_status || {}; // Thanks PinkDuwc._#3443 reported this issue - this.customStatus.status = data.status; - } - if ('friend_source_flags' in data) { - this.addFriendFrom = { - all: data.friend_source_flags.all || false, - mutual_friends: - data.friend_source_flags.all ? true : data.friend_source_flags.mutual_friends, - mutual_guilds: - data.friend_source_flags.all ? true : data.friend_source_flags.mutual_guilds, - }; - } - if ('guild_folders' in data) { - const data_ = data.guild_positions.map((guildId, i) => { - // Find folder - const folderIndex = data.guild_folders.findIndex((obj) => - obj.guild_ids.includes(guildId), - ); - const metadata = { - guildId: guildId, - guildIndex: i, - folderId: data.guild_folders[folderIndex]?.id, - folderIndex, - folderName: data.guild_folders[folderIndex]?.name, - folderColor: data.guild_folders[folderIndex]?.color, - folderGuilds: data.guild_folders[folderIndex]?.guild_ids, - }; - return [guildId, metadata]; - }); - this.guildMetadata = new Collection(data_); - } - if ('restricted_guilds' in data) { - data.restricted_guilds.map((guildId) => { - const guild = this.client.guilds.cache.get(guildId); - if (!guild) return; - guild.disableDM = true; - this.disableDMfromServer.set(guildId, true); - }); - } - } - async fetch() { - if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); - try { - const data = await this.client.api.users('@me').settings.get(); - this._patch(data); - return this; - } catch (e) { - throw e; - } - } - /** - * Edit data - * @param {Object} data Data to edit - * @private - */ - async edit(data) { - if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); - try { - const res = await this.client.api.users('@me').settings.patch({ data }); - this._patch(res); - return this; - } catch (e) { - throw e; - } - } - /** - * Set compact mode - * @param {Boolean | null} value Compact mode enable or disable - * @returns {Boolean} - */ - async setDisplayCompactMode(value) { - if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); - if ( - typeof value !== 'boolean' && - typeof value !== 'null' && - typeof value !== 'undefined' - ) - throw new TypeError( - 'INVALID_TYPE', - 'value', - 'boolean | null | undefined', - true, - ); - if (!value) value = !this.compactMode; - if (value !== this.compactMode) { - await this.edit({ message_display_compact: value }); - } - return this.compactMode; - } - /** - * Discord Theme - * @param {null |dark |light} value Theme to set - * @returns {theme} - */ - async setTheme(value) { - if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); - const validValues = ['dark', 'light']; - if ( - typeof value !== 'string' && - typeof value !== 'null' && - typeof value !== 'undefined' - ) - throw new TypeError( - 'INVALID_TYPE', - 'value', - 'string | null | undefined', - true, - ); - if (!validValues.includes(value)) { - value == validValues[0] - ? (value = validValues[1]) - : (value = validValues[0]); - } - if (value !== this.theme) { - await this.edit({ theme: value }); - } - return this.theme; - } - /** - * * Locale Setting, must be one of: - * * `DANISH` - * * `GERMAN` - * * `ENGLISH_UK` - * * `ENGLISH_US` - * * `SPANISH` - * * `FRENCH` - * * `CROATIAN` - * * `ITALIAN` - * * `LITHUANIAN` - * * `HUNGARIAN` - * * `DUTCH` - * * `NORWEGIAN` - * * `POLISH` - * * `BRAZILIAN_PORTUGUESE` - * * `ROMANIA_ROMANIAN` - * * `FINNISH` - * * `SWEDISH` - * * `VIETNAMESE` - * * `TURKISH` - * * `CZECH` - * * `GREEK` - * * `BULGARIAN` - * * `RUSSIAN` - * * `UKRAINIAN` - * * `HINDI` - * * `THAI` - * * `CHINA_CHINESE` - * * `JAPANESE` - * * `TAIWAN_CHINESE` - * * `KOREAN` - * @param {string} value - * @returns {locale} - */ - async setLocale(value) { - if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); - if (typeof value !== 'string') - throw new TypeError('INVALID_TYPE', 'value', 'string', true); - if (!localeObject[value]) throw new Error('INVALID_LOCALE'); - if (localeObject[value] !== this.locale) { - await this.edit({ locale: localeObject[value] }); - } - return this.locale; - } - // TODO: Guild positions & folders - // Change Index in Array [Hidden] - /** - * - * @param {Array} array Array - * @param {Number} from Index1 - * @param {Number} to Index2 - * @returns {Array} - * @private - */ - _move(array, from, to) { - array.splice(to, 0, array.splice(from, 1)[0]); - return array; - } - // TODO: Move Guild - // folder to folder - // folder to home - // home to home - // home to folder - /** - * Change Guild Position (from * to Folder or Home) - * @param {GuildIDResolve} guildId guild.id - * @param {Number} newPosition Guild Position - * * **WARNING**: Type = `FOLDER`, newPosition is the guild's index in the Folder. - * @param {number} type Move to folder or home - * * `FOLDER`: 1 - * * `HOME`: 2 - * @param {FolderID} folderId If you want to move to folder - * @private - */ - async guildChangePosition(guildId, newPosition, type, folderId) { - // get Guild default position - // Escape - const oldGuildFolderPosition = this.rawSetting.guild_folders.findIndex( - (value) => value.guild_ids.includes(guildId), - ); - const newGuildFolderPosition = this.rawSetting.guild_folders.findIndex( - (value) => - value.guild_ids.includes(this.rawSetting.guild_positions[newPosition]), - ); - if (type == 2 || `${type}`.toUpperCase() == 'HOME') { - // Delete GuildID from Folder and create new Folder - // Check it is folder - const folder = this.rawSetting.guild_folders[oldGuildFolderPosition]; - if (folder.id) { - this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids = - this.rawSetting.guild_folders[ - oldGuildFolderPosition - ].guild_ids.filter((v) => v !== guildId); - } - this.rawSetting.guild_folders = this._move( - this.rawSetting.guild_folders, - oldGuildFolderPosition, - newGuildFolderPosition, - ); - this.rawSetting.guild_folders[newGuildFolderPosition].id = null; - } else if (type == 1 || `${type}`.toUpperCase() == 'FOLDER') { - // Delete GuildID from oldFolder - this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids = - this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids.filter( - (v) => v !== guildId, - ); - // Index new Folder - const folderIndex = this.rawSetting.guild_folders.findIndex( - (value) => value.id == folderId, - ); - const folder = this.rawSetting.guild_folders[folderIndex]; - folder.guild_ids.push(guildId); - folder.guild_ids = [...new Set(folder.guild_ids)]; - folder.guild_ids = this._move( - folder.guild_ids, - folder.guild_ids.findIndex((v) => v == guildId), - newPosition, - ); - } - this.edit({ guild_folders: this.rawSetting.guild_folders }); - } -} - -module.exports = ClientUserSettingManager; +'use strict'; + +const CachedManager = require('./CachedManager'); +const { default: Collection } = require('@discordjs/collection'); +const { Error, TypeError } = require('../errors/DJSError'); +const { remove } = require('lodash'); +const { localeObject, DMScanLevel, stickerAnimationMode } = require('../util/Constants') +/** + * Manages API methods for users and stores their cache. + * @extends {CachedManager} + */ +class ClientUserSettingManager extends CachedManager { + constructor(client, iterable) { + super(client); + // Raw data + this.rawSetting = {}; + // Language + this.locale = null; + // Setting => ACTIVITY SETTINGS => Activity Status => Display current activity as a status message + this.activityDisplay = null; + // + this.disableDMfromServer = new Collection(); + // Allow direct messages from server members + this.DMfromServerMode = null; + // + this.displayImage = null; + // + this.linkedImageDisplay = null; + // Setting => APP SETTINGS => Accessibility => Automatically play GIFs when Discord is focused. + this.autoplayGIF = null; + // Show embeds and preview website links pasted into chat + this.previewLink = null; + // Setting => APP SETTINGS => Accessibility => Play Animated Emojis + this.animatedEmojis = null; + // Setting => APP SETTINGS => Accessibility => Text-to-speech => Allow playback + this.allowTTS = null; + // Setting => APP SETTINGS => Appearance => Message Display => Compact Mode [OK] + this.compactMode = null; + // Setting => APP SETTINGS => Text & Images => Emoji => Convert Emoticons + this.convertEmoticons = null; + // SAFE DIRECT MESSAGING + this.DMScanLevel = null; + // Setting => APP SETTINGS => Appearance => Theme [OK] + this.theme = ''; + // + this.developerMode = null; + // + this.afkTimeout = null; + // + this.stickerAnimationMode = null; + // WHO CAN ADD YOU AS A FRIEND ? + this.addFriendFrom = { + all: null, + mutual_friends: null, + mutual_guilds: null, + }; + // Setting => APP SETTINGS => Text & Images => Emoji => Show emoji reactions + this.showEmojiReactions = null; + // Custom Stauts [It's not working now] + this.customStatus = null; + // Guild folder and position + this.guildMetadata = new Collection(); + // Todo: add new method from Discum + } + /** + * + * @param {Object} data Raw Data to patch + * @extends https://github.com/Merubokkusu/Discord-S.C.U.M/blob/master/discum/user/user.py + * @private + */ + _patch(data) { + this.rawSetting = data; + if ('locale' in data) { + this.locale = localeObject[data.locale]; + } + if ('show_current_game' in data) { + this.activityDisplay = data.show_current_game; + } + if ('default_guilds_restricted' in data) { + this.DMfromServerMode = data.default_guilds_restricted; + } + if ('inline_attachment_media' in data) { + this.displayImage = data.inline_attachment_media; + } + if ('inline_embed_media' in data) { + this.linkedImageDisplay = data.inline_embed_media; + } + if ('gif_auto_play' in data) { + this.autoplayGIF = data.gif_auto_play; + } + if ('render_embeds' in data) { + this.previewLink = data.render_embeds; + } + if ('animate_emoji' in data) { + this.animatedEmojis = data.animate_emoji; + } + if ('enable_tts_command' in data) { + this.allowTTS = data.enable_tts_command; + } + if ('message_display_compact' in data) { + this.compactMode = data.message_display_compact; + } + if ('convert_emoticons' in data) { + this.convertEmoticons = data.convert_emoticons; + } + if ('explicit_content_filter' in data) { + this.DMScanLevel = DMScanLevel[data.explicit_content_filter]; + } + if ('theme' in data) { + this.theme = data.theme; + } + if ('developer_mode' in data) { + this.developerMode = data.developer_mode; + } + if ('afk_timeout' in data) { + this.afkTimeout = data.afk_timeout * 1000; // second => milisecond + } + if ('animate_stickers' in data) { + this.stickerAnimationMode = stickerAnimationMode[data.animate_stickers]; + } + if ('render_reactions' in data) { + this.showEmojiReactions = data.render_reactions; + } + if ('custom_status' in data) { + this.customStatus = data.custom_status || {}; // Thanks PinkDuwc._#3443 reported this issue + this.customStatus.status = data.status; + } + if ('friend_source_flags' in data) { + this.addFriendFrom = { + all: data.friend_source_flags.all || false, + mutual_friends: + data.friend_source_flags.all ? true : data.friend_source_flags.mutual_friends, + mutual_guilds: + data.friend_source_flags.all ? true : data.friend_source_flags.mutual_guilds, + }; + } + if ('guild_folders' in data) { + const data_ = data.guild_positions.map((guildId, i) => { + // Find folder + const folderIndex = data.guild_folders.findIndex((obj) => + obj.guild_ids.includes(guildId), + ); + const metadata = { + guildId: guildId, + guildIndex: i, + folderId: data.guild_folders[folderIndex]?.id, + folderIndex, + folderName: data.guild_folders[folderIndex]?.name, + folderColor: data.guild_folders[folderIndex]?.color, + folderGuilds: data.guild_folders[folderIndex]?.guild_ids, + }; + return [guildId, metadata]; + }); + this.guildMetadata = new Collection(data_); + } + if ('restricted_guilds' in data) { + data.restricted_guilds.map((guildId) => { + const guild = this.client.guilds.cache.get(guildId); + if (!guild) return; + guild.disableDM = true; + this.disableDMfromServer.set(guildId, true); + }); + } + } + async fetch() { + if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); + try { + const data = await this.client.api.users('@me').settings.get(); + this._patch(data); + return this; + } catch (e) { + throw e; + } + } + /** + * Edit data + * @param {Object} data Data to edit + * @private + */ + async edit(data) { + if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); + try { + const res = await this.client.api.users('@me').settings.patch({ data }); + this._patch(res); + return this; + } catch (e) { + throw e; + } + } + /** + * Set compact mode + * @param {Boolean | null} value Compact mode enable or disable + * @returns {Boolean} + */ + async setDisplayCompactMode(value) { + if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); + if ( + typeof value !== 'boolean' && + typeof value !== 'null' && + typeof value !== 'undefined' + ) + throw new TypeError( + 'INVALID_TYPE', + 'value', + 'boolean | null | undefined', + true, + ); + if (!value) value = !this.compactMode; + if (value !== this.compactMode) { + await this.edit({ message_display_compact: value }); + } + return this.compactMode; + } + /** + * Discord Theme + * @param {null |dark |light} value Theme to set + * @returns {theme} + */ + async setTheme(value) { + if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); + const validValues = ['dark', 'light']; + if ( + typeof value !== 'string' && + typeof value !== 'null' && + typeof value !== 'undefined' + ) + throw new TypeError( + 'INVALID_TYPE', + 'value', + 'string | null | undefined', + true, + ); + if (!validValues.includes(value)) { + value == validValues[0] + ? (value = validValues[1]) + : (value = validValues[0]); + } + if (value !== this.theme) { + await this.edit({ theme: value }); + } + return this.theme; + } + /** + * * Locale Setting, must be one of: + * * `DANISH` + * * `GERMAN` + * * `ENGLISH_UK` + * * `ENGLISH_US` + * * `SPANISH` + * * `FRENCH` + * * `CROATIAN` + * * `ITALIAN` + * * `LITHUANIAN` + * * `HUNGARIAN` + * * `DUTCH` + * * `NORWEGIAN` + * * `POLISH` + * * `BRAZILIAN_PORTUGUESE` + * * `ROMANIA_ROMANIAN` + * * `FINNISH` + * * `SWEDISH` + * * `VIETNAMESE` + * * `TURKISH` + * * `CZECH` + * * `GREEK` + * * `BULGARIAN` + * * `RUSSIAN` + * * `UKRAINIAN` + * * `HINDI` + * * `THAI` + * * `CHINA_CHINESE` + * * `JAPANESE` + * * `TAIWAN_CHINESE` + * * `KOREAN` + * @param {string} value + * @returns {locale} + */ + async setLocale(value) { + if (this.client.bot) throw new Error('INVALID_BOT_METHOD'); + if (typeof value !== 'string') + throw new TypeError('INVALID_TYPE', 'value', 'string', true); + if (!localeObject[value]) throw new Error('INVALID_LOCALE'); + if (localeObject[value] !== this.locale) { + await this.edit({ locale: localeObject[value] }); + } + return this.locale; + } + // TODO: Guild positions & folders + // Change Index in Array [Hidden] + /** + * + * @param {Array} array Array + * @param {Number} from Index1 + * @param {Number} to Index2 + * @returns {Array} + * @private + */ + _move(array, from, to) { + array.splice(to, 0, array.splice(from, 1)[0]); + return array; + } + // TODO: Move Guild + // folder to folder + // folder to home + // home to home + // home to folder + /** + * Change Guild Position (from * to Folder or Home) + * @param {GuildIDResolve} guildId guild.id + * @param {Number} newPosition Guild Position + * * **WARNING**: Type = `FOLDER`, newPosition is the guild's index in the Folder. + * @param {number} type Move to folder or home + * * `FOLDER`: 1 + * * `HOME`: 2 + * @param {FolderID} folderId If you want to move to folder + * @private + */ + async guildChangePosition(guildId, newPosition, type, folderId) { + // get Guild default position + // Escape + const oldGuildFolderPosition = this.rawSetting.guild_folders.findIndex( + (value) => value.guild_ids.includes(guildId), + ); + const newGuildFolderPosition = this.rawSetting.guild_folders.findIndex( + (value) => + value.guild_ids.includes(this.rawSetting.guild_positions[newPosition]), + ); + if (type == 2 || `${type}`.toUpperCase() == 'HOME') { + // Delete GuildID from Folder and create new Folder + // Check it is folder + const folder = this.rawSetting.guild_folders[oldGuildFolderPosition]; + if (folder.id) { + this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids = + this.rawSetting.guild_folders[ + oldGuildFolderPosition + ].guild_ids.filter((v) => v !== guildId); + } + this.rawSetting.guild_folders = this._move( + this.rawSetting.guild_folders, + oldGuildFolderPosition, + newGuildFolderPosition, + ); + this.rawSetting.guild_folders[newGuildFolderPosition].id = null; + } else if (type == 1 || `${type}`.toUpperCase() == 'FOLDER') { + // Delete GuildID from oldFolder + this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids = + this.rawSetting.guild_folders[oldGuildFolderPosition].guild_ids.filter( + (v) => v !== guildId, + ); + // Index new Folder + const folderIndex = this.rawSetting.guild_folders.findIndex( + (value) => value.id == folderId, + ); + const folder = this.rawSetting.guild_folders[folderIndex]; + folder.guild_ids.push(guildId); + folder.guild_ids = [...new Set(folder.guild_ids)]; + folder.guild_ids = this._move( + folder.guild_ids, + folder.guild_ids.findIndex((v) => v == guildId), + newPosition, + ); + } + this.edit({ guild_folders: this.rawSetting.guild_folders }); + } +} + +module.exports = ClientUserSettingManager; diff --git a/src/managers/FriendsManager.js b/src/managers/FriendsManager.js index 1d45dcb..612af61 100644 --- a/src/managers/FriendsManager.js +++ b/src/managers/FriendsManager.js @@ -1,75 +1,75 @@ -'use strict'; - -const CachedManager = require('./CachedManager'); -const GuildMember = require('../structures/GuildMember'); -const Message = require('../structures/Message'); -const ThreadMember = require('../structures/ThreadMember'); -const User = require('../structures/User'); - -/** - * Manages API methods for users and stores their cache. - * @extends {CachedManager} - */ -class FriendsManager extends CachedManager { - constructor(client, iterable) { - super(client, User, iterable); - } - - /** - * The cache of this manager - * @type {Collection} - * @name FriendsManager#cache - */ - - /** - * Data that resolves to give a User object. This can be: - * * A User object - * * A Snowflake - * * A Message object (resolves to the message author) - * * A GuildMember object - * * A ThreadMember object - * @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable - */ - - /** - * Resolves a {@link UserResolvable} to a {@link User} object. - * @param {UserResolvable} user The UserResolvable to identify - * @returns {?User} - */ - resolve(user) { - if (user instanceof GuildMember || user instanceof ThreadMember) return user.user; - if (user instanceof Message) return user.author; - return super.resolve(user); - } - - /** - * Resolves a {@link UserResolvable} to a {@link User} id. - * @param {UserResolvable} user The UserResolvable to identify - * @returns {?Snowflake} - */ - resolveId(user) { - if (user instanceof ThreadMember) return user.id; - if (user instanceof GuildMember) return user.user.id; - if (user instanceof Message) return user.author.id; - return super.resolveId(user); - } - - /** - * Obtains a user from Discord, or the user cache if it's already available. - * @param {UserResolvable} user The user to fetch - * @param {BaseFetchOptions} [options] Additional options for this fetch - * @returns {Promise} - */ - async fetch(user, { cache = true, force = false } = {}) { - const id = this.resolveId(user); - if (!force) { - const existing = this.cache.get(id); - if (existing && !existing.partial) return existing; - } - - const data = await this.client.api.users(id).get(); - return this._add(data, cache); - } -} - -module.exports = FriendsManager; +'use strict'; + +const CachedManager = require('./CachedManager'); +const GuildMember = require('../structures/GuildMember'); +const Message = require('../structures/Message'); +const ThreadMember = require('../structures/ThreadMember'); +const User = require('../structures/User'); + +/** + * Manages API methods for users and stores their cache. + * @extends {CachedManager} + */ +class FriendsManager extends CachedManager { + constructor(client, iterable) { + super(client, User, iterable); + } + + /** + * The cache of this manager + * @type {Collection} + * @name FriendsManager#cache + */ + + /** + * Data that resolves to give a User object. This can be: + * * A User object + * * A Snowflake + * * A Message object (resolves to the message author) + * * A GuildMember object + * * A ThreadMember object + * @typedef {User|Snowflake|Message|GuildMember|ThreadMember} UserResolvable + */ + + /** + * Resolves a {@link UserResolvable} to a {@link User} object. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?User} + */ + resolve(user) { + if (user instanceof GuildMember || user instanceof ThreadMember) return user.user; + if (user instanceof Message) return user.author; + return super.resolve(user); + } + + /** + * Resolves a {@link UserResolvable} to a {@link User} id. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {?Snowflake} + */ + resolveId(user) { + if (user instanceof ThreadMember) return user.id; + if (user instanceof GuildMember) return user.user.id; + if (user instanceof Message) return user.author.id; + return super.resolveId(user); + } + + /** + * Obtains a user from Discord, or the user cache if it's already available. + * @param {UserResolvable} user The user to fetch + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise} + */ + async fetch(user, { cache = true, force = false } = {}) { + const id = this.resolveId(user); + if (!force) { + const existing = this.cache.get(id); + if (existing && !existing.partial) return existing; + } + + const data = await this.client.api.users(id).get(); + return this._add(data, cache); + } +} + +module.exports = FriendsManager; diff --git a/src/managers/MessageManager.js b/src/managers/MessageManager.js index dd599dd..d156d55 100644 --- a/src/managers/MessageManager.js +++ b/src/managers/MessageManager.js @@ -1,247 +1,247 @@ -'use strict'; - -const { Collection } = require('@discordjs/collection'); -const CachedManager = require('./CachedManager'); -const { TypeError, Error } = require('../errors'); -const { Message } = require('../structures/Message'); -const MessagePayload = require('../structures/MessagePayload'); -const Util = require('../util/Util'); -const BigNumber = require('bignumber.js'); - -/** - * Manages API methods for Messages and holds their cache. - * @extends {CachedManager} - */ -class MessageManager extends CachedManager { - constructor(channel, iterable) { - super(channel.client, Message, iterable); - - /** - * The channel that the messages belong to - * @type {TextBasedChannels} - */ - this.channel = channel; - } - - /** - * The cache of Messages - * @type {Collection} - * @name MessageManager#cache - */ - - _add(data, cache) { - return super._add(data, cache); - } - - /** - * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and - * `after` are mutually exclusive. All the parameters are optional. - * @typedef {Object} ChannelLogsQueryOptions - * @property {number} [limit=50] Number of messages to acquire - * @property {Snowflake} [before] The message's id to get the messages that were posted before it - * @property {Snowflake} [after] The message's id to get the messages that were posted after it - * @property {Snowflake} [around] The message's id to get the messages that were posted around it - */ - - /** - * Gets a message, or messages, from this channel. - * The returned Collection does not contain reaction users of the messages if they were not cached. - * Those need to be fetched separately in such a case. - * @param {Snowflake|ChannelLogsQueryOptions} [message] The id of the message to fetch, or query parameters. - * @param {BaseFetchOptions} [options] Additional options for this fetch - * @returns {Promise>} - * @example - * // Get message - * channel.messages.fetch('99539446449315840') - * .then(message => console.log(message.content)) - * .catch(console.error); - * @example - * // Get messages - * channel.messages.fetch({ limit: 10 }) - * .then(messages => console.log(`Received ${messages.size} messages`)) - * .catch(console.error); - * @example - * // Get messages and filter by user id - * channel.messages.fetch() - * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) - * .catch(console.error); - */ - fetch(message, { cache = true, force = false } = {}) { - return typeof message === 'string' ? this._fetchId(message, cache, force) : this._fetchMany(message, cache); - } - - /** - * Fetches the pinned messages of this channel and returns a collection of them. - * The returned Collection does not contain any reaction data of the messages. - * Those need to be fetched separately. - * @param {boolean} [cache=true] Whether to cache the message(s) - * @returns {Promise>} - * @example - * // Get pinned messages - * channel.messages.fetchPinned() - * .then(messages => console.log(`Received ${messages.size} messages`)) - * .catch(console.error); - */ - async fetchPinned(cache = true) { - const data = await this.client.api.channels[this.channel.id].pins.get(); - const messages = new Collection(); - for (const message of data) messages.set(message.id, this._add(message, cache)); - return messages; - } - - /** - * Data that can be resolved to a Message object. This can be: - * * A Message - * * A Snowflake - * @typedef {Message|Snowflake} MessageResolvable - */ - - /** - * Resolves a {@link MessageResolvable} to a {@link Message} object. - * @method resolve - * @memberof MessageManager - * @instance - * @param {MessageResolvable} message The message resolvable to resolve - * @returns {?Message} - */ - - /** - * Resolves a {@link MessageResolvable} to a {@link Message} id. - * @method resolveId - * @memberof MessageManager - * @instance - * @param {MessageResolvable} message The message resolvable to resolve - * @returns {?Snowflake} - */ - - /** - * Edits a message, even if it's not cached. - * @param {MessageResolvable} message The message to edit - * @param {string|MessageEditOptions|MessagePayload} options The options to edit the message - * @returns {Promise} - */ - async edit(message, options) { - const messageId = this.resolveId(message); - if (!messageId) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - - let messagePayload; - if (options instanceof MessagePayload) { - messagePayload = await options.resolveData(); - } else { - messagePayload = await MessagePayload.create( - message instanceof Message ? message : this, - options, - ).resolveData(); - } - const { data, files } = await messagePayload.resolveFiles(); - const d = await this.client.api.channels[this.channel.id].messages[messageId].patch({ data, files }); - - const existing = this.cache.get(messageId); - if (existing) { - const clone = existing._clone(); - clone._patch(d); - return clone; - } - return this._add(d); - } - - /** - * Publishes a message in an announcement channel to all channels following it, even if it's not cached. - * @param {MessageResolvable} message The message to publish - * @returns {Promise} - */ - async crosspost(message) { - message = this.resolveId(message); - if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - - const data = await this.client.api.channels(this.channel.id).messages(message).crosspost.post(); - return this.cache.get(data.id) ?? this._add(data); - } - - /** - * Pins a message to the channel's pinned messages, even if it's not cached. - * @param {MessageResolvable} message The message to pin - * @returns {Promise} - */ - async pin(message) { - message = this.resolveId(message); - if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - - await this.client.api.channels(this.channel.id).pins(message).put(); - } - - /** - * Unpins a message from the channel's pinned messages, even if it's not cached. - * @param {MessageResolvable} message The message to unpin - * @returns {Promise} - */ - async unpin(message) { - message = this.resolveId(message); - if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - - await this.client.api.channels(this.channel.id).pins(message).delete(); - } - - /** - * Adds a reaction to a message, even if it's not cached. - * @param {MessageResolvable} message The message to react to - * @param {EmojiIdentifierResolvable} emoji The emoji to react with - * @returns {Promise} - */ - async react(message, emoji) { - message = this.resolveId(message); - if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - - emoji = Util.resolvePartialEmoji(emoji); - if (!emoji) throw new TypeError('EMOJI_TYPE', 'emoji', 'EmojiIdentifierResolvable'); - - const emojiId = emoji.id - ? `${emoji.animated ? 'a:' : ''}${emoji.name}:${emoji.id}` - : encodeURIComponent(emoji.name); - - // eslint-disable-next-line newline-per-chained-call - await this.client.api.channels(this.channel.id).messages(message).reactions(emojiId, '@me').put(); - } - - /** - * Deletes a message, even if it's not cached. - * @param {MessageResolvable} message The message to delete - * @returns {Promise} - */ - async delete(message) { - message = this.resolveId(message); - if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); - - await this.client.api.channels(this.channel.id).messages(message).delete(); - } - - async _fetchId(messageId, cache, force) { - if (!force) { - const existing = this.cache.get(messageId); - if (existing && !existing.partial) return existing; - } - - // const data = await this.client.api.channels[this.channel.id].messages[messageId].get(); // Discord Block - // https://canary.discord.com/api/v9/guilds/809133733591384155/messages/search?channel_id=840225732902518825&max_id=957254525360697375&min_id=957254525360697373 - const data = ( - await this.client.api.guilds[this.channel.guild.id].messages.search.get({ - query: { - channel_id: this.channel.id, - max_id: new BigNumber.BigNumber(messageId).plus(1).toString(), - min_id: new BigNumber.BigNumber(messageId).minus(1).toString(), - }, - }) - ).messages[0] - if (data) return this._add(data[0], cache); - else throw new Error('MESSAGE_ID_NOT_FOUND'); - } - - async _fetchMany(options = {}, cache) { - const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); - const messages = new Collection(); - for (const message of data) messages.set(message.id, this._add(message, cache)); - return messages; - } -} - -module.exports = MessageManager; +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const CachedManager = require('./CachedManager'); +const { TypeError, Error } = require('../errors'); +const { Message } = require('../structures/Message'); +const MessagePayload = require('../structures/MessagePayload'); +const Util = require('../util/Util'); +const BigNumber = require('bignumber.js'); + +/** + * Manages API methods for Messages and holds their cache. + * @extends {CachedManager} + */ +class MessageManager extends CachedManager { + constructor(channel, iterable) { + super(channel.client, Message, iterable); + + /** + * The channel that the messages belong to + * @type {TextBasedChannels} + */ + this.channel = channel; + } + + /** + * The cache of Messages + * @type {Collection} + * @name MessageManager#cache + */ + + _add(data, cache) { + return super._add(data, cache); + } + + /** + * The parameters to pass in when requesting previous messages from a channel. `around`, `before` and + * `after` are mutually exclusive. All the parameters are optional. + * @typedef {Object} ChannelLogsQueryOptions + * @property {number} [limit=50] Number of messages to acquire + * @property {Snowflake} [before] The message's id to get the messages that were posted before it + * @property {Snowflake} [after] The message's id to get the messages that were posted after it + * @property {Snowflake} [around] The message's id to get the messages that were posted around it + */ + + /** + * Gets a message, or messages, from this channel. + * The returned Collection does not contain reaction users of the messages if they were not cached. + * Those need to be fetched separately in such a case. + * @param {Snowflake|ChannelLogsQueryOptions} [message] The id of the message to fetch, or query parameters. + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise>} + * @example + * // Get message + * channel.messages.fetch('99539446449315840') + * .then(message => console.log(message.content)) + * .catch(console.error); + * @example + * // Get messages + * channel.messages.fetch({ limit: 10 }) + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + * @example + * // Get messages and filter by user id + * channel.messages.fetch() + * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) + * .catch(console.error); + */ + fetch(message, { cache = true, force = false } = {}) { + return typeof message === 'string' ? this._fetchId(message, cache, force) : this._fetchMany(message, cache); + } + + /** + * Fetches the pinned messages of this channel and returns a collection of them. + * The returned Collection does not contain any reaction data of the messages. + * Those need to be fetched separately. + * @param {boolean} [cache=true] Whether to cache the message(s) + * @returns {Promise>} + * @example + * // Get pinned messages + * channel.messages.fetchPinned() + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + */ + async fetchPinned(cache = true) { + const data = await this.client.api.channels[this.channel.id].pins.get(); + const messages = new Collection(); + for (const message of data) messages.set(message.id, this._add(message, cache)); + return messages; + } + + /** + * Data that can be resolved to a Message object. This can be: + * * A Message + * * A Snowflake + * @typedef {Message|Snowflake} MessageResolvable + */ + + /** + * Resolves a {@link MessageResolvable} to a {@link Message} object. + * @method resolve + * @memberof MessageManager + * @instance + * @param {MessageResolvable} message The message resolvable to resolve + * @returns {?Message} + */ + + /** + * Resolves a {@link MessageResolvable} to a {@link Message} id. + * @method resolveId + * @memberof MessageManager + * @instance + * @param {MessageResolvable} message The message resolvable to resolve + * @returns {?Snowflake} + */ + + /** + * Edits a message, even if it's not cached. + * @param {MessageResolvable} message The message to edit + * @param {string|MessageEditOptions|MessagePayload} options The options to edit the message + * @returns {Promise} + */ + async edit(message, options) { + const messageId = this.resolveId(message); + if (!messageId) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); + + let messagePayload; + if (options instanceof MessagePayload) { + messagePayload = await options.resolveData(); + } else { + messagePayload = await MessagePayload.create( + message instanceof Message ? message : this, + options, + ).resolveData(); + } + const { data, files } = await messagePayload.resolveFiles(); + const d = await this.client.api.channels[this.channel.id].messages[messageId].patch({ data, files }); + + const existing = this.cache.get(messageId); + if (existing) { + const clone = existing._clone(); + clone._patch(d); + return clone; + } + return this._add(d); + } + + /** + * Publishes a message in an announcement channel to all channels following it, even if it's not cached. + * @param {MessageResolvable} message The message to publish + * @returns {Promise} + */ + async crosspost(message) { + message = this.resolveId(message); + if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); + + const data = await this.client.api.channels(this.channel.id).messages(message).crosspost.post(); + return this.cache.get(data.id) ?? this._add(data); + } + + /** + * Pins a message to the channel's pinned messages, even if it's not cached. + * @param {MessageResolvable} message The message to pin + * @returns {Promise} + */ + async pin(message) { + message = this.resolveId(message); + if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); + + await this.client.api.channels(this.channel.id).pins(message).put(); + } + + /** + * Unpins a message from the channel's pinned messages, even if it's not cached. + * @param {MessageResolvable} message The message to unpin + * @returns {Promise} + */ + async unpin(message) { + message = this.resolveId(message); + if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); + + await this.client.api.channels(this.channel.id).pins(message).delete(); + } + + /** + * Adds a reaction to a message, even if it's not cached. + * @param {MessageResolvable} message The message to react to + * @param {EmojiIdentifierResolvable} emoji The emoji to react with + * @returns {Promise} + */ + async react(message, emoji) { + message = this.resolveId(message); + if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); + + emoji = Util.resolvePartialEmoji(emoji); + if (!emoji) throw new TypeError('EMOJI_TYPE', 'emoji', 'EmojiIdentifierResolvable'); + + const emojiId = emoji.id + ? `${emoji.animated ? 'a:' : ''}${emoji.name}:${emoji.id}` + : encodeURIComponent(emoji.name); + + // eslint-disable-next-line newline-per-chained-call + await this.client.api.channels(this.channel.id).messages(message).reactions(emojiId, '@me').put(); + } + + /** + * Deletes a message, even if it's not cached. + * @param {MessageResolvable} message The message to delete + * @returns {Promise} + */ + async delete(message) { + message = this.resolveId(message); + if (!message) throw new TypeError('INVALID_TYPE', 'message', 'MessageResolvable'); + + await this.client.api.channels(this.channel.id).messages(message).delete(); + } + + async _fetchId(messageId, cache, force) { + if (!force) { + const existing = this.cache.get(messageId); + if (existing && !existing.partial) return existing; + } + + // const data = await this.client.api.channels[this.channel.id].messages[messageId].get(); // Discord Block + // https://canary.discord.com/api/v9/guilds/809133733591384155/messages/search?channel_id=840225732902518825&max_id=957254525360697375&min_id=957254525360697373 + const data = ( + await this.client.api.guilds[this.channel.guild.id].messages.search.get({ + query: { + channel_id: this.channel.id, + max_id: new BigNumber.BigNumber(messageId).plus(1).toString(), + min_id: new BigNumber.BigNumber(messageId).minus(1).toString(), + }, + }) + ).messages[0] + if (data) return this._add(data[0], cache); + else throw new Error('MESSAGE_ID_NOT_FOUND'); + } + + async _fetchMany(options = {}, cache) { + const data = await this.client.api.channels[this.channel.id].messages.get({ query: options }); + const messages = new Collection(); + for (const message of data) messages.set(message.id, this._add(message, cache)); + return messages; + } +} + +module.exports = MessageManager; diff --git a/src/managers/ReactionUserManager.js b/src/managers/ReactionUserManager.js index e9089a8..fc9a217 100644 --- a/src/managers/ReactionUserManager.js +++ b/src/managers/ReactionUserManager.js @@ -3,14 +3,14 @@ const { Collection } = require('@discordjs/collection'); const CachedManager = require('./CachedManager'); const { Error } = require('../errors'); -const { User } = require('discord.js-selfbot-v13'); +const Discord = require("discord.js-selfbot-v13") /** * Manages API methods for users who reacted to a reaction and stores their cache. * @extends {CachedManager} */ class ReactionUserManager extends CachedManager { constructor(reaction, iterable) { - super(reaction.client, User, iterable); + super(reaction.client, Discord.User, iterable); /** * The reaction that this manager belongs to @@ -21,7 +21,7 @@ class ReactionUserManager extends CachedManager { /** * The cache of this manager - * @type {Collection} + * @type {Collection} * @name ReactionUserManager#cache */ @@ -35,7 +35,7 @@ class ReactionUserManager extends CachedManager { /** * Fetches all the users that gave this reaction. Resolves with a collection of users, mapped by their ids. * @param {FetchReactionUsersOptions} [options] Options for fetching the users - * @returns {Promise>} + * @returns {Promise>} */ async fetch({ limit = 100, after } = {}) { const message = this.reaction.message;