diff options
Diffstat (limited to 'node_modules/discord.js/src/managers')
35 files changed, 6082 insertions, 0 deletions
diff --git a/node_modules/discord.js/src/managers/ApplicationCommandManager.js b/node_modules/discord.js/src/managers/ApplicationCommandManager.js new file mode 100644 index 0000000..417afc4 --- /dev/null +++ b/node_modules/discord.js/src/managers/ApplicationCommandManager.js @@ -0,0 +1,263 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { makeURLSearchParams } = require('@discordjs/rest'); +const { isJSONEncodable } = require('@discordjs/util'); +const { Routes } = require('discord-api-types/v10'); +const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager'); +const CachedManager = require('./CachedManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const ApplicationCommand = require('../structures/ApplicationCommand'); +const PermissionsBitField = require('../util/PermissionsBitField'); + +/** + * Manages API methods for application commands and stores their cache. + * @extends {CachedManager} + */ +class ApplicationCommandManager extends CachedManager { + constructor(client, iterable) { + super(client, ApplicationCommand, iterable); + + /** + * The manager for permissions of arbitrary commands on arbitrary guilds + * @type {ApplicationCommandPermissionsManager} + */ + this.permissions = new ApplicationCommandPermissionsManager(this); + } + + /** + * The cache of this manager + * @type {Collection<Snowflake, ApplicationCommand>} + * @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 {string} + * @private + */ + commandPath({ id, guildId } = {}) { + if (this.guild ?? guildId) { + if (id) { + return Routes.applicationGuildCommand(this.client.application.id, this.guild?.id ?? guildId, id); + } + + return Routes.applicationGuildCommands(this.client.application.id, this.guild?.id ?? guildId); + } + + if (id) { + return Routes.applicationCommand(this.client.application.id, id); + } + + return Routes.applicationCommands(this.client.application.id); + } + + /** + * Data that resolves to give an ApplicationCommand object. This can be: + * * An ApplicationCommand object + * * A Snowflake + * @typedef {ApplicationCommand|Snowflake} ApplicationCommandResolvable + */ + + /** + * Data that resolves to the data of an ApplicationCommand + * @typedef {ApplicationCommandData|APIApplicationCommand} ApplicationCommandDataResolvable + */ + + /** + * 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 + * @property {LocaleString} [locale] The locale to use when fetching this command + * @property {boolean} [withLocalizations] Whether to fetch all localization data + */ + + /** + * 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<ApplicationCommand|Collection<Snowflake, ApplicationCommand>>} + * @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, locale, withLocalizations } = {}) { + if (typeof id === 'object') { + ({ guildId, cache = true, locale, withLocalizations } = id); + } else if (id) { + if (!force) { + const existing = this.cache.get(id); + if (existing) return existing; + } + const command = await this.client.rest.get(this.commandPath({ id, guildId })); + return this._add(command, cache); + } + + const data = await this.client.rest.get(this.commandPath({ guildId }), { + headers: { + 'X-Discord-Locale': locale, + }, + query: makeURLSearchParams({ with_localizations: withLocalizations }), + }); + return data.reduce((coll, command) => coll.set(command.id, this._add(command, cache, guildId)), new Collection()); + } + + /** + * Creates an application command. + * @param {ApplicationCommandDataResolvable} command The command + * @param {Snowflake} [guildId] The guild's id to create this command in, + * ignored when using a {@link GuildApplicationCommandManager} + * @returns {Promise<ApplicationCommand>} + * @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) { + const data = await this.client.rest.post(this.commandPath({ guildId }), { + body: this.constructor.transformCommand(command), + }); + return this._add(data, true, guildId); + } + + /** + * Sets all the commands for this application or guild. + * @param {ApplicationCommandDataResolvable[]} commands The commands + * @param {Snowflake} [guildId] The guild's id to create the commands in, + * ignored when using a {@link GuildApplicationCommandManager} + * @returns {Promise<Collection<Snowflake, ApplicationCommand>>} + * @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) { + const data = await this.client.rest.put(this.commandPath({ guildId }), { + body: 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 {Partial<ApplicationCommandDataResolvable>} 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<ApplicationCommand>} + * @example + * // Edit an existing command + * client.application.commands.edit('123456789012345678', { + * description: 'New description', + * }) + * .then(console.log) + * .catch(console.error); + */ + async edit(command, data, guildId) { + const id = this.resolveId(command); + if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'command', 'ApplicationCommandResolvable'); + + const patched = await this.client.rest.patch(this.commandPath({ id, guildId }), { + body: 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<?ApplicationCommand>} + * @example + * // Delete a command + * guild.commands.delete('123456789012345678') + * .then(console.log) + * .catch(console.error); + */ + async delete(command, guildId) { + const id = this.resolveId(command); + if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'command', 'ApplicationCommandResolvable'); + + await this.client.rest.delete(this.commandPath({ id, guildId })); + + 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 {ApplicationCommandDataResolvable} command The command to transform + * @returns {APIApplicationCommand} + * @private + */ + static transformCommand(command) { + if (isJSONEncodable(command)) return command.toJSON(); + + let default_member_permissions; + + if ('default_member_permissions' in command) { + default_member_permissions = command.default_member_permissions + ? new PermissionsBitField(BigInt(command.default_member_permissions)).bitfield.toString() + : command.default_member_permissions; + } + + if ('defaultMemberPermissions' in command) { + default_member_permissions = + command.defaultMemberPermissions !== null + ? new PermissionsBitField(command.defaultMemberPermissions).bitfield.toString() + : command.defaultMemberPermissions; + } + + return { + name: command.name, + name_localizations: command.nameLocalizations ?? command.name_localizations, + description: command.description, + nsfw: command.nsfw, + description_localizations: command.descriptionLocalizations ?? command.description_localizations, + type: command.type, + options: command.options?.map(o => ApplicationCommand.transformOption(o)), + default_member_permissions, + dm_permission: command.dmPermission ?? command.dm_permission, + }; + } +} + +module.exports = ApplicationCommandManager; diff --git a/node_modules/discord.js/src/managers/ApplicationCommandPermissionsManager.js b/node_modules/discord.js/src/managers/ApplicationCommandPermissionsManager.js new file mode 100644 index 0000000..2f7279a --- /dev/null +++ b/node_modules/discord.js/src/managers/ApplicationCommandPermissionsManager.js @@ -0,0 +1,434 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { ApplicationCommandPermissionType, RESTJSONErrorCodes, Routes } = require('discord-api-types/v10'); +const BaseManager = require('./BaseManager'); +const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors'); + +/** + * Manages API methods for permissions of Application Commands. + * @extends {BaseManager} + */ +class ApplicationCommandPermissionsManager extends BaseManager { + constructor(manager) { + super(manager.client); + + /** + * The manager or command that this manager belongs to + * @type {ApplicationCommandManager|ApplicationCommand} + * @private + */ + this.manager = manager; + + /** + * The guild that this manager acts on + * @type {?Guild} + */ + this.guild = manager.guild ?? null; + + /** + * The id of the guild that this manager acts on + * @type {?Snowflake} + */ + this.guildId = manager.guildId ?? manager.guild?.id ?? null; + + /** + * The id of the command this manager acts on + * @type {?Snowflake} + */ + this.commandId = manager.id ?? null; + } + + /** + * The APIRouter path to the commands + * @param {Snowflake} guildId The guild's id to use in the path, + * @param {Snowflake} [commandId] The application command's id + * @returns {string} + * @private + */ + permissionsPath(guildId, commandId) { + if (commandId) { + return Routes.applicationCommandPermissions(this.client.application.id, guildId, commandId); + } + + return Routes.guildApplicationCommandsPermissions(this.client.application.id, guildId); + } + + /* eslint-disable max-len */ + /** + * The object returned when fetching permissions for an application command. + * @typedef {Object} ApplicationCommandPermissions + * @property {Snowflake} id The role, user, or channel's id. Can also be a + * {@link https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-constants permission constant}. + * @property {ApplicationCommandPermissionType} type Whether this permission is for a role or a user + * @property {boolean} permission Whether the role or user has the permission to use this command + */ + /* eslint-enable max-len */ + + /** + * Options for managing permissions for one or more Application Commands + * <warn>When passing these options to a manager where `guildId` is `null`, + * `guild` is a required parameter</warn> + * @typedef {Object} BaseApplicationCommandPermissionsOptions + * @property {GuildResolvable} [guild] The guild to modify / check permissions for + * <warn>Ignored when the manager has a non-null `guildId` property</warn> + * @property {ApplicationCommandResolvable} [command] The command to modify / check permissions for + * <warn>Ignored when the manager has a non-null `commandId` property</warn> + */ + + /** + * Fetches the permissions for one or multiple commands. Providing the client's id as the "command id" will fetch + * *only* the guild level permissions + * @param {BaseApplicationCommandPermissionsOptions} [options] Options used to fetch permissions + * @returns {Promise<ApplicationCommandPermissions[]|Collection<Snowflake, ApplicationCommandPermissions[]>>} + * @example + * // Fetch permissions for one command + * guild.commands.permissions.fetch({ command: '123456789012345678' }) + * .then(perms => console.log(`Fetched ${perms.length} overwrites`)) + * .catch(console.error); + * @example + * // Fetch permissions for all commands in a guild + * client.application.commands.permissions.fetch({ guild: '123456789012345678' }) + * .then(perms => console.log(`Fetched permissions for ${perms.size} commands`)) + * .catch(console.error); + * @example + * // Fetch guild level permissions + * guild.commands.permissions.fetch({ command: client.user.id }) + * .then(perms => console.log(`Fetched ${perms.length} guild level permissions`)) + * .catch(console.error); + */ + async fetch({ guild, command } = {}) { + const { guildId, commandId } = this._validateOptions(guild, command); + if (commandId) { + const data = await this.client.rest.get(this.permissionsPath(guildId, commandId)); + return data.permissions; + } + + const data = await this.client.rest.get(this.permissionsPath(guildId)); + return data.reduce((coll, perm) => coll.set(perm.id, perm.permissions), new Collection()); + } + + /** + * Options used to set permissions for one or more Application Commands in a guild + * <warn>Omitting the `command` parameter edits the guild wide permissions + * when the manager's `commandId` is `null`</warn> + * @typedef {BaseApplicationCommandPermissionsOptions} ApplicationCommandPermissionsEditOptions + * @property {ApplicationCommandPermissions[]} permissions The new permissions for the guild or overwrite + * @property {string} token The bearer token to use that authorizes the permission edit + */ + + /** + * Sets the permissions for the guild or a command overwrite. + * @param {ApplicationCommandPermissionsEditOptions} options Options used to set permissions + * @returns {Promise<ApplicationCommandPermissions[]|Collection<Snowflake, ApplicationCommandPermissions[]>>} + * @example + * // Set a permission overwrite for a command + * client.application.commands.permissions.set({ + * guild: '892455839386304532', + * command: '123456789012345678', + * token: 'TotallyRealToken', + * permissions: [ + * { + * id: '876543210987654321', + * type: ApplicationCommandPermissionType.User, + * permission: false, + * }, + * ]}) + * .then(console.log) + * .catch(console.error); + * @example + * // Set the permissions used for the guild (commands without overwrites) + * guild.commands.permissions.set({ token: 'TotallyRealToken', permissions: [ + * { + * id: '123456789012345678', + * permissions: [{ + * id: '876543210987654321', + * type: ApplicationCommandPermissionType.User, + * permission: false, + * }], + * }, + * ]}) + * .then(console.log) + * .catch(console.error); + */ + async set({ guild, command, permissions, token } = {}) { + if (!token) { + throw new DiscordjsError(ErrorCodes.ApplicationCommandPermissionsTokenMissing); + } + let { guildId, commandId } = this._validateOptions(guild, command); + + if (!Array.isArray(permissions)) { + throw new DiscordjsTypeError( + ErrorCodes.InvalidType, + 'permissions', + 'Array of ApplicationCommandPermissions', + true, + ); + } + + if (!commandId) { + commandId = this.client.user.id; + } + const data = await this.client.rest.put(this.permissionsPath(guildId, commandId), { + body: { permissions }, + auth: false, + headers: { Authorization: `Bearer ${token}` }, + }); + return data.permissions; + } + + /** + * Add permissions to a command. + * @param {ApplicationCommandPermissionsEditOptions} options Options used to add permissions + * @returns {Promise<ApplicationCommandPermissions[]>} + * @example + * // Add a rule to block a role from using a command + * guild.commands.permissions.add({ command: '123456789012345678', token: 'TotallyRealToken', permissions: [ + * { + * id: '876543211234567890', + * type: ApplicationCommandPermissionType.Role, + * permission: false + * }, + * ]}) + * .then(console.log) + * .catch(console.error); + */ + async add({ guild, command, permissions, token } = {}) { + if (!token) { + throw new DiscordjsError(ErrorCodes.ApplicationCommandPermissionsTokenMissing); + } + let { guildId, commandId } = this._validateOptions(guild, command); + if (!commandId) { + commandId = this.client.user.id; + } + if (!Array.isArray(permissions)) { + throw new DiscordjsTypeError( + ErrorCodes.InvalidType, + 'permissions', + 'Array of ApplicationCommandPermissions', + true, + ); + } + + let existing = []; + try { + existing = await this.fetch({ guild: guildId, command: commandId }); + } catch (error) { + if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error; + } + + const newPermissions = permissions.slice(); + for (const perm of existing) { + if (!newPermissions.some(x => x.id === perm.id)) { + newPermissions.push(perm); + } + } + + return this.set({ guild: guildId, command: commandId, permissions: newPermissions, token }); + } + + /** + * A static snowflake that identifies the everyone role for application command permissions. + * It is the same as the guild id + * @typedef {Snowflake} RolePermissionConstant + */ + + /** + * A static snowflake that identifies the "all channels" entity for application command permissions. + * It will be the result of the calculation `guildId - 1` + * @typedef {Snowflake} ChannelPermissionConstant + */ + + /** + * Options used to remove permissions from a command + * <warn>Omitting the `command` parameter removes from the guild wide permissions + * when the managers `commandId` is `null`</warn> + * <warn>At least one of `users`, `roles`, and `channels` is required</warn> + * @typedef {BaseApplicationCommandPermissionsOptions} RemoveApplicationCommandPermissionsOptions + * @property {string} token The bearer token to use that authorizes the permission removal + * @property {UserResolvable[]} [users] The user(s) to remove + * @property {Array<RoleResolvable|RolePermissionConstant>} [roles] The role(s) to remove + * @property {Array<GuildChannelResolvable|ChannelPermissionConstant>} [channels] The channel(s) to remove + */ + + /** + * Remove permissions from a command. + * @param {RemoveApplicationCommandPermissionsOptions} options Options used to remove permissions + * @returns {Promise<ApplicationCommandPermissions[]>} + * @example + * // Remove a user permission from this command + * guild.commands.permissions.remove({ + * command: '123456789012345678', users: '876543210123456789', token: 'TotallyRealToken', + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove multiple roles from this command + * guild.commands.permissions.remove({ + * command: '123456789012345678', roles: ['876543210123456789', '765432101234567890'], token: 'TotallyRealToken', + * }) + * .then(console.log) + * .catch(console.error); + */ + async remove({ guild, command, users, roles, channels, token } = {}) { + if (!token) { + throw new DiscordjsError(ErrorCodes.ApplicationCommandPermissionsTokenMissing); + } + let { guildId, commandId } = this._validateOptions(guild, command); + if (!commandId) { + commandId = this.client.user.id; + } + + if (!users && !roles && !channels) { + throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'users OR roles OR channels', 'Array or Resolvable', true); + } + + let resolvedUserIds = []; + if (Array.isArray(users)) { + for (const user of users) { + const userId = this.client.users.resolveId(user); + if (!userId) throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array', 'users', user); + resolvedUserIds.push(userId); + } + } + + let resolvedRoleIds = []; + if (Array.isArray(roles)) { + for (const role of roles) { + if (typeof role === 'string') { + resolvedRoleIds.push(role); + continue; + } + if (!this.guild) throw new DiscordjsError(ErrorCodes.GuildUncachedEntityResolve, 'roles'); + const roleId = this.guild.roles.resolveId(role); + if (!roleId) throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array', 'users', role); + resolvedRoleIds.push(roleId); + } + } + + let resolvedChannelIds = []; + if (Array.isArray(channels)) { + for (const channel of channels) { + if (typeof channel === 'string') { + resolvedChannelIds.push(channel); + continue; + } + if (!this.guild) throw new DiscordjsError(ErrorCodes.GuildUncachedEntityResolve, 'channels'); + const channelId = this.guild.channels.resolveId(channel); + if (!channelId) throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array', 'channels', channel); + resolvedChannelIds.push(channelId); + } + } + + let existing = []; + try { + existing = await this.fetch({ guild: guildId, command: commandId }); + } catch (error) { + if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error; + } + + const permissions = existing.filter(perm => { + switch (perm.type) { + case ApplicationCommandPermissionType.Role: + return !resolvedRoleIds.includes(perm.id); + case ApplicationCommandPermissionType.User: + return !resolvedUserIds.includes(perm.id); + case ApplicationCommandPermissionType.Channel: + return !resolvedChannelIds.includes(perm.id); + } + return true; + }); + + return this.set({ guild: guildId, command: commandId, permissions, token }); + } + + /** + * Options used to check the existence of permissions on a command + * <warn>The `command` parameter is not optional when the managers `commandId` is `null`</warn> + * @typedef {BaseApplicationCommandPermissionsOptions} HasApplicationCommandPermissionsOptions + * @property {ApplicationCommandPermissionIdResolvable} permissionId The entity to check if a permission exists for + * on this command. + * @property {ApplicationCommandPermissionType} [permissionType] Check for a specific type of permission + */ + + /** + * Check whether a permission exists for a user, role, or channel + * @param {HasApplicationCommandPermissionsOptions} options Options used to check permissions + * @returns {Promise<boolean>} + * @example + * // Check whether a user has permission to use a command + * guild.commands.permissions.has({ command: '123456789012345678', permissionId: '876543210123456789' }) + * .then(console.log) + * .catch(console.error); + */ + async has({ guild, command, permissionId, permissionType }) { + const { guildId, commandId } = this._validateOptions(guild, command); + if (!commandId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'command', 'ApplicationCommandResolvable'); + + if (!permissionId) { + throw new DiscordjsTypeError( + ErrorCodes.InvalidType, + 'permissionId', + 'UserResolvable, RoleResolvable, ChannelResolvable, or Permission Constant', + ); + } + let resolvedId = permissionId; + if (typeof permissionId !== 'string') { + resolvedId = this.client.users.resolveId(permissionId); + if (!resolvedId) { + if (!this.guild) throw new DiscordjsError(ErrorCodes.GuildUncachedEntityResolve, 'roles'); + resolvedId = this.guild.roles.resolveId(permissionId); + } + if (!resolvedId) { + resolvedId = this.guild.channels.resolveId(permissionId); + } + if (!resolvedId) { + throw new DiscordjsTypeError( + ErrorCodes.InvalidType, + 'permissionId', + 'UserResolvable, RoleResolvable, ChannelResolvable, or Permission Constant', + ); + } + } + + let existing = []; + try { + existing = await this.fetch({ guild: guildId, command: commandId }); + } catch (error) { + if (error.code !== RESTJSONErrorCodes.UnknownApplicationCommandPermissions) throw error; + } + + // Check permission type if provided for the single edge case where a channel id is the same as the everyone role id + return existing.some(perm => perm.id === resolvedId && (permissionType ?? perm.type) === perm.type); + } + + _validateOptions(guild, command) { + const guildId = this.guildId ?? this.client.guilds.resolveId(guild); + if (!guildId) throw new DiscordjsError(ErrorCodes.GlobalCommandPermissions); + let commandId = this.commandId; + if (command && !commandId) { + commandId = this.manager.resolveId?.(command); + if (!commandId && this.guild) { + commandId = this.guild.commands.resolveId(command); + } + commandId ??= this.client.application?.commands.resolveId(command); + if (!commandId) { + throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'command', 'ApplicationCommandResolvable', true); + } + } + return { guildId, commandId }; + } +} + +module.exports = ApplicationCommandPermissionsManager; + +/* eslint-disable max-len */ +/** + * @external APIApplicationCommandPermissions + * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-permissions-object-application-command-permissions-structure} + */ + +/** + * Data that resolves to an id used for an application command permission + * @typedef {UserResolvable|RoleResolvable|GuildChannelResolvable|RolePermissionConstant|ChannelPermissionConstant} ApplicationCommandPermissionIdResolvable + */ diff --git a/node_modules/discord.js/src/managers/AutoModerationRuleManager.js b/node_modules/discord.js/src/managers/AutoModerationRuleManager.js new file mode 100644 index 0000000..dd0ee4e --- /dev/null +++ b/node_modules/discord.js/src/managers/AutoModerationRuleManager.js @@ -0,0 +1,288 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const AutoModerationRule = require('../structures/AutoModerationRule'); + +/** + * Manages API methods for auto moderation rules and stores their cache. + * @extends {CachedManager} + */ +class AutoModerationRuleManager extends CachedManager { + constructor(guild, iterable) { + super(guild.client, AutoModerationRule, iterable); + + /** + * The guild this manager belongs to. + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of this manager + * @type {Collection<Snowflake, AutoModerationRule>} + * @name AutoModerationRuleManager#cache + */ + + /** + * Resolves an {@link AutoModerationRuleResolvable} to an {@link AutoModerationRule} object. + * @method resolve + * @memberof AutoModerationRuleManager + * @instance + * @param {AutoModerationRuleResolvable} autoModerationRule The AutoModerationRule resolvable to resolve + * @returns {?AutoModerationRule} + */ + + /** + * Resolves an {@link AutoModerationRuleResolvable} to a {@link AutoModerationRule} id. + * @method resolveId + * @memberof AutoModerationRuleManager + * @instance + * @param {AutoModerationRuleResolvable} autoModerationRule The AutoModerationRule resolvable to resolve + * @returns {?Snowflake} + */ + + _add(data, cache) { + return super._add(data, cache, { extras: [this.guild] }); + } + + /** + * Options used to set the trigger metadata of an auto moderation rule. + * @typedef {Object} AutoModerationTriggerMetadataOptions + * @property {string[]} [keywordFilter] The substrings that will be searched for in the content + * @property {string[]} [regexPatterns] The regular expression patterns which will be matched against the content + * <info>Only Rust-flavored regular expressions are supported.</info> + * @property {AutoModerationRuleKeywordPresetType[]} [presets] + * The internally pre-defined wordsets which will be searched for in the content + * @property {string[]} [allowList] The substrings that will be exempt from triggering + * {@link AutoModerationRuleTriggerType.Keyword} and {@link AutoModerationRuleTriggerType.KeywordPreset} + * @property {?number} [mentionTotalLimit] The total number of role & user mentions allowed per message + * @property {boolean} [mentionRaidProtectionEnabled] Whether to automatically detect mention raids + */ + + /** + * Options used to set the actions of an auto moderation rule. + * @typedef {Object} AutoModerationActionOptions + * @property {AutoModerationActionType} type The type of this auto moderation rule action + * @property {AutoModerationActionMetadataOptions} [metadata] Additional metadata needed during execution + * <info>This property is required if using a `type` of + * {@link AutoModerationActionType.SendAlertMessage} or {@link AutoModerationActionType.Timeout}.</info> + */ + + /** + * Options used to set the metadata of an auto moderation rule action. + * @typedef {Object} AutoModerationActionMetadataOptions + * @property {GuildTextChannelResolvable|ThreadChannel} [channel] The channel to which content will be logged + * @property {number} [durationSeconds] The timeout duration in seconds + * @property {string} [customMessage] The custom message that is shown whenever a message is blocked + */ + + /** + * Options used to create an auto moderation rule. + * @typedef {Object} AutoModerationRuleCreateOptions + * @property {string} name The name of the auto moderation rule + * @property {AutoModerationRuleEventType} eventType The event type of the auto moderation rule + * @property {AutoModerationRuleTriggerType} triggerType The trigger type of the auto moderation rule + * @property {AutoModerationTriggerMetadataOptions} [triggerMetadata] The trigger metadata of the auto moderation rule + * <info>This property is required if using a `triggerType` of + * {@link AutoModerationRuleTriggerType.Keyword}, {@link AutoModerationRuleTriggerType.KeywordPreset}, + * or {@link AutoModerationRuleTriggerType.MentionSpam}.</info> + * @property {AutoModerationActionOptions[]} actions + * The actions that will execute when the auto moderation rule is triggered + * @property {boolean} [enabled] Whether the auto moderation rule should be enabled + * @property {Collection<Snowflake, Role>|RoleResolvable[]} [exemptRoles] + * The roles that should not be affected by the auto moderation rule + * @property {Collection<Snowflake, GuildChannel|ThreadChannel>|GuildChannelResolvable[]} [exemptChannels] + * The channels that should not be affected by the auto moderation rule + * @property {string} [reason] The reason for creating the auto moderation rule + */ + + /** + * Creates a new auto moderation rule. + * @param {AutoModerationRuleCreateOptions} options Options for creating the auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + async create({ + name, + eventType, + triggerType, + triggerMetadata, + actions, + enabled, + exemptRoles, + exemptChannels, + reason, + }) { + const data = await this.client.rest.post(Routes.guildAutoModerationRules(this.guild.id), { + body: { + name, + event_type: eventType, + trigger_type: triggerType, + trigger_metadata: triggerMetadata && { + keyword_filter: triggerMetadata.keywordFilter, + regex_patterns: triggerMetadata.regexPatterns, + presets: triggerMetadata.presets, + allow_list: triggerMetadata.allowList, + mention_total_limit: triggerMetadata.mentionTotalLimit, + mention_raid_protection_enabled: triggerMetadata.mentionRaidProtectionEnabled, + }, + actions: actions.map(action => ({ + type: action.type, + metadata: { + duration_seconds: action.metadata?.durationSeconds, + channel_id: action.metadata?.channel && this.guild.channels.resolveId(action.metadata.channel), + custom_message: action.metadata?.customMessage, + }, + })), + enabled, + exempt_roles: exemptRoles?.map(exemptRole => this.guild.roles.resolveId(exemptRole)), + exempt_channels: exemptChannels?.map(exemptChannel => this.guild.channels.resolveId(exemptChannel)), + }, + reason, + }); + + return this._add(data); + } + + /** + * Options used to edit an auto moderation rule. + * @typedef {Object} AutoModerationRuleEditOptions + * @property {string} [name] The name of the auto moderation rule + * @property {AutoModerationRuleEventType} [eventType] The event type of the auto moderation rule + * @property {AutoModerationTriggerMetadataOptions} [triggerMetadata] The trigger metadata of the auto moderation rule + * @property {AutoModerationActionOptions[]} [actions] + * The actions that will execute when the auto moderation rule is triggered + * @property {boolean} [enabled] Whether the auto moderation rule should be enabled + * @property {Collection<Snowflake, Role>|RoleResolvable[]} [exemptRoles] + * The roles that should not be affected by the auto moderation rule + * @property {Collection<Snowflake, GuildChannel|ThreadChannel>|GuildChannelResolvable[]} [exemptChannels] + * The channels that should not be affected by the auto moderation rule + * @property {string} [reason] The reason for creating the auto moderation rule + */ + + /** + * Edits an auto moderation rule. + * @param {AutoModerationRuleResolvable} autoModerationRule The auto moderation rule to edit + * @param {AutoModerationRuleEditOptions} options Options for editing the auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + async edit( + autoModerationRule, + { name, eventType, triggerMetadata, actions, enabled, exemptRoles, exemptChannels, reason }, + ) { + const autoModerationRuleId = this.resolveId(autoModerationRule); + + const data = await this.client.rest.patch(Routes.guildAutoModerationRule(this.guild.id, autoModerationRuleId), { + body: { + name, + event_type: eventType, + trigger_metadata: triggerMetadata && { + keyword_filter: triggerMetadata.keywordFilter, + regex_patterns: triggerMetadata.regexPatterns, + presets: triggerMetadata.presets, + allow_list: triggerMetadata.allowList, + mention_total_limit: triggerMetadata.mentionTotalLimit, + mention_raid_protection_enabled: triggerMetadata.mentionRaidProtectionEnabled, + }, + actions: actions?.map(action => ({ + type: action.type, + metadata: { + duration_seconds: action.metadata?.durationSeconds, + channel_id: action.metadata?.channel && this.guild.channels.resolveId(action.metadata.channel), + custom_message: action.metadata?.customMessage, + }, + })), + enabled, + exempt_roles: exemptRoles?.map(exemptRole => this.guild.roles.resolveId(exemptRole)), + exempt_channels: exemptChannels?.map(exemptChannel => this.guild.channels.resolveId(exemptChannel)), + }, + reason, + }); + + return this._add(data); + } + + /** + * Data that can be resolved to give an AutoModerationRule object. This can be: + * * An AutoModerationRule + * * A Snowflake + * @typedef {AutoModerationRule|Snowflake} AutoModerationRuleResolvable + */ + + /** + * Options used to fetch a single auto moderation rule from a guild. + * @typedef {BaseFetchOptions} FetchAutoModerationRuleOptions + * @property {AutoModerationRuleResolvable} autoModerationRule The auto moderation rule to fetch + */ + + /** + * Options used to fetch all auto moderation rules from a guild. + * @typedef {Object} FetchAutoModerationRulesOptions + * @property {boolean} [cache] Whether to cache the fetched auto moderation rules + */ + + /** + * Fetches auto moderation rules from Discord. + * @param {AutoModerationRuleResolvable|FetchAutoModerationRuleOptions|FetchAutoModerationRulesOptions} [options] + * Options for fetching auto moderation rule(s) + * @returns {Promise<AutoModerationRule|Collection<Snowflake, AutoModerationRule>>} + * @example + * // Fetch all auto moderation rules from a guild without caching + * guild.autoModerationRules.fetch({ cache: false }) + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch a single auto moderation rule + * guild.autoModerationRules.fetch('979083472868098119') + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch a single auto moderation rule without checking cache and without caching + * guild.autoModerationRules.fetch({ autoModerationRule: '979083472868098119', cache: false, force: true }) + * .then(console.log) + * .catch(console.error) + */ + fetch(options) { + if (!options) return this._fetchMany(); + const { autoModerationRule, cache, force } = options; + const resolvedAutoModerationRule = this.resolveId(autoModerationRule ?? options); + if (resolvedAutoModerationRule) { + return this._fetchSingle({ autoModerationRule: resolvedAutoModerationRule, cache, force }); + } + return this._fetchMany(options); + } + + async _fetchSingle({ autoModerationRule, cache, force = false }) { + if (!force) { + const existing = this.cache.get(autoModerationRule); + if (existing) return existing; + } + + const data = await this.client.rest.get(Routes.guildAutoModerationRule(this.guild.id, autoModerationRule)); + return this._add(data, cache); + } + + async _fetchMany(options = {}) { + const data = await this.client.rest.get(Routes.guildAutoModerationRules(this.guild.id)); + + return data.reduce( + (col, autoModerationRule) => col.set(autoModerationRule.id, this._add(autoModerationRule, options.cache)), + new Collection(), + ); + } + + /** + * Deletes an auto moderation rule. + * @param {AutoModerationRuleResolvable} autoModerationRule The auto moderation rule to delete + * @param {string} [reason] The reason for deleting the auto moderation rule + * @returns {Promise<void>} + */ + async delete(autoModerationRule, reason) { + const autoModerationRuleId = this.resolveId(autoModerationRule); + await this.client.rest.delete(Routes.guildAutoModerationRule(this.guild.id, autoModerationRuleId), { reason }); + } +} + +module.exports = AutoModerationRuleManager; diff --git a/node_modules/discord.js/src/managers/BaseGuildEmojiManager.js b/node_modules/discord.js/src/managers/BaseGuildEmojiManager.js new file mode 100644 index 0000000..89eee4c --- /dev/null +++ b/node_modules/discord.js/src/managers/BaseGuildEmojiManager.js @@ -0,0 +1,80 @@ +'use strict'; + +const CachedManager = require('./CachedManager'); +const GuildEmoji = require('../structures/GuildEmoji'); +const ReactionEmoji = require('../structures/ReactionEmoji'); +const { parseEmoji } = require('../util/Util'); + +/** + * Holds methods to resolve GuildEmojis and stores their cache. + * @extends {CachedManager} + */ +class BaseGuildEmojiManager extends CachedManager { + constructor(client, iterable) { + super(client, GuildEmoji, iterable); + } + + /** + * The cache of GuildEmojis + * @type {Collection<Snowflake, GuildEmoji>} + * @name BaseGuildEmojiManager#cache + */ + + /** + * Data that can be resolved into a GuildEmoji object. This can be: + * * A Snowflake + * * A GuildEmoji object + * * A ReactionEmoji object + * @typedef {Snowflake|GuildEmoji|ReactionEmoji} EmojiResolvable + */ + + /** + * Resolves an EmojiResolvable to an Emoji object. + * @param {EmojiResolvable} emoji The Emoji resolvable to identify + * @returns {?GuildEmoji} + */ + resolve(emoji) { + if (emoji instanceof ReactionEmoji) return super.resolve(emoji.id); + return super.resolve(emoji); + } + + /** + * Resolves an EmojiResolvable to an Emoji id string. + * @param {EmojiResolvable} emoji The Emoji resolvable to identify + * @returns {?Snowflake} + */ + resolveId(emoji) { + if (emoji instanceof ReactionEmoji) return emoji.id; + return super.resolveId(emoji); + } + + /** + * Data that can be resolved to give an emoji identifier. This can be: + * * An EmojiResolvable + * * The `<a:name:id>`, `<:name:id>`, `a:name:id` or `name:id` emoji identifier string of an emoji + * * The Unicode representation of an emoji + * @typedef {string|EmojiResolvable} EmojiIdentifierResolvable + */ + + /** + * Resolves an EmojiResolvable to an emoji identifier. + * @param {EmojiIdentifierResolvable} emoji The emoji resolvable to resolve + * @returns {?string} + */ + resolveIdentifier(emoji) { + const emojiResolvable = this.resolve(emoji); + if (emojiResolvable) return emojiResolvable.identifier; + if (emoji instanceof ReactionEmoji) return emoji.identifier; + if (typeof emoji === 'string') { + const res = parseEmoji(emoji); + if (res?.name.length) { + emoji = `${res.animated ? 'a:' : ''}${res.name}${res.id ? `:${res.id}` : ''}`; + } + if (!emoji.includes('%')) return encodeURIComponent(emoji); + return emoji; + } + return null; + } +} + +module.exports = BaseGuildEmojiManager; diff --git a/node_modules/discord.js/src/managers/BaseManager.js b/node_modules/discord.js/src/managers/BaseManager.js new file mode 100644 index 0000000..0651401 --- /dev/null +++ b/node_modules/discord.js/src/managers/BaseManager.js @@ -0,0 +1,19 @@ +'use strict'; + +/** + * Manages the API methods of a data model. + * @abstract + */ +class BaseManager { + constructor(client) { + /** + * The client that instantiated this Manager + * @name BaseManager#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + } +} + +module.exports = BaseManager; diff --git a/node_modules/discord.js/src/managers/CachedManager.js b/node_modules/discord.js/src/managers/CachedManager.js new file mode 100644 index 0000000..b4c50b1 --- /dev/null +++ b/node_modules/discord.js/src/managers/CachedManager.js @@ -0,0 +1,64 @@ +'use strict'; + +const DataManager = require('./DataManager'); +const { MakeCacheOverrideSymbol } = require('../util/Symbols'); + +/** + * Manages the API methods of a data model with a mutable cache of instances. + * @extends {DataManager} + * @abstract + */ +class CachedManager extends DataManager { + constructor(client, holds, iterable) { + super(client, holds); + + /** + * The private cache of items for this manager. + * @type {Collection} + * @private + * @readonly + * @name CachedManager#_cache + */ + Object.defineProperty(this, '_cache', { + value: this.client.options.makeCache( + this.constructor[MakeCacheOverrideSymbol] ?? this.constructor, + this.holds, + this.constructor, + ), + }); + + if (iterable) { + for (const item of iterable) { + this._add(item); + } + } + } + + /** + * The cache of items for this manager. + * @type {Collection} + * @abstract + */ + get cache() { + return this._cache; + } + + _add(data, cache = true, { id, extras = [] } = {}) { + const existing = this.cache.get(id ?? data.id); + if (existing) { + if (cache) { + existing._patch(data); + return existing; + } + const clone = existing._clone(); + clone._patch(data); + return clone; + } + + const entry = this.holds ? new this.holds(this.client, data, ...extras) : data; + if (cache) this.cache.set(id ?? entry.id, entry); + return entry; + } +} + +module.exports = CachedManager; diff --git a/node_modules/discord.js/src/managers/CategoryChannelChildManager.js b/node_modules/discord.js/src/managers/CategoryChannelChildManager.js new file mode 100644 index 0000000..347526a --- /dev/null +++ b/node_modules/discord.js/src/managers/CategoryChannelChildManager.js @@ -0,0 +1,77 @@ +'use strict'; + +const DataManager = require('./DataManager'); +const GuildChannel = require('../structures/GuildChannel'); + +/** + * Manages API methods for CategoryChannels' children. + * @extends {DataManager} + */ +class CategoryChannelChildManager extends DataManager { + constructor(channel) { + super(channel.client, GuildChannel); + /** + * The category channel this manager belongs to + * @type {CategoryChannel} + */ + this.channel = channel; + } + + /** + * The channels that are a part of this category + * @type {Collection<Snowflake, GuildChannel>} + * @readonly + */ + get cache() { + return this.guild.channels.cache.filter(c => c.parentId === this.channel.id); + } + + /** + * The guild this manager belongs to + * @type {Guild} + * @readonly + */ + get guild() { + return this.channel.guild; + } + + /** + * Options for creating a channel using {@link CategoryChannel#createChannel}. + * @typedef {Object} CategoryCreateChannelOptions + * @property {string} name The name for the new channel + * @property {ChannelType} [type=ChannelType.GuildText] The type of the new channel. + * @property {string} [topic] The topic for the new channel + * @property {boolean} [nsfw] Whether the new channel is NSFW + * @property {number} [bitrate] Bitrate of the new channel in bits (only voice) + * @property {number} [userLimit] Maximum amount of users allowed in the new channel (only voice) + * @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites] + * Permission overwrites of the new channel + * @property {number} [position] Position of the new channel + * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds + * @property {string} [rtcRegion] The specific region of the new channel. + * @property {VideoQualityMode} [videoQualityMode] The camera video quality mode of the voice channel + * @property {GuildForumTagData[]} [availableTags] The tags that can be used in this channel (forum only). + * @property {DefaultReactionEmoji} [defaultReactionEmoji] + * The emoji to show in the add reaction button on a thread in a guild forum channel. + * @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration] + * The default auto archive duration for all new threads in this channel + * @property {SortOrderType} [defaultSortOrder] The default sort order mode used to order posts (forum only). + * @property {ForumLayoutType} [defaultForumLayout] The default layout used to display posts (forum only). + * @property {string} [reason] Reason for creating the new channel + */ + + /** + * Creates a new channel within this category. + * <info>You cannot create a channel of type {@link ChannelType.GuildCategory} inside a CategoryChannel.</info> + * @param {CategoryCreateChannelOptions} options Options for creating the new channel + * @returns {Promise<GuildChannel>} + */ + create(options) { + return this.guild.channels.create({ + ...options, + parent: this.channel.id, + }); + } +} + +module.exports = CategoryChannelChildManager; diff --git a/node_modules/discord.js/src/managers/ChannelManager.js b/node_modules/discord.js/src/managers/ChannelManager.js new file mode 100644 index 0000000..0126d91 --- /dev/null +++ b/node_modules/discord.js/src/managers/ChannelManager.js @@ -0,0 +1,128 @@ +'use strict'; + +const process = require('node:process'); +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { BaseChannel } = require('../structures/BaseChannel'); +const { createChannel } = require('../util/Channels'); +const { ThreadChannelTypes } = require('../util/Constants'); +const Events = require('../util/Events'); + +let cacheWarningEmitted = false; + +/** + * A manager of channels belonging to a client + * @extends {CachedManager} + */ +class ChannelManager extends CachedManager { + constructor(client, iterable) { + super(client, BaseChannel, iterable); + const defaultCaching = + this._cache.constructor.name === 'Collection' || + this._cache.maxSize === undefined || + this._cache.maxSize === Infinity; + if (!cacheWarningEmitted && !defaultCaching) { + cacheWarningEmitted = true; + process.emitWarning( + `Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`, + 'UnsupportedCacheOverwriteWarning', + ); + } + } + + /** + * The cache of Channels + * @type {Collection<Snowflake, BaseChannel>} + * @name ChannelManager#cache + */ + + _add(data, guild, { cache = true, allowUnknownGuild = false } = {}) { + const existing = this.cache.get(data.id); + if (existing) { + if (cache) existing._patch(data); + guild?.channels?._add(existing); + if (ThreadChannelTypes.includes(existing.type)) { + existing.parent?.threads?._add(existing); + } + return existing; + } + + const channel = createChannel(this.client, data, guild, { allowUnknownGuild }); + + if (!channel) { + this.client.emit(Events.Debug, `Failed to find guild, or unknown type for channel ${data.id} ${data.type}`); + return null; + } + + if (cache && !allowUnknownGuild) this.cache.set(channel.id, channel); + + return channel; + } + + _remove(id) { + const channel = this.cache.get(id); + channel?.guild?.channels.cache.delete(id); + + for (const [code, invite] of channel?.guild?.invites.cache ?? []) { + if (invite.channelId === id) channel.guild.invites.cache.delete(code); + } + + channel?.parent?.threads?.cache.delete(id); + this.cache.delete(id); + } + + /** + * Data that can be resolved to give a Channel object. This can be: + * * A Channel object + * * A Snowflake + * @typedef {BaseChannel|Snowflake} ChannelResolvable + */ + + /** + * Resolves a ChannelResolvable to a Channel object. + * @method resolve + * @memberof ChannelManager + * @instance + * @param {ChannelResolvable} channel The channel resolvable to resolve + * @returns {?BaseChannel} + */ + + /** + * Resolves a ChannelResolvable to a channel id string. + * @method resolveId + * @memberof ChannelManager + * @instance + * @param {ChannelResolvable} channel The channel resolvable to resolve + * @returns {?Snowflake} + */ + + /** + * Options for fetching a channel from Discord + * @typedef {BaseFetchOptions} FetchChannelOptions + * @property {boolean} [allowUnknownGuild=false] Allows the channel to be returned even if the guild is not in cache, + * it will not be cached. <warn>Many of the properties and methods on the returned channel will throw errors</warn> + */ + + /** + * Obtains a channel from Discord, or the channel cache if it's already available. + * @param {Snowflake} id The channel's id + * @param {FetchChannelOptions} [options] Additional options for this fetch + * @returns {Promise<?BaseChannel>} + * @example + * // Fetch a channel by its id + * client.channels.fetch('222109930545610754') + * .then(channel => console.log(channel.name)) + * .catch(console.error); + */ + async fetch(id, { allowUnknownGuild = false, cache = true, force = false } = {}) { + if (!force) { + const existing = this.cache.get(id); + if (existing && !existing.partial) return existing; + } + + const data = await this.client.rest.get(Routes.channel(id)); + return this._add(data, null, { cache, allowUnknownGuild }); + } +} + +module.exports = ChannelManager; diff --git a/node_modules/discord.js/src/managers/DMMessageManager.js b/node_modules/discord.js/src/managers/DMMessageManager.js new file mode 100644 index 0000000..f0b3a33 --- /dev/null +++ b/node_modules/discord.js/src/managers/DMMessageManager.js @@ -0,0 +1,17 @@ +'use strict'; + +const MessageManager = require('./MessageManager'); + +/** + * Manages API methods for messages in direct message channels and holds their cache. + * @extends {MessageManager} + */ +class DMMessageManager extends MessageManager { + /** + * The channel that the messages belong to + * @name DMMessageManager#channel + * @type {DMChannel} + */ +} + +module.exports = DMMessageManager; diff --git a/node_modules/discord.js/src/managers/DataManager.js b/node_modules/discord.js/src/managers/DataManager.js new file mode 100644 index 0000000..383844e --- /dev/null +++ b/node_modules/discord.js/src/managers/DataManager.js @@ -0,0 +1,61 @@ +'use strict'; + +const BaseManager = require('./BaseManager'); +const { DiscordjsError, ErrorCodes } = require('../errors'); + +/** + * Manages the API methods of a data model along with a collection of instances. + * @extends {BaseManager} + * @abstract + */ +class DataManager extends BaseManager { + constructor(client, holds) { + super(client); + + /** + * The data structure belonging to this manager. + * @name DataManager#holds + * @type {Function} + * @private + * @readonly + */ + Object.defineProperty(this, 'holds', { value: holds }); + } + + /** + * The cache of items for this manager. + * @type {Collection} + * @abstract + */ + get cache() { + throw new DiscordjsError(ErrorCodes.NotImplemented, 'get cache', this.constructor.name); + } + + /** + * Resolves a data entry to a data Object. + * @param {string|Object} idOrInstance The id or instance of something in this Manager + * @returns {?Object} An instance from this Manager + */ + resolve(idOrInstance) { + if (idOrInstance instanceof this.holds) return idOrInstance; + if (typeof idOrInstance === 'string') return this.cache.get(idOrInstance) ?? null; + return null; + } + + /** + * Resolves a data entry to an instance id. + * @param {string|Object} idOrInstance The id or instance of something in this Manager + * @returns {?Snowflake} + */ + resolveId(idOrInstance) { + if (idOrInstance instanceof this.holds) return idOrInstance.id; + if (typeof idOrInstance === 'string') return idOrInstance; + return null; + } + + valueOf() { + return this.cache; + } +} + +module.exports = DataManager; diff --git a/node_modules/discord.js/src/managers/GuildApplicationCommandManager.js b/node_modules/discord.js/src/managers/GuildApplicationCommandManager.js new file mode 100644 index 0000000..97fea5e --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildApplicationCommandManager.js @@ -0,0 +1,28 @@ +'use strict'; + +const ApplicationCommandManager = require('./ApplicationCommandManager'); +const ApplicationCommandPermissionsManager = require('./ApplicationCommandPermissionsManager'); + +/** + * An extension for guild-specific application commands. + * @extends {ApplicationCommandManager} + */ +class GuildApplicationCommandManager extends ApplicationCommandManager { + constructor(guild, iterable) { + super(guild.client, iterable); + + /** + * The guild that this manager belongs to + * @type {Guild} + */ + this.guild = guild; + + /** + * The manager for permissions of arbitrary commands on this guild + * @type {ApplicationCommandPermissionsManager} + */ + this.permissions = new ApplicationCommandPermissionsManager(this); + } +} + +module.exports = GuildApplicationCommandManager; diff --git a/node_modules/discord.js/src/managers/GuildBanManager.js b/node_modules/discord.js/src/managers/GuildBanManager.js new file mode 100644 index 0000000..d3c8a00 --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildBanManager.js @@ -0,0 +1,204 @@ +'use strict'; + +const process = require('node:process'); +const { Collection } = require('@discordjs/collection'); +const { makeURLSearchParams } = require('@discordjs/rest'); +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsTypeError, DiscordjsError, ErrorCodes } = require('../errors'); +const GuildBan = require('../structures/GuildBan'); +const { GuildMember } = require('../structures/GuildMember'); + +let deprecationEmittedForDeleteMessageDays = false; + +/** + * Manages API methods for guild bans and stores their cache. + * @extends {CachedManager} + */ +class GuildBanManager extends CachedManager { + constructor(guild, iterable) { + super(guild.client, GuildBan, iterable); + + /** + * The guild this Manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of this Manager + * @type {Collection<Snowflake, GuildBan>} + * @name GuildBanManager#cache + */ + + _add(data, cache) { + return super._add(data, cache, { id: data.user.id, extras: [this.guild] }); + } + + /** + * Data that resolves to give a GuildBan object. This can be: + * * A GuildBan object + * * A User resolvable + * @typedef {GuildBan|UserResolvable} GuildBanResolvable + */ + + /** + * Resolves a GuildBanResolvable to a GuildBan object. + * @param {GuildBanResolvable} ban The ban that is in the guild + * @returns {?GuildBan} + */ + resolve(ban) { + return super.resolve(ban) ?? super.resolve(this.client.users.resolveId(ban)); + } + + /** + * Options used to fetch a single ban from a guild. + * @typedef {BaseFetchOptions} FetchBanOptions + * @property {UserResolvable} user The ban to fetch + */ + + /** + * Options used to fetch multiple bans from a guild. + * @typedef {Object} FetchBansOptions + * @property {number} [limit] The maximum number of bans to return + * @property {Snowflake} [before] Consider only bans before this id + * @property {Snowflake} [after] Consider only bans after this id + * @property {boolean} [cache] Whether to cache the fetched bans + */ + + /** + * Fetches ban(s) from Discord. + * @param {UserResolvable|FetchBanOptions|FetchBansOptions} [options] Options for fetching guild ban(s) + * @returns {Promise<GuildBan|Collection<Snowflake, GuildBan>>} + * @example + * // Fetch multiple bans from a guild + * guild.bans.fetch() + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch a maximum of 5 bans from a guild without caching + * guild.bans.fetch({ limit: 5, cache: false }) + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch a single ban + * guild.bans.fetch('351871113346809860') + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch a single ban without checking cache + * guild.bans.fetch({ user, force: true }) + * .then(console.log) + * .catch(console.error) + * @example + * // Fetch a single ban without caching + * guild.bans.fetch({ user, cache: false }) + * .then(console.log) + * .catch(console.error); + */ + fetch(options) { + if (!options) return this._fetchMany(); + const { user, cache, force, limit, before, after } = options; + const resolvedUser = this.client.users.resolveId(user ?? options); + if (resolvedUser) return this._fetchSingle({ user: resolvedUser, cache, force }); + + if (!before && !after && !limit && cache === undefined) { + return Promise.reject(new DiscordjsError(ErrorCodes.FetchBanResolveId)); + } + + return this._fetchMany(options); + } + + async _fetchSingle({ user, cache, force = false }) { + if (!force) { + const existing = this.cache.get(user); + if (existing && !existing.partial) return existing; + } + + const data = await this.client.rest.get(Routes.guildBan(this.guild.id, user)); + return this._add(data, cache); + } + + async _fetchMany(options = {}) { + const data = await this.client.rest.get(Routes.guildBans(this.guild.id), { + query: makeURLSearchParams(options), + }); + + return data.reduce((col, ban) => col.set(ban.user.id, this._add(ban, options.cache)), new Collection()); + } + + /** + * Options used to ban a user from a guild. + * @typedef {Object} BanOptions + * @property {number} [deleteMessageDays] Number of days of messages to delete, must be between 0 and 7, inclusive + * <warn>This property is deprecated. Use `deleteMessageSeconds` instead.</warn> + * @property {number} [deleteMessageSeconds] Number of seconds of messages to delete, + * must be between 0 and 604800 (7 days), inclusive + * @property {string} [reason] The reason for the ban + */ + + /** + * Bans a user from the guild. + * @param {UserResolvable} user The user to ban + * @param {BanOptions} [options] Options for the ban + * @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible. + * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot + * be resolved, the user id will be the result. + * @example + * // Ban a user by id (or with a user/guild member object) + * guild.bans.create('84484653687267328') + * .then(banInfo => console.log(`Banned ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`)) + * .catch(console.error); + */ + async create(user, options = {}) { + if (typeof options !== 'object') throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options', 'object', true); + const id = this.client.users.resolveId(user); + if (!id) throw new DiscordjsError(ErrorCodes.BanResolveId, true); + + if (options.deleteMessageDays !== undefined && !deprecationEmittedForDeleteMessageDays) { + process.emitWarning( + // eslint-disable-next-line max-len + 'The deleteMessageDays option for GuildBanManager#create() is deprecated. Use the deleteMessageSeconds option instead.', + 'DeprecationWarning', + ); + + deprecationEmittedForDeleteMessageDays = true; + } + + await this.client.rest.put(Routes.guildBan(this.guild.id, id), { + body: { + delete_message_seconds: + options.deleteMessageSeconds ?? + (options.deleteMessageDays ? options.deleteMessageDays * 24 * 60 * 60 : undefined), + }, + reason: options.reason, + }); + if (user instanceof GuildMember) return user; + const _user = this.client.users.resolve(id); + if (_user) { + return this.guild.members.resolve(_user) ?? _user; + } + return id; + } + + /** + * Unbans a user from the guild. + * @param {UserResolvable} user The user to unban + * @param {string} [reason] Reason for unbanning user + * @returns {Promise<?User>} + * @example + * // Unban a user by id (or with a user/guild member object) + * guild.bans.remove('84484653687267328') + * .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`)) + * .catch(console.error); + */ + async remove(user, reason) { + const id = this.client.users.resolveId(user); + if (!id) throw new DiscordjsError(ErrorCodes.BanResolveId); + await this.client.rest.delete(Routes.guildBan(this.guild.id, id), { reason }); + return this.client.users.resolve(user); + } +} + +module.exports = GuildBanManager; diff --git a/node_modules/discord.js/src/managers/GuildChannelManager.js b/node_modules/discord.js/src/managers/GuildChannelManager.js new file mode 100644 index 0000000..7ca9287 --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildChannelManager.js @@ -0,0 +1,503 @@ +'use strict'; + +const process = require('node:process'); +const { Collection } = require('@discordjs/collection'); +const { ChannelType, Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const GuildTextThreadManager = require('./GuildTextThreadManager'); +const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors'); +const GuildChannel = require('../structures/GuildChannel'); +const PermissionOverwrites = require('../structures/PermissionOverwrites'); +const ThreadChannel = require('../structures/ThreadChannel'); +const Webhook = require('../structures/Webhook'); +const ChannelFlagsBitField = require('../util/ChannelFlagsBitField'); +const { transformGuildForumTag, transformGuildDefaultReaction } = require('../util/Channels'); +const { ThreadChannelTypes } = require('../util/Constants'); +const DataResolver = require('../util/DataResolver'); +const { setPosition } = require('../util/Util'); + +let cacheWarningEmitted = false; + +/** + * Manages API methods for GuildChannels and stores their cache. + * @extends {CachedManager} + */ +class GuildChannelManager extends CachedManager { + constructor(guild, iterable) { + super(guild.client, GuildChannel, iterable); + const defaultCaching = + this._cache.constructor.name === 'Collection' || + this._cache.maxSize === undefined || + this._cache.maxSize === Infinity; + if (!cacheWarningEmitted && !defaultCaching) { + cacheWarningEmitted = true; + process.emitWarning( + `Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`, + 'UnsupportedCacheOverwriteWarning', + ); + } + + /** + * The guild this Manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The number of channels in this managers cache excluding thread channels + * that do not count towards a guild's maximum channels restriction. + * @type {number} + * @readonly + */ + get channelCountWithoutThreads() { + return this.cache.reduce((acc, channel) => { + if (ThreadChannelTypes.includes(channel.type)) return acc; + return ++acc; + }, 0); + } + + /** + * The cache of this Manager + * @type {Collection<Snowflake, GuildChannel|ThreadChannel>} + * @name GuildChannelManager#cache + */ + + _add(channel) { + const existing = this.cache.get(channel.id); + if (existing) return existing; + this.cache.set(channel.id, channel); + return channel; + } + + /** + * Data that can be resolved to give a Guild Channel object. This can be: + * * A GuildChannel object + * * A ThreadChannel object + * * A Snowflake + * @typedef {GuildChannel|ThreadChannel|Snowflake} GuildChannelResolvable + */ + + /** + * Resolves a GuildChannelResolvable to a Channel object. + * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve + * @returns {?(GuildChannel|ThreadChannel)} + */ + resolve(channel) { + if (channel instanceof ThreadChannel) return super.resolve(channel.id); + return super.resolve(channel); + } + + /** + * Resolves a GuildChannelResolvable to a channel id. + * @param {GuildChannelResolvable} channel The GuildChannel resolvable to resolve + * @returns {?Snowflake} + */ + resolveId(channel) { + if (channel instanceof ThreadChannel) return super.resolveId(channel.id); + return super.resolveId(channel); + } + + /** + * Adds the target channel to a channel's followers. + * @param {NewsChannel|Snowflake} channel The channel to follow + * @param {TextChannelResolvable} targetChannel The channel where published announcements will be posted at + * @param {string} [reason] Reason for creating the webhook + * @returns {Promise<Snowflake>} Returns created target webhook id. + */ + async addFollower(channel, targetChannel, reason) { + const channelId = this.resolveId(channel); + const targetChannelId = this.resolveId(targetChannel); + if (!channelId || !targetChannelId) throw new Error(ErrorCodes.GuildChannelResolve); + const { webhook_id } = await this.client.rest.post(Routes.channelFollowers(channelId), { + body: { webhook_channel_id: targetChannelId }, + reason, + }); + return webhook_id; + } + + /** + * Options used to create a new channel in a guild. + * @typedef {CategoryCreateChannelOptions} GuildChannelCreateOptions + * @property {?CategoryChannelResolvable} [parent] Parent of the new channel + */ + + /** + * Creates a new channel in the guild. + * @param {GuildChannelCreateOptions} options Options for creating the new channel + * @returns {Promise<GuildChannel>} + * @example + * // Create a new text channel + * guild.channels.create({ name: 'new-general', reason: 'Needed a cool new channel' }) + * .then(console.log) + * .catch(console.error); + * @example + * // Create a new channel with permission overwrites + * guild.channels.create({ + * name: 'new-general', + * type: ChannelType.GuildVoice, + * permissionOverwrites: [ + * { + * id: message.author.id, + * deny: [PermissionFlagsBits.ViewChannel], + * }, + * ], + * }) + */ + async create({ + name, + type, + topic, + nsfw, + bitrate, + userLimit, + parent, + permissionOverwrites, + position, + rateLimitPerUser, + rtcRegion, + videoQualityMode, + availableTags, + defaultReactionEmoji, + defaultAutoArchiveDuration, + defaultSortOrder, + defaultForumLayout, + reason, + }) { + parent &&= this.client.channels.resolveId(parent); + permissionOverwrites &&= permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild)); + + const data = await this.client.rest.post(Routes.guildChannels(this.guild.id), { + body: { + name, + topic, + type, + nsfw, + bitrate, + user_limit: userLimit, + parent_id: parent, + position, + permission_overwrites: permissionOverwrites, + rate_limit_per_user: rateLimitPerUser, + rtc_region: rtcRegion, + video_quality_mode: videoQualityMode, + available_tags: availableTags?.map(availableTag => transformGuildForumTag(availableTag)), + default_reaction_emoji: defaultReactionEmoji && transformGuildDefaultReaction(defaultReactionEmoji), + default_auto_archive_duration: defaultAutoArchiveDuration, + default_sort_order: defaultSortOrder, + default_forum_layout: defaultForumLayout, + }, + reason, + }); + return this.client.actions.ChannelCreate.handle(data).channel; + } + + /** + * @typedef {ChannelWebhookCreateOptions} WebhookCreateOptions + * @property {TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel|Snowflake} channel + * The channel to create the webhook for + */ + + /** + * Creates a webhook for the channel. + * @param {WebhookCreateOptions} options Options for creating the webhook + * @returns {Promise<Webhook>} Returns the created Webhook + * @example + * // Create a webhook for the current channel + * guild.channels.createWebhook({ + * channel: '222197033908436994', + * name: 'Snek', + * avatar: 'https://i.imgur.com/mI8XcpG.jpg', + * reason: 'Needed a cool new Webhook' + * }) + * .then(console.log) + * .catch(console.error) + */ + async createWebhook({ channel, name, avatar, reason }) { + const id = this.resolveId(channel); + if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'channel', 'GuildChannelResolvable'); + if (typeof avatar === 'string' && !avatar.startsWith('data:')) { + avatar = await DataResolver.resolveImage(avatar); + } + const data = await this.client.rest.post(Routes.channelWebhooks(id), { + body: { + name, + avatar, + }, + reason, + }); + return new Webhook(this.client, data); + } + + /** + * Options used to edit a guild channel. + * @typedef {Object} GuildChannelEditOptions + * @property {string} [name] The name of the channel + * @property {ChannelType} [type] The type of the channel (only conversion between text and news is supported) + * @property {number} [position] The position of the channel + * @property {?string} [topic] The topic of the text channel + * @property {boolean} [nsfw] Whether the channel is NSFW + * @property {number} [bitrate] The bitrate of the voice channel + * @property {number} [userLimit] The user limit of the voice channel + * @property {?CategoryChannelResolvable} [parent] The parent of the channel + * @property {boolean} [lockPermissions] + * Lock the permissions of the channel to what the parent's permissions are + * @property {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} [permissionOverwrites] + * Permission overwrites for the channel + * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the channel in seconds + * @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration] + * The default auto archive duration for all new threads in this channel + * @property {?string} [rtcRegion] The RTC region of the channel + * @property {?VideoQualityMode} [videoQualityMode] The camera video quality mode of the channel + * @property {GuildForumTagData[]} [availableTags] The tags to set as available in a forum channel + * @property {?DefaultReactionEmoji} [defaultReactionEmoji] The emoji to set as the default reaction emoji + * @property {number} [defaultThreadRateLimitPerUser] The rate limit per user (slowmode) to set on forum posts + * @property {ChannelFlagsResolvable} [flags] The flags to set on the channel + * @property {?SortOrderType} [defaultSortOrder] The default sort order mode to set on the channel + * @property {ForumLayoutType} [defaultForumLayout] The default forum layout to set on the channel + * @property {string} [reason] Reason for editing this channel + */ + + /** + * Edits the channel. + * @param {GuildChannelResolvable} channel The channel to edit + * @param {GuildChannelEditOptions} options Options for editing the channel + * @returns {Promise<GuildChannel>} + * @example + * // Edit a channel + * guild.channels.edit('222197033908436994', { name: 'new-channel' }) + * .then(console.log) + * .catch(console.error); + */ + async edit(channel, options) { + channel = this.resolve(channel); + if (!channel) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'channel', 'GuildChannelResolvable'); + + const parent = options.parent && this.client.channels.resolveId(options.parent); + + if (options.position !== undefined) { + await this.setPosition(channel, options.position, { position: options.position, reason: options.reason }); + } + + let permission_overwrites = options.permissionOverwrites?.map(o => PermissionOverwrites.resolve(o, this.guild)); + + if (options.lockPermissions) { + if (parent) { + const newParent = this.guild.channels.resolve(parent); + if (newParent?.type === ChannelType.GuildCategory) { + permission_overwrites = newParent.permissionOverwrites.cache.map(o => + PermissionOverwrites.resolve(o, this.guild), + ); + } + } else if (channel.parent) { + permission_overwrites = channel.parent.permissionOverwrites.cache.map(o => + PermissionOverwrites.resolve(o, this.guild), + ); + } + } + + const newData = await this.client.rest.patch(Routes.channel(channel.id), { + body: { + name: (options.name ?? channel.name).trim(), + type: options.type, + topic: options.topic, + nsfw: options.nsfw, + bitrate: options.bitrate ?? channel.bitrate, + user_limit: options.userLimit ?? channel.userLimit, + rtc_region: 'rtcRegion' in options ? options.rtcRegion : channel.rtcRegion, + video_quality_mode: options.videoQualityMode, + parent_id: parent, + lock_permissions: options.lockPermissions, + rate_limit_per_user: options.rateLimitPerUser, + default_auto_archive_duration: options.defaultAutoArchiveDuration, + permission_overwrites, + available_tags: options.availableTags?.map(availableTag => transformGuildForumTag(availableTag)), + default_reaction_emoji: + options.defaultReactionEmoji && transformGuildDefaultReaction(options.defaultReactionEmoji), + default_thread_rate_limit_per_user: options.defaultThreadRateLimitPerUser, + flags: 'flags' in options ? ChannelFlagsBitField.resolve(options.flags) : undefined, + default_sort_order: options.defaultSortOrder, + default_forum_layout: options.defaultForumLayout, + }, + reason: options.reason, + }); + + return this.client.actions.ChannelUpdate.handle(newData).updated; + } + + /** + * Sets a new position for the guild channel. + * @param {GuildChannelResolvable} channel The channel to set the position for + * @param {number} position The new position for the guild channel + * @param {SetChannelPositionOptions} options Options for setting position + * @returns {Promise<GuildChannel>} + * @example + * // Set a new channel position + * guild.channels.setPosition('222078374472843266', 2) + * .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`)) + * .catch(console.error); + */ + async setPosition(channel, position, { relative, reason } = {}) { + channel = this.resolve(channel); + if (!channel) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'channel', 'GuildChannelResolvable'); + const updatedChannels = await setPosition( + channel, + position, + relative, + this.guild._sortedChannels(channel), + this.client, + Routes.guildChannels(this.guild.id), + reason, + ); + + this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: this.guild.id, + channels: updatedChannels, + }); + return channel; + } + + /** + * Obtains one or more guild channels from Discord, or the channel cache if they're already available. + * @param {Snowflake} [id] The channel's id + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise<?GuildChannel|ThreadChannel|Collection<Snowflake, ?GuildChannel>>} + * @example + * // Fetch all channels from the guild (excluding threads) + * message.guild.channels.fetch() + * .then(channels => console.log(`There are ${channels.size} channels.`)) + * .catch(console.error); + * @example + * // Fetch a single channel + * message.guild.channels.fetch('222197033908436994') + * .then(channel => console.log(`The channel name is: ${channel.name}`)) + * .catch(console.error); + */ + async fetch(id, { cache = true, force = false } = {}) { + if (id && !force) { + const existing = this.cache.get(id); + if (existing) return existing; + } + + if (id) { + const data = await this.client.rest.get(Routes.channel(id)); + // Since this is the guild manager, throw if on a different guild + if (this.guild.id !== data.guild_id) throw new DiscordjsError(ErrorCodes.GuildChannelUnowned); + return this.client.channels._add(data, this.guild, { cache }); + } + + const data = await this.client.rest.get(Routes.guildChannels(this.guild.id)); + const channels = new Collection(); + for (const channel of data) channels.set(channel.id, this.client.channels._add(channel, this.guild, { cache })); + return channels; + } + + /** + * Fetches all webhooks for the channel. + * @param {GuildChannelResolvable} channel The channel to fetch webhooks for + * @returns {Promise<Collection<Snowflake, Webhook>>} + * @example + * // Fetch webhooks + * guild.channels.fetchWebhooks('769862166131245066') + * .then(hooks => console.log(`This channel has ${hooks.size} hooks`)) + * .catch(console.error); + */ + async fetchWebhooks(channel) { + const id = this.resolveId(channel); + if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'channel', 'GuildChannelResolvable'); + const data = await this.client.rest.get(Routes.channelWebhooks(id)); + return data.reduce((hooks, hook) => hooks.set(hook.id, new Webhook(this.client, hook)), new Collection()); + } + + /** + * Data that can be resolved to give a Category Channel object. This can be: + * * A CategoryChannel object + * * A Snowflake + * @typedef {CategoryChannel|Snowflake} CategoryChannelResolvable + */ + + /** + * The data needed for updating a channel's position. + * @typedef {Object} ChannelPosition + * @property {GuildChannel|Snowflake} channel Channel to update + * @property {number} [position] New position for the channel + * @property {CategoryChannelResolvable} [parent] Parent channel for this channel + * @property {boolean} [lockPermissions] If the overwrites should be locked to the parents overwrites + */ + + /** + * Batch-updates the guild's channels' positions. + * <info>Only one channel's parent can be changed at a time</info> + * @param {ChannelPosition[]} channelPositions Channel positions to update + * @returns {Promise<Guild>} + * @example + * guild.channels.setPositions([{ channel: channelId, position: newChannelIndex }]) + * .then(guild => console.log(`Updated channel positions for ${guild}`)) + * .catch(console.error); + */ + async setPositions(channelPositions) { + channelPositions = channelPositions.map(r => ({ + id: this.client.channels.resolveId(r.channel), + position: r.position, + lock_permissions: r.lockPermissions, + parent_id: r.parent !== undefined ? this.resolveId(r.parent) : undefined, + })); + + await this.client.rest.patch(Routes.guildChannels(this.guild.id), { body: channelPositions }); + return this.client.actions.GuildChannelsPositionUpdate.handle({ + guild_id: this.guild.id, + channels: channelPositions, + }).guild; + } + + /** + * Data returned from fetching threads. + * @typedef {Object} FetchedThreads + * @property {Collection<Snowflake, ThreadChannel>} threads The threads that were fetched + * @property {Collection<Snowflake, ThreadMember>} members The thread members in the received threads + */ + + /** + * Obtains all active thread channels in the guild. + * @param {boolean} [cache=true] Whether to cache the fetched data + * @returns {Promise<FetchedThreads>} + * @example + * // Fetch all threads from the guild + * message.guild.channels.fetchActiveThreads() + * .then(fetched => console.log(`There are ${fetched.threads.size} threads.`)) + * .catch(console.error); + */ + async fetchActiveThreads(cache = true) { + const data = await this.rawFetchGuildActiveThreads(); + return GuildTextThreadManager._mapThreads(data, this.client, { guild: this.guild, cache }); + } + + /** + * `GET /guilds/{guild.id}/threads/active` + * @private + * @returns {Promise<RESTGetAPIGuildThreadsResult>} + */ + rawFetchGuildActiveThreads() { + return this.client.rest.get(Routes.guildActiveThreads(this.guild.id)); + } + + /** + * Deletes the channel. + * @param {GuildChannelResolvable} channel The channel to delete + * @param {string} [reason] Reason for deleting this channel + * @returns {Promise<void>} + * @example + * // Delete the channel + * guild.channels.delete('858850993013260338', 'making room for new channels') + * .then(console.log) + * .catch(console.error); + */ + async delete(channel, reason) { + const id = this.resolveId(channel); + if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'channel', 'GuildChannelResolvable'); + await this.client.rest.delete(Routes.channel(id), { reason }); + this.client.actions.ChannelDelete.handle({ id }); + } +} + +module.exports = GuildChannelManager; diff --git a/node_modules/discord.js/src/managers/GuildEmojiManager.js b/node_modules/discord.js/src/managers/GuildEmojiManager.js new file mode 100644 index 0000000..61f5050 --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildEmojiManager.js @@ -0,0 +1,174 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { Routes, PermissionFlagsBits } = require('discord-api-types/v10'); +const BaseGuildEmojiManager = require('./BaseGuildEmojiManager'); +const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors'); +const DataResolver = require('../util/DataResolver'); + +/** + * Manages API methods for GuildEmojis and stores their cache. + * @extends {BaseGuildEmojiManager} + */ +class GuildEmojiManager extends BaseGuildEmojiManager { + constructor(guild, iterable) { + super(guild.client, iterable); + + /** + * The guild this manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + _add(data, cache) { + return super._add(data, cache, { extras: [this.guild] }); + } + + /** + * Options used for creating an emoji in a guild. + * @typedef {Object} GuildEmojiCreateOptions + * @property {BufferResolvable|Base64Resolvable} attachment The image for the emoji + * @property {string} name The name for the emoji + * @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] The roles to limit the emoji to + * @property {string} [reason] The reason for creating the emoji + */ + + /** + * Creates a new custom emoji in the guild. + * @param {GuildEmojiCreateOptions} options Options for creating the emoji + * @returns {Promise<Emoji>} The created emoji + * @example + * // Create a new emoji from a URL + * guild.emojis.create({ attachment: 'https://i.imgur.com/w3duR07.png', name: 'rip' }) + * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) + * .catch(console.error); + * @example + * // Create a new emoji from a file on your computer + * guild.emojis.create({ attachment: './memes/banana.png', name: 'banana' }) + * .then(emoji => console.log(`Created new emoji with name ${emoji.name}!`)) + * .catch(console.error); + */ + async create({ attachment, name, roles, reason }) { + attachment = await DataResolver.resolveImage(attachment); + if (!attachment) throw new DiscordjsTypeError(ErrorCodes.ReqResourceType); + + const body = { image: attachment, name }; + if (roles) { + if (!Array.isArray(roles) && !(roles instanceof Collection)) { + throw new DiscordjsTypeError( + ErrorCodes.InvalidType, + 'options.roles', + 'Array or Collection of Roles or Snowflakes', + true, + ); + } + body.roles = []; + for (const role of roles.values()) { + const resolvedRole = this.guild.roles.resolveId(role); + if (!resolvedRole) { + throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array or Collection', 'options.roles', role); + } + body.roles.push(resolvedRole); + } + } + + const emoji = await this.client.rest.post(Routes.guildEmojis(this.guild.id), { body, reason }); + return this.client.actions.GuildEmojiCreate.handle(this.guild, emoji).emoji; + } + + /** + * Obtains one or more emojis from Discord, or the emoji cache if they're already available. + * @param {Snowflake} [id] The emoji's id + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise<GuildEmoji|Collection<Snowflake, GuildEmoji>>} + * @example + * // Fetch all emojis from the guild + * message.guild.emojis.fetch() + * .then(emojis => console.log(`There are ${emojis.size} emojis.`)) + * .catch(console.error); + * @example + * // Fetch a single emoji + * message.guild.emojis.fetch('222078108977594368') + * .then(emoji => console.log(`The emoji name is: ${emoji.name}`)) + * .catch(console.error); + */ + async fetch(id, { cache = true, force = false } = {}) { + if (id) { + if (!force) { + const existing = this.cache.get(id); + if (existing) return existing; + } + const emoji = await this.client.rest.get(Routes.guildEmoji(this.guild.id, id)); + return this._add(emoji, cache); + } + + const data = await this.client.rest.get(Routes.guildEmojis(this.guild.id)); + const emojis = new Collection(); + for (const emoji of data) emojis.set(emoji.id, this._add(emoji, cache)); + return emojis; + } + + /** + * Deletes an emoji. + * @param {EmojiResolvable} emoji The Emoji resolvable to delete + * @param {string} [reason] Reason for deleting the emoji + * @returns {Promise<void>} + */ + async delete(emoji, reason) { + const id = this.resolveId(emoji); + if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'emoji', 'EmojiResolvable', true); + await this.client.rest.delete(Routes.guildEmoji(this.guild.id, id), { reason }); + } + + /** + * Edits an emoji. + * @param {EmojiResolvable} emoji The Emoji resolvable to edit + * @param {GuildEmojiEditOptions} options The options to provide + * @returns {Promise<GuildEmoji>} + */ + async edit(emoji, options) { + const id = this.resolveId(emoji); + if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'emoji', 'EmojiResolvable', true); + const roles = options.roles?.map(r => this.guild.roles.resolveId(r)); + const newData = await this.client.rest.patch(Routes.guildEmoji(this.guild.id, id), { + body: { + name: options.name, + roles, + }, + reason: options.reason, + }); + const existing = this.cache.get(id); + if (existing) { + const clone = existing._clone(); + clone._patch(newData); + return clone; + } + return this._add(newData); + } + + /** + * Fetches the author for this emoji + * @param {EmojiResolvable} emoji The emoji to fetch the author of + * @returns {Promise<User>} + */ + async fetchAuthor(emoji) { + emoji = this.resolve(emoji); + if (!emoji) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'emoji', 'EmojiResolvable', true); + if (emoji.managed) { + throw new DiscordjsError(ErrorCodes.EmojiManaged); + } + + const { me } = this.guild.members; + if (!me) throw new DiscordjsError(ErrorCodes.GuildUncachedMe); + if (!me.permissions.has(PermissionFlagsBits.ManageGuildExpressions)) { + throw new DiscordjsError(ErrorCodes.MissingManageGuildExpressionsPermission, this.guild); + } + + const data = await this.client.rest.get(Routes.guildEmoji(this.guild.id, emoji.id)); + emoji._patch(data); + return emoji.author; + } +} + +module.exports = GuildEmojiManager; diff --git a/node_modules/discord.js/src/managers/GuildEmojiRoleManager.js b/node_modules/discord.js/src/managers/GuildEmojiRoleManager.js new file mode 100644 index 0000000..7b97f41 --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildEmojiRoleManager.js @@ -0,0 +1,118 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const DataManager = require('./DataManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const { Role } = require('../structures/Role'); + +/** + * Manages API methods for roles belonging to emojis and stores their cache. + * @extends {DataManager} + */ +class GuildEmojiRoleManager extends DataManager { + constructor(emoji) { + super(emoji.client, Role); + + /** + * The emoji belonging to this manager + * @type {GuildEmoji} + */ + this.emoji = emoji; + /** + * The guild belonging to this manager + * @type {Guild} + */ + this.guild = emoji.guild; + } + + /** + * The cache of roles belonging to this emoji + * @type {Collection<Snowflake, Role>} + * @readonly + */ + get cache() { + return this.guild.roles.cache.filter(role => this.emoji._roles.includes(role.id)); + } + + /** + * Adds a role (or multiple roles) to the list of roles that can use this emoji. + * @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to add + * @returns {Promise<GuildEmoji>} + */ + add(roleOrRoles) { + if (!Array.isArray(roleOrRoles) && !(roleOrRoles instanceof Collection)) roleOrRoles = [roleOrRoles]; + + const resolvedRoles = []; + for (const role of roleOrRoles.values()) { + const resolvedRole = this.guild.roles.resolveId(role); + if (!resolvedRole) { + return Promise.reject(new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array or Collection', 'roles', role)); + } + resolvedRoles.push(resolvedRole); + } + + const newRoles = [...new Set(resolvedRoles.concat(...this.cache.keys()))]; + return this.set(newRoles); + } + + /** + * Removes a role (or multiple roles) from the list of roles that can use this emoji. + * @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to remove + * @returns {Promise<GuildEmoji>} + */ + remove(roleOrRoles) { + if (!Array.isArray(roleOrRoles) && !(roleOrRoles instanceof Collection)) roleOrRoles = [roleOrRoles]; + + const resolvedRoleIds = []; + for (const role of roleOrRoles.values()) { + const roleId = this.guild.roles.resolveId(role); + if (!roleId) { + return Promise.reject(new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array or Collection', 'roles', role)); + } + resolvedRoleIds.push(roleId); + } + + const newRoles = [...this.cache.keys()].filter(id => !resolvedRoleIds.includes(id)); + return this.set(newRoles); + } + + /** + * Sets the role(s) that can use this emoji. + * @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role ids to apply + * @returns {Promise<GuildEmoji>} + * @example + * // Set the emoji's roles to a single role + * guildEmoji.roles.set(['391156570408615936']) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove all roles from an emoji + * guildEmoji.roles.set([]) + * .then(console.log) + * .catch(console.error); + */ + set(roles) { + return this.emoji.edit({ roles }); + } + + clone() { + const clone = new this.constructor(this.emoji); + clone._patch([...this.cache.keys()]); + return clone; + } + + /** + * Patches the roles for this manager's cache + * @param {Snowflake[]} roles The new roles + * @private + */ + _patch(roles) { + this.emoji._roles = roles; + } + + valueOf() { + return this.cache; + } +} + +module.exports = GuildEmojiRoleManager; diff --git a/node_modules/discord.js/src/managers/GuildForumThreadManager.js b/node_modules/discord.js/src/managers/GuildForumThreadManager.js new file mode 100644 index 0000000..f830b98 --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildForumThreadManager.js @@ -0,0 +1,83 @@ +'use strict'; + +const { Routes } = require('discord-api-types/v10'); +const ThreadManager = require('./ThreadManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const MessagePayload = require('../structures/MessagePayload'); + +/** + * Manages API methods for threads in forum channels and stores their cache. + * @extends {ThreadManager} + */ +class GuildForumThreadManager extends ThreadManager { + /** + * The channel this Manager belongs to + * @name GuildForumThreadManager#channel + * @type {ForumChannel} + */ + + /** + * @typedef {BaseMessageOptions} GuildForumThreadMessageCreateOptions + * @property {StickerResolvable} [stickers] The stickers to send with the message + * @property {BitFieldResolvable} [flags] The flags to send with the message + * <info>Only `MessageFlags.SuppressEmbeds` and `MessageFlags.SuppressNotifications` can be set.</info> + */ + + /** + * Options for creating a thread. + * @typedef {StartThreadOptions} GuildForumThreadCreateOptions + * @property {GuildForumThreadMessageCreateOptions|MessagePayload} message The message associated with the thread post + * @property {Snowflake[]} [appliedTags] The tags to apply to the thread + */ + + /** + * Creates a new thread in the channel. + * @param {GuildForumThreadCreateOptions} [options] Options to create a new thread + * @returns {Promise<ThreadChannel>} + * @example + * // Create a new forum post + * forum.threads + * .create({ + * name: 'Food Talk', + * autoArchiveDuration: ThreadAutoArchiveDuration.OneHour, + * message: { + * content: 'Discuss your favorite food!', + * }, + * reason: 'Needed a separate thread for food', + * }) + * .then(threadChannel => console.log(threadChannel)) + * .catch(console.error); + */ + async create({ + name, + autoArchiveDuration = this.channel.defaultAutoArchiveDuration, + message, + reason, + rateLimitPerUser, + appliedTags, + } = {}) { + if (!message) { + throw new DiscordjsTypeError(ErrorCodes.GuildForumMessageRequired); + } + + const { body, files } = await (message instanceof MessagePayload ? message : MessagePayload.create(this, message)) + .resolveBody() + .resolveFiles(); + + const data = await this.client.rest.post(Routes.threads(this.channel.id), { + body: { + name, + auto_archive_duration: autoArchiveDuration, + rate_limit_per_user: rateLimitPerUser, + applied_tags: appliedTags, + message: body, + }, + files, + reason, + }); + + return this.client.actions.ThreadCreate.handle(data).thread; + } +} + +module.exports = GuildForumThreadManager; diff --git a/node_modules/discord.js/src/managers/GuildInviteManager.js b/node_modules/discord.js/src/managers/GuildInviteManager.js new file mode 100644 index 0000000..f1fe3eb --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildInviteManager.js @@ -0,0 +1,214 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsError, ErrorCodes } = require('../errors'); +const Invite = require('../structures/Invite'); +const DataResolver = require('../util/DataResolver'); + +/** + * Manages API methods for GuildInvites and stores their cache. + * @extends {CachedManager} + */ +class GuildInviteManager extends CachedManager { + constructor(guild, iterable) { + super(guild.client, Invite, iterable); + + /** + * The guild this Manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of this Manager + * @type {Collection<string, Invite>} + * @name GuildInviteManager#cache + */ + + _add(data, cache) { + return super._add(data, cache, { id: data.code, extras: [this.guild] }); + } + + /** + * Data that resolves to give an Invite object. This can be: + * * An invite code + * * An invite URL + * @typedef {string} InviteResolvable + */ + + /** + * Data that can be resolved to a channel that an invite can be created on. This can be: + * * TextChannel + * * VoiceChannel + * * NewsChannel + * * StageChannel + * * ForumChannel + * * Snowflake + * @typedef {TextChannel|VoiceChannel|NewsChannel|StageChannel|ForumChannel|Snowflake} + * GuildInvitableChannelResolvable + */ + + /** + * Resolves an InviteResolvable to an Invite object. + * @method resolve + * @memberof GuildInviteManager + * @instance + * @param {InviteResolvable} invite The invite resolvable to resolve + * @returns {?Invite} + */ + + /** + * Resolves an InviteResolvable to an invite code string. + * @method resolveId + * @memberof GuildInviteManager + * @instance + * @param {InviteResolvable} invite The invite resolvable to resolve + * @returns {?string} + */ + + /** + * Options used to fetch a single invite from a guild. + * @typedef {Object} FetchInviteOptions + * @property {InviteResolvable} code The invite to fetch + * @property {boolean} [cache=true] Whether or not to cache the fetched invite + * @property {boolean} [force=false] Whether to skip the cache check and request the API + */ + + /** + * Options used to fetch all invites from a guild. + * @typedef {Object} FetchInvitesOptions + * @property {GuildInvitableChannelResolvable} [channelId] + * The channel to fetch all invites from + * @property {boolean} [cache=true] Whether or not to cache the fetched invites + */ + + /** + * Fetches invite(s) from Discord. + * @param {InviteResolvable|FetchInviteOptions|FetchInvitesOptions} [options] Options for fetching guild invite(s) + * @returns {Promise<Invite|Collection<string, Invite>>} + * @example + * // Fetch all invites from a guild + * guild.invites.fetch() + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch all invites from a guild without caching + * guild.invites.fetch({ cache: false }) + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch all invites from a channel + * guild.invites.fetch({ channelId: '222197033908436994' }) + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch a single invite + * guild.invites.fetch('bRCvFy9') + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch a single invite without checking cache + * guild.invites.fetch({ code: 'bRCvFy9', force: true }) + * .then(console.log) + * .catch(console.error) + * @example + * // Fetch a single invite without caching + * guild.invites.fetch({ code: 'bRCvFy9', cache: false }) + * .then(console.log) + * .catch(console.error); + */ + fetch(options) { + if (!options) return this._fetchMany(); + if (typeof options === 'string') { + const code = DataResolver.resolveInviteCode(options); + if (!code) return Promise.reject(new DiscordjsError(ErrorCodes.InviteResolveCode)); + return this._fetchSingle({ code, cache: true }); + } + if (!options.code) { + if (options.channelId) { + const id = this.guild.channels.resolveId(options.channelId); + if (!id) return Promise.reject(new DiscordjsError(ErrorCodes.GuildChannelResolve)); + return this._fetchChannelMany(id, options.cache); + } + + if ('cache' in options) return this._fetchMany(options.cache); + return Promise.reject(new DiscordjsError(ErrorCodes.InviteResolveCode)); + } + return this._fetchSingle({ + ...options, + code: DataResolver.resolveInviteCode(options.code), + }); + } + + async _fetchSingle({ code, cache, force = false }) { + if (!force) { + const existing = this.cache.get(code); + if (existing) return existing; + } + + const invites = await this._fetchMany(cache); + const invite = invites.get(code); + if (!invite) throw new DiscordjsError(ErrorCodes.InviteNotFound); + return invite; + } + + async _fetchMany(cache) { + const data = await this.client.rest.get(Routes.guildInvites(this.guild.id)); + return data.reduce((col, invite) => col.set(invite.code, this._add(invite, cache)), new Collection()); + } + + async _fetchChannelMany(channelId, cache) { + const data = await this.client.rest.get(Routes.channelInvites(channelId)); + return data.reduce((col, invite) => col.set(invite.code, this._add(invite, cache)), new Collection()); + } + + /** + * Create an invite to the guild from the provided channel. + * @param {GuildInvitableChannelResolvable} channel The options for creating the invite from a channel. + * @param {InviteCreateOptions} [options={}] The options for creating the invite from a channel. + * @returns {Promise<Invite>} + * @example + * // Create an invite to a selected channel + * guild.invites.create('599942732013764608') + * .then(console.log) + * .catch(console.error); + */ + async create( + channel, + { temporary, maxAge, maxUses, unique, targetUser, targetApplication, targetType, reason } = {}, + ) { + const id = this.guild.channels.resolveId(channel); + if (!id) throw new DiscordjsError(ErrorCodes.GuildChannelResolve); + + const invite = await this.client.rest.post(Routes.channelInvites(id), { + body: { + temporary, + max_age: maxAge, + max_uses: maxUses, + unique, + target_user_id: this.client.users.resolveId(targetUser), + target_application_id: targetApplication?.id ?? targetApplication?.applicationId ?? targetApplication, + target_type: targetType, + }, + reason, + }); + return new Invite(this.client, invite); + } + + /** + * Deletes an invite. + * @param {InviteResolvable} invite The invite to delete + * @param {string} [reason] Reason for deleting the invite + * @returns {Promise<void>} + */ + async delete(invite, reason) { + const code = DataResolver.resolveInviteCode(invite); + + await this.client.rest.delete(Routes.invite(code), { reason }); + } +} + +module.exports = GuildInviteManager; diff --git a/node_modules/discord.js/src/managers/GuildManager.js b/node_modules/discord.js/src/managers/GuildManager.js new file mode 100644 index 0000000..1d2d4ba --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildManager.js @@ -0,0 +1,283 @@ +'use strict'; + +const process = require('node:process'); +const { setTimeout, clearTimeout } = require('node:timers'); +const { Collection } = require('@discordjs/collection'); +const { makeURLSearchParams } = require('@discordjs/rest'); +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { Guild } = require('../structures/Guild'); +const GuildChannel = require('../structures/GuildChannel'); +const GuildEmoji = require('../structures/GuildEmoji'); +const { GuildMember } = require('../structures/GuildMember'); +const Invite = require('../structures/Invite'); +const OAuth2Guild = require('../structures/OAuth2Guild'); +const { Role } = require('../structures/Role'); +const DataResolver = require('../util/DataResolver'); +const Events = require('../util/Events'); +const PermissionsBitField = require('../util/PermissionsBitField'); +const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField'); +const { resolveColor } = require('../util/Util'); + +let cacheWarningEmitted = false; + +/** + * Manages API methods for Guilds and stores their cache. + * @extends {CachedManager} + */ +class GuildManager extends CachedManager { + constructor(client, iterable) { + super(client, Guild, iterable); + if (!cacheWarningEmitted && this._cache.constructor.name !== 'Collection') { + cacheWarningEmitted = true; + process.emitWarning( + `Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`, + 'UnsupportedCacheOverwriteWarning', + ); + } + } + + /** + * The cache of this Manager + * @type {Collection<Snowflake, Guild>} + * @name GuildManager#cache + */ + + /** + * Data that resolves to give a Guild object. This can be: + * * A Guild object + * * A GuildChannel object + * * A GuildEmoji object + * * A Role object + * * A Snowflake + * * An Invite object + * @typedef {Guild|GuildChannel|GuildMember|GuildEmoji|Role|Snowflake|Invite} GuildResolvable + */ + + /** + * Partial data for a Role. + * @typedef {Object} PartialRoleData + * @property {Snowflake|number} [id] The role's id, used to set channel overrides. + * This is a placeholder and will be replaced by the API after consumption + * @property {string} [name] The name of the role + * @property {ColorResolvable} [color] The color of the role, either a hex string or a base 10 number + * @property {boolean} [hoist] Whether the role should be hoisted + * @property {number} [position] The position of the role + * @property {PermissionResolvable} [permissions] The permissions of the role + * @property {boolean} [mentionable] Whether the role should be mentionable + */ + + /** + * Partial overwrite data. + * @typedef {Object} PartialOverwriteData + * @property {Snowflake|number} id The id of the {@link Role} or {@link User} this overwrite belongs to + * @property {OverwriteType} [type] The type of this overwrite + * @property {PermissionResolvable} [allow] The permissions to allow + * @property {PermissionResolvable} [deny] The permissions to deny + */ + + /** + * Partial data for a Channel. + * @typedef {Object} PartialChannelData + * @property {Snowflake|number} [id] The channel's id, used to set its parent. + * This is a placeholder and will be replaced by the API after consumption + * @property {Snowflake|number} [parentId] The parent id for this channel + * @property {ChannelType.GuildText|ChannelType.GuildVoice|ChannelType.GuildCategory} [type] The type of the channel + * @property {string} name The name of the channel + * @property {?string} [topic] The topic of the text channel + * @property {boolean} [nsfw] Whether the channel is NSFW + * @property {number} [bitrate] The bitrate of the voice channel + * @property {number} [userLimit] The user limit of the channel + * @property {?string} [rtcRegion] The RTC region of the channel + * @property {VideoQualityMode} [videoQualityMode] The camera video quality mode of the channel + * @property {PartialOverwriteData[]} [permissionOverwrites] + * Overwrites of the channel + * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) of the channel in seconds + */ + + /** + * Resolves a GuildResolvable to a Guild object. + * @method resolve + * @memberof GuildManager + * @instance + * @param {GuildResolvable} guild The guild resolvable to identify + * @returns {?Guild} + */ + resolve(guild) { + if ( + guild instanceof GuildChannel || + guild instanceof GuildMember || + guild instanceof GuildEmoji || + guild instanceof Role || + (guild instanceof Invite && guild.guild) + ) { + return super.resolve(guild.guild); + } + return super.resolve(guild); + } + + /** + * Resolves a {@link GuildResolvable} to a {@link Guild} id string. + * @method resolveId + * @memberof GuildManager + * @instance + * @param {GuildResolvable} guild The guild resolvable to identify + * @returns {?Snowflake} + */ + resolveId(guild) { + if ( + guild instanceof GuildChannel || + guild instanceof GuildMember || + guild instanceof GuildEmoji || + guild instanceof Role || + (guild instanceof Invite && guild.guild) + ) { + return super.resolveId(guild.guild.id); + } + return super.resolveId(guild); + } + + /** + * Options used to create a guild. + * @typedef {Object} GuildCreateOptions + * @property {string} name The name of the guild + * @property {?(BufferResolvable|Base64Resolvable)} [icon=null] The icon for the guild + * @property {GuildVerificationLevel} [verificationLevel] The verification level for the guild + * @property {GuildDefaultMessageNotifications} [defaultMessageNotifications] The default message notifications + * for the guild + * @property {GuildExplicitContentFilter} [explicitContentFilter] The explicit content filter level for the guild + * @property {PartialRoleData[]} [roles=[]] The roles for this guild, + * @property {PartialChannelData[]} [channels=[]] The channels for this guild + * @property {Snowflake|number} [afkChannelId] The AFK channel's id + * @property {number} [afkTimeout] The AFK timeout in seconds + * the first element of this array is used to change properties of the guild's everyone role. + * @property {Snowflake|number} [systemChannelId] The system channel's id + * @property {SystemChannelFlagsResolvable} [systemChannelFlags] The flags of the system channel + */ + /* eslint-enable max-len */ + + /** + * Creates a guild. + * <warn>This is only available to bots in fewer than 10 guilds.</warn> + * @param {GuildCreateOptions} options Options for creating the guild + * @returns {Promise<Guild>} The guild that was created + */ + async create({ + name, + icon = null, + verificationLevel, + defaultMessageNotifications, + explicitContentFilter, + roles = [], + channels = [], + afkChannelId, + afkTimeout, + systemChannelId, + systemChannelFlags, + }) { + const data = await this.client.rest.post(Routes.guilds(), { + body: { + name, + icon: icon && (await DataResolver.resolveImage(icon)), + verification_level: verificationLevel, + default_message_notifications: defaultMessageNotifications, + explicit_content_filter: explicitContentFilter, + roles: roles.map(({ color, permissions, ...options }) => ({ + ...options, + color: color && resolveColor(color), + permissions: permissions === undefined ? undefined : PermissionsBitField.resolve(permissions).toString(), + })), + channels: channels.map( + ({ + parentId, + userLimit, + rtcRegion, + videoQualityMode, + permissionOverwrites, + rateLimitPerUser, + ...options + }) => ({ + ...options, + parent_id: parentId, + user_limit: userLimit, + rtc_region: rtcRegion, + video_quality_mode: videoQualityMode, + permission_overwrites: permissionOverwrites?.map(({ allow, deny, ...permissionOverwriteOptions }) => ({ + ...permissionOverwriteOptions, + allow: allow === undefined ? undefined : PermissionsBitField.resolve(allow).toString(), + deny: deny === undefined ? undefined : PermissionsBitField.resolve(deny).toString(), + })), + rate_limit_per_user: rateLimitPerUser, + }), + ), + afk_channel_id: afkChannelId, + afk_timeout: afkTimeout, + system_channel_id: systemChannelId, + system_channel_flags: + systemChannelFlags === undefined ? undefined : SystemChannelFlagsBitField.resolve(systemChannelFlags), + }, + }); + + return ( + this.client.guilds.cache.get(data.id) ?? + new Promise(resolve => { + const handleGuild = guild => { + if (guild.id === data.id) { + clearTimeout(timeout); + this.client.decrementMaxListeners(); + resolve(guild); + } + }; + this.client.incrementMaxListeners(); + this.client.once(Events.GuildCreate, handleGuild); + + const timeout = setTimeout(() => { + this.client.removeListener(Events.GuildCreate, handleGuild); + this.client.decrementMaxListeners(); + resolve(this.client.guilds._add(data)); + }, 10_000).unref(); + }) + ); + } + + /** + * Options used to fetch a single guild. + * @typedef {BaseFetchOptions} FetchGuildOptions + * @property {GuildResolvable} guild The guild to fetch + * @property {boolean} [withCounts=true] Whether the approximate member and presence counts should be returned + */ + + /** + * Options used to fetch multiple guilds. + * @typedef {Object} FetchGuildsOptions + * @property {Snowflake} [before] Get guilds before this guild id + * @property {Snowflake} [after] Get guilds after this guild id + * @property {number} [limit] Maximum number of guilds to request (1-200) + */ + + /** + * Obtains one or multiple guilds from Discord, or the guild cache if it's already available. + * @param {GuildResolvable|FetchGuildOptions|FetchGuildsOptions} [options] The guild's id or options + * @returns {Promise<Guild|Collection<Snowflake, OAuth2Guild>>} + */ + async fetch(options = {}) { + const id = this.resolveId(options) ?? this.resolveId(options.guild); + + if (id) { + if (!options.force) { + const existing = this.cache.get(id); + if (existing) return existing; + } + + const data = await this.client.rest.get(Routes.guild(id), { + query: makeURLSearchParams({ with_counts: options.withCounts ?? true }), + }); + return this._add(data, options.cache); + } + + const data = await this.client.rest.get(Routes.userGuilds(), { query: makeURLSearchParams(options) }); + return data.reduce((coll, guild) => coll.set(guild.id, new OAuth2Guild(this.client, guild)), new Collection()); + } +} + +module.exports = GuildManager; diff --git a/node_modules/discord.js/src/managers/GuildMemberManager.js b/node_modules/discord.js/src/managers/GuildMemberManager.js new file mode 100644 index 0000000..a4cf09b --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildMemberManager.js @@ -0,0 +1,540 @@ +'use strict'; + +const { setTimeout, clearTimeout } = require('node:timers'); +const { Collection } = require('@discordjs/collection'); +const { makeURLSearchParams } = require('@discordjs/rest'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { Routes, GatewayOpcodes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsError, DiscordjsTypeError, DiscordjsRangeError, ErrorCodes } = require('../errors'); +const BaseGuildVoiceChannel = require('../structures/BaseGuildVoiceChannel'); +const { GuildMember } = require('../structures/GuildMember'); +const { Role } = require('../structures/Role'); +const Events = require('../util/Events'); +const { GuildMemberFlagsBitField } = require('../util/GuildMemberFlagsBitField'); +const Partials = require('../util/Partials'); + +/** + * Manages API methods for GuildMembers and stores their cache. + * @extends {CachedManager} + */ +class GuildMemberManager extends CachedManager { + constructor(guild, iterable) { + super(guild.client, GuildMember, iterable); + + /** + * The guild this manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of this Manager + * @type {Collection<Snowflake, GuildMember>} + * @name GuildMemberManager#cache + */ + + _add(data, cache = true) { + return super._add(data, cache, { id: data.user.id, extras: [this.guild] }); + } + + /** + * Data that resolves to give a GuildMember object. This can be: + * * A GuildMember object + * * A User resolvable + * @typedef {GuildMember|UserResolvable} GuildMemberResolvable + */ + + /** + * Resolves a {@link GuildMemberResolvable} to a {@link GuildMember} object. + * @param {GuildMemberResolvable} member The user that is part of the guild + * @returns {?GuildMember} + */ + resolve(member) { + const memberResolvable = super.resolve(member); + if (memberResolvable) return memberResolvable; + const userResolvable = this.client.users.resolveId(member); + if (userResolvable) return super.resolve(userResolvable); + return null; + } + + /** + * Resolves a {@link GuildMemberResolvable} to a member id. + * @param {GuildMemberResolvable} member The user that is part of the guild + * @returns {?Snowflake} + */ + resolveId(member) { + const memberResolvable = super.resolveId(member); + if (memberResolvable) return memberResolvable; + const userResolvable = this.client.users.resolveId(member); + return this.cache.has(userResolvable) ? userResolvable : null; + } + + /** + * Options used to add a user to a guild using OAuth2. + * @typedef {Object} AddGuildMemberOptions + * @property {string} accessToken An OAuth2 access token for the user with the {@link OAuth2Scopes.GuildsJoin} + * scope granted to the bot's application + * @property {string} [nick] The nickname to give to the member + * <info>This property requires the {@link PermissionFlagsBits.ManageNicknames} permission.</info> + * @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] The roles to add to the member + * <info>This property requires the {@link PermissionFlagsBits.ManageRoles} permission.</info> + * @property {boolean} [mute] Whether the member should be muted + * <info>This property requires the {@link PermissionFlagsBits.MuteMembers} permission.</info> + * @property {boolean} [deaf] Whether the member should be deafened + * <info>This property requires the {@link PermissionFlagsBits.MuteMembers} permission.</info> + * @property {boolean} [force] Whether to skip the cache check and request the API directly + * @property {boolean} [fetchWhenExisting=true] Whether to fetch the user if not cached and already a member + */ + + /** + * Adds a user to the guild using OAuth2. + * <info>This method requires the {@link PermissionFlagsBits.CreateInstantInvite} permission. + * @param {UserResolvable} user The user to add to the guild + * @param {AddGuildMemberOptions} options Options for adding the user to the guild + * @returns {Promise<GuildMember|null>} + */ + async add(user, options) { + const userId = this.client.users.resolveId(user); + if (!userId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'user', 'UserResolvable'); + if (!options.force) { + const cachedUser = this.cache.get(userId); + if (cachedUser) return cachedUser; + } + const resolvedOptions = { + access_token: options.accessToken, + nick: options.nick, + mute: options.mute, + deaf: options.deaf, + }; + if (options.roles) { + if (!Array.isArray(options.roles) && !(options.roles instanceof Collection)) { + throw new DiscordjsTypeError( + ErrorCodes.InvalidType, + 'options.roles', + 'Array or Collection of Roles or Snowflakes', + true, + ); + } + const resolvedRoles = []; + for (const role of options.roles.values()) { + const resolvedRole = this.guild.roles.resolveId(role); + if (!resolvedRole) { + throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array or Collection', 'options.roles', role); + } + resolvedRoles.push(resolvedRole); + } + resolvedOptions.roles = resolvedRoles; + } + const data = await this.client.rest.put(Routes.guildMember(this.guild.id, userId), { body: resolvedOptions }); + // Data is an empty Uint8Array if the member is already part of the guild. + return data instanceof Uint8Array + ? options.fetchWhenExisting === false + ? null + : this.fetch(userId) + : this._add(data); + } + + /** + * The client user as a GuildMember of this guild + * @type {?GuildMember} + * @readonly + */ + get me() { + return ( + this.resolve(this.client.user.id) ?? + (this.client.options.partials.includes(Partials.GuildMember) + ? this._add({ user: { id: this.client.user.id } }, true) + : null) + ); + } + + /** + * Options used to fetch a single member from a guild. + * @typedef {BaseFetchOptions} FetchMemberOptions + * @property {UserResolvable} user The user to fetch + */ + + /** + * Options used to fetch multiple members from a guild. + * @typedef {Object} FetchMembersOptions + * @property {UserResolvable|UserResolvable[]} [user] The user(s) to fetch + * @property {?string} [query] Limit fetch to members with similar usernames + * @property {number} [limit=0] Maximum number of members to request + * @property {boolean} [withPresences=false] Whether to include the presences + * @property {number} [time=120e3] Timeout for receipt of members + * @property {?string} [nonce] Nonce for this request (32 characters max - default to base 16 now timestamp) + */ + + /** + * Fetches member(s) from a guild. + * @param {UserResolvable|FetchMemberOptions|FetchMembersOptions} [options] Options for fetching member(s). + * Omitting the parameter or providing `undefined` will fetch all members. + * @returns {Promise<GuildMember|Collection<Snowflake, GuildMember>>} + * @example + * // Fetch all members from a guild + * guild.members.fetch() + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch a single member + * guild.members.fetch('66564597481480192') + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch a single member without checking cache + * guild.members.fetch({ user, force: true }) + * .then(console.log) + * .catch(console.error) + * @example + * // Fetch a single member without caching + * guild.members.fetch({ user, cache: false }) + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch by an array of users including their presences + * guild.members.fetch({ user: ['66564597481480192', '191615925336670208'], withPresences: true }) + * .then(console.log) + * .catch(console.error); + * @example + * // Fetch by query + * guild.members.fetch({ query: 'hydra', limit: 1 }) + * .then(console.log) + * .catch(console.error); + */ + fetch(options) { + if (!options) return this._fetchMany(); + const { user: users, limit, withPresences, cache, force } = options; + const resolvedUser = this.client.users.resolveId(users ?? options); + if (resolvedUser && !limit && !withPresences) return this._fetchSingle({ user: resolvedUser, cache, force }); + const resolvedUsers = users?.map?.(user => this.client.users.resolveId(user)) ?? resolvedUser ?? undefined; + return this._fetchMany({ ...options, users: resolvedUsers }); + } + + async _fetchSingle({ user, cache, force = false }) { + if (!force) { + const existing = this.cache.get(user); + if (existing && !existing.partial) return existing; + } + + const data = await this.client.rest.get(Routes.guildMember(this.guild.id, user)); + return this._add(data, cache); + } + + _fetchMany({ + limit = 0, + withPresences: presences, + users, + query, + time = 120e3, + nonce = DiscordSnowflake.generate().toString(), + } = {}) { + if (nonce.length > 32) return Promise.reject(new DiscordjsRangeError(ErrorCodes.MemberFetchNonceLength)); + + return new Promise((resolve, reject) => { + if (!query && !users) query = ''; + this.guild.shard.send({ + op: GatewayOpcodes.RequestGuildMembers, + d: { + guild_id: this.guild.id, + presences, + user_ids: users, + query, + nonce, + limit, + }, + }); + const fetchedMembers = new Collection(); + let i = 0; + const handler = (members, _, chunk) => { + if (chunk.nonce !== nonce) return; + timeout.refresh(); + i++; + for (const member of members.values()) { + fetchedMembers.set(member.id, member); + } + if (members.size < 1_000 || (limit && fetchedMembers.size >= limit) || i === chunk.count) { + clearTimeout(timeout); + this.client.removeListener(Events.GuildMembersChunk, handler); + this.client.decrementMaxListeners(); + resolve(users && !Array.isArray(users) && fetchedMembers.size ? fetchedMembers.first() : fetchedMembers); + } + }; + const timeout = setTimeout(() => { + this.client.removeListener(Events.GuildMembersChunk, handler); + this.client.decrementMaxListeners(); + reject(new DiscordjsError(ErrorCodes.GuildMembersTimeout)); + }, time).unref(); + this.client.incrementMaxListeners(); + this.client.on(Events.GuildMembersChunk, handler); + }); + } + + /** + * Fetches the client user as a GuildMember of the guild. + * @param {BaseFetchOptions} [options] The options for fetching the member + * @returns {Promise<GuildMember>} + */ + fetchMe(options) { + return this.fetch({ ...options, user: this.client.user.id }); + } + + /** + * Options used for searching guild members. + * @typedef {Object} GuildSearchMembersOptions + * @property {string} query Filter members whose username or nickname start with this query + * @property {number} [limit] Maximum number of members to search + * @property {boolean} [cache=true] Whether or not to cache the fetched member(s) + */ + + /** + * Searches for members in the guild based on a query. + * @param {GuildSearchMembersOptions} options Options for searching members + * @returns {Promise<Collection<Snowflake, GuildMember>>} + */ + async search({ query, limit, cache = true } = {}) { + const data = await this.client.rest.get(Routes.guildMembersSearch(this.guild.id), { + query: makeURLSearchParams({ query, limit }), + }); + return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection()); + } + + /** + * Options used for listing guild members. + * @typedef {Object} GuildListMembersOptions + * @property {Snowflake} [after] Limit fetching members to those with an id greater than the supplied id + * @property {number} [limit] Maximum number of members to list + * @property {boolean} [cache=true] Whether or not to cache the fetched member(s) + */ + + /** + * Lists up to 1000 members of the guild. + * @param {GuildListMembersOptions} [options] Options for listing members + * @returns {Promise<Collection<Snowflake, GuildMember>>} + */ + async list({ after, limit, cache = true } = {}) { + const query = makeURLSearchParams({ limit, after }); + const data = await this.client.rest.get(Routes.guildMembers(this.guild.id), { query }); + return data.reduce((col, member) => col.set(member.user.id, this._add(member, cache)), new Collection()); + } + + /** + * The data for editing a guild member. + * @typedef {Object} GuildMemberEditOptions + * @property {?string} [nick] The nickname to set for the member + * @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] The roles or role ids to apply + * @property {boolean} [mute] Whether or not the member should be muted + * @property {boolean} [deaf] Whether or not the member should be deafened + * @property {GuildVoiceChannelResolvable|null} [channel] Channel to move the member to + * (if they are connected to voice), or `null` if you want to disconnect them from voice + * @property {DateResolvable|null} [communicationDisabledUntil] The date or timestamp + * for the member's communication to be disabled until. Provide `null` to enable communication again. + * @property {GuildMemberFlagsResolvable} [flags] The flags to set for the member + * @property {string} [reason] Reason for editing this user + */ + + /** + * Edits a member of the guild. + * <info>The user must be a member of the guild</info> + * @param {UserResolvable} user The member to edit + * @param {GuildMemberEditOptions} options The options to provide + * @returns {Promise<GuildMember>} + */ + async edit(user, { reason, ...options }) { + const id = this.client.users.resolveId(user); + if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'user', 'UserResolvable'); + + if (options.channel) { + options.channel = this.guild.channels.resolve(options.channel); + if (!(options.channel instanceof BaseGuildVoiceChannel)) { + throw new DiscordjsError(ErrorCodes.GuildVoiceChannelResolve); + } + options.channel_id = options.channel.id; + options.channel = undefined; + } else if (options.channel === null) { + options.channel_id = null; + options.channel = undefined; + } + options.roles &&= options.roles.map(role => (role instanceof Role ? role.id : role)); + + if (options.communicationDisabledUntil !== undefined) { + options.communication_disabled_until = + // eslint-disable-next-line eqeqeq + options.communicationDisabledUntil != null + ? new Date(options.communicationDisabledUntil).toISOString() + : options.communicationDisabledUntil; + } + + if (options.flags !== undefined) { + options.flags = GuildMemberFlagsBitField.resolve(options.flags); + } + + let endpoint; + if (id === this.client.user.id) { + const keys = Object.keys(options); + if (keys.length === 1 && keys[0] === 'nick') endpoint = Routes.guildMember(this.guild.id); + else endpoint = Routes.guildMember(this.guild.id, id); + } else { + endpoint = Routes.guildMember(this.guild.id, id); + } + const d = await this.client.rest.patch(endpoint, { body: options, reason }); + + const clone = this.cache.get(id)?._clone(); + clone?._patch(d); + return clone ?? this._add(d, false); + } + + /** + * Options used for pruning guild members. + * <info>It's recommended to set {@link GuildPruneMembersOptions#count options.count} + * to `false` for large guilds.</info> + * @typedef {Object} GuildPruneMembersOptions + * @property {number} [days] Number of days of inactivity required to kick + * @property {boolean} [dry=false] Get the number of users that will be kicked, without actually kicking them + * @property {boolean} [count] Whether or not to return the number of users that have been kicked. + * @property {RoleResolvable[]} [roles] Array of roles to bypass the "...and no roles" constraint when pruning + * @property {string} [reason] Reason for this prune + */ + + /** + * Prunes members from the guild based on how long they have been inactive. + * @param {GuildPruneMembersOptions} [options] Options for pruning + * @returns {Promise<number|null>} The number of members that were/will be kicked + * @example + * // See how many members will be pruned + * guild.members.prune({ dry: true }) + * .then(pruned => console.log(`This will prune ${pruned} people!`)) + * .catch(console.error); + * @example + * // Actually prune the members + * guild.members.prune({ days: 1, reason: 'too many people!' }) + * .then(pruned => console.log(`I just pruned ${pruned} people!`)) + * .catch(console.error); + * @example + * // Include members with a specified role + * guild.members.prune({ days: 7, roles: ['657259391652855808'] }) + * .then(pruned => console.log(`I just pruned ${pruned} people!`)) + * .catch(console.error); + */ + async prune({ days, dry = false, count: compute_prune_count, roles = [], reason } = {}) { + if (typeof days !== 'number') throw new DiscordjsTypeError(ErrorCodes.PruneDaysType); + + const query = { days }; + const resolvedRoles = []; + + for (const role of roles) { + const resolvedRole = this.guild.roles.resolveId(role); + if (!resolvedRole) { + throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array', 'options.roles', role); + } + resolvedRoles.push(resolvedRole); + } + + if (resolvedRoles.length) { + query.include_roles = dry ? resolvedRoles.join(',') : resolvedRoles; + } + + const endpoint = Routes.guildPrune(this.guild.id); + + const { pruned } = await (dry + ? this.client.rest.get(endpoint, { query: makeURLSearchParams(query), reason }) + : this.client.rest.post(endpoint, { body: { ...query, compute_prune_count }, reason })); + + return pruned; + } + + /** + * Kicks a user from the guild. + * <info>The user must be a member of the guild</info> + * @param {UserResolvable} user The member to kick + * @param {string} [reason] Reason for kicking + * @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible. + * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot + * be resolved, the user's id will be the result. + * @example + * // Kick a user by id (or with a user/guild member object) + * guild.members.kick('84484653687267328') + * .then(kickInfo => console.log(`Kicked ${kickInfo.user?.tag ?? kickInfo.tag ?? kickInfo}`)) + * .catch(console.error); + */ + async kick(user, reason) { + const id = this.client.users.resolveId(user); + if (!id) return Promise.reject(new DiscordjsTypeError(ErrorCodes.InvalidType, 'user', 'UserResolvable')); + + await this.client.rest.delete(Routes.guildMember(this.guild.id, id), { reason }); + + return this.resolve(user) ?? this.client.users.resolve(user) ?? id; + } + + /** + * Bans a user from the guild. + * @param {UserResolvable} user The user to ban + * @param {BanOptions} [options] Options for the ban + * @returns {Promise<GuildMember|User|Snowflake>} Result object will be resolved as specifically as possible. + * If the GuildMember cannot be resolved, the User will instead be attempted to be resolved. If that also cannot + * be resolved, the user id will be the result. + * Internally calls the GuildBanManager#create method. + * @example + * // Ban a user by id (or with a user/guild member object) + * guild.members.ban('84484653687267328') + * .then(banInfo => console.log(`Banned ${banInfo.user?.tag ?? banInfo.tag ?? banInfo}`)) + * .catch(console.error); + */ + ban(user, options) { + return this.guild.bans.create(user, options); + } + + /** + * Unbans a user from the guild. Internally calls the {@link GuildBanManager#remove} method. + * @param {UserResolvable} user The user to unban + * @param {string} [reason] Reason for unbanning user + * @returns {Promise<?User>} The user that was unbanned + * @example + * // Unban a user by id (or with a user/guild member object) + * guild.members.unban('84484653687267328') + * .then(user => console.log(`Unbanned ${user.username} from ${guild.name}`)) + * .catch(console.error); + */ + unban(user, reason) { + return this.guild.bans.remove(user, reason); + } + + /** + * Options used for adding or removing a role from a member. + * @typedef {Object} AddOrRemoveGuildMemberRoleOptions + * @property {GuildMemberResolvable} user The user to add/remove the role from + * @property {RoleResolvable} role The role to add/remove + * @property {string} [reason] Reason for adding/removing the role + */ + + /** + * Adds a role to a member. + * @param {AddOrRemoveGuildMemberRoleOptions} options Options for adding the role + * @returns {Promise<GuildMember|User|Snowflake>} + */ + async addRole(options) { + const { user, role, reason } = options; + const userId = this.guild.members.resolveId(user); + const roleId = this.guild.roles.resolveId(role); + await this.client.rest.put(Routes.guildMemberRole(this.guild.id, userId, roleId), { reason }); + + return this.resolve(user) ?? this.client.users.resolve(user) ?? userId; + } + + /** + * Removes a role from a member. + * @param {AddOrRemoveGuildMemberRoleOptions} options Options for removing the role + * @returns {Promise<GuildMember|User|Snowflake>} + */ + async removeRole(options) { + const { user, role, reason } = options; + const userId = this.guild.members.resolveId(user); + const roleId = this.guild.roles.resolveId(role); + await this.client.rest.delete(Routes.guildMemberRole(this.guild.id, userId, roleId), { reason }); + + return this.resolve(user) ?? this.client.users.resolve(user) ?? userId; + } +} + +module.exports = GuildMemberManager; diff --git a/node_modules/discord.js/src/managers/GuildMemberRoleManager.js b/node_modules/discord.js/src/managers/GuildMemberRoleManager.js new file mode 100644 index 0000000..e530268 --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildMemberRoleManager.js @@ -0,0 +1,204 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { Routes } = require('discord-api-types/v10'); +const DataManager = require('./DataManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const { Role } = require('../structures/Role'); + +/** + * Manages API methods for roles of a GuildMember and stores their cache. + * @extends {DataManager} + */ +class GuildMemberRoleManager extends DataManager { + constructor(member) { + super(member.client, Role); + + /** + * The GuildMember this manager belongs to + * @type {GuildMember} + */ + this.member = member; + + /** + * The Guild this manager belongs to + * @type {Guild} + */ + this.guild = member.guild; + } + + /** + * The roles of this member + * @type {Collection<Snowflake, Role>} + * @readonly + */ + get cache() { + const everyone = this.guild.roles.everyone; + return this.guild.roles.cache.filter(role => this.member._roles.includes(role.id)).set(everyone.id, everyone); + } + + /** + * The role of the member used to hoist them in a separate category in the users list + * @type {?Role} + * @readonly + */ + get hoist() { + const hoistedRoles = this.cache.filter(role => role.hoist); + if (!hoistedRoles.size) return null; + return hoistedRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev)); + } + + /** + * The role of the member used to set their role icon + * @type {?Role} + * @readonly + */ + get icon() { + const iconRoles = this.cache.filter(role => role.icon || role.unicodeEmoji); + if (!iconRoles.size) return null; + return iconRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev)); + } + + /** + * The role of the member used to set their color + * @type {?Role} + * @readonly + */ + get color() { + const coloredRoles = this.cache.filter(role => role.color); + if (!coloredRoles.size) return null; + return coloredRoles.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev)); + } + + /** + * The role of the member with the highest position + * @type {Role} + * @readonly + */ + get highest() { + return this.cache.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev), this.cache.first()); + } + + /** + * The premium subscriber role of the guild, if present on the member + * @type {?Role} + * @readonly + */ + get premiumSubscriberRole() { + return this.cache.find(role => role.tags?.premiumSubscriberRole) ?? null; + } + + /** + * The managed role this member created when joining the guild, if any + * <info>Only ever available on bots</info> + * @type {?Role} + * @readonly + */ + get botRole() { + if (!this.member.user.bot) return null; + return this.cache.find(role => role.tags?.botId === this.member.user.id) ?? null; + } + + /** + * Adds a role (or multiple roles) to the member. + * @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to add + * @param {string} [reason] Reason for adding the role(s) + * @returns {Promise<GuildMember>} + */ + async add(roleOrRoles, reason) { + if (roleOrRoles instanceof Collection || Array.isArray(roleOrRoles)) { + const resolvedRoles = []; + for (const role of roleOrRoles.values()) { + const resolvedRole = this.guild.roles.resolveId(role); + if (!resolvedRole) { + throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array or Collection', 'roles', role); + } + resolvedRoles.push(resolvedRole); + } + + const newRoles = [...new Set(resolvedRoles.concat(...this.cache.keys()))]; + return this.set(newRoles, reason); + } else { + roleOrRoles = this.guild.roles.resolveId(roleOrRoles); + if (roleOrRoles === null) { + throw new DiscordjsTypeError( + ErrorCodes.InvalidType, + 'roles', + 'Role, Snowflake or Array or Collection of Roles or Snowflakes', + ); + } + + await this.client.rest.put(Routes.guildMemberRole(this.guild.id, this.member.id, roleOrRoles), { reason }); + + const clone = this.member._clone(); + clone._roles = [...this.cache.keys(), roleOrRoles]; + return clone; + } + } + + /** + * Removes a role (or multiple roles) from the member. + * @param {RoleResolvable|RoleResolvable[]|Collection<Snowflake, Role>} roleOrRoles The role or roles to remove + * @param {string} [reason] Reason for removing the role(s) + * @returns {Promise<GuildMember>} + */ + async remove(roleOrRoles, reason) { + if (roleOrRoles instanceof Collection || Array.isArray(roleOrRoles)) { + const resolvedRoles = []; + for (const role of roleOrRoles.values()) { + const resolvedRole = this.guild.roles.resolveId(role); + if (!resolvedRole) { + throw new DiscordjsTypeError(ErrorCodes.InvalidElement, 'Array or Collection', 'roles', role); + } + resolvedRoles.push(resolvedRole); + } + + const newRoles = this.cache.filter(role => !resolvedRoles.includes(role.id)); + return this.set(newRoles, reason); + } else { + roleOrRoles = this.guild.roles.resolveId(roleOrRoles); + if (roleOrRoles === null) { + throw new DiscordjsTypeError( + ErrorCodes.InvalidType, + 'roles', + 'Role, Snowflake or Array or Collection of Roles or Snowflakes', + ); + } + + await this.client.rest.delete(Routes.guildMemberRole(this.guild.id, this.member.id, roleOrRoles), { reason }); + + const clone = this.member._clone(); + const newRoles = this.cache.filter(role => role.id !== roleOrRoles); + clone._roles = [...newRoles.keys()]; + return clone; + } + } + + /** + * Sets the roles applied to the member. + * @param {Collection<Snowflake, Role>|RoleResolvable[]} roles The roles or role ids to apply + * @param {string} [reason] Reason for applying the roles + * @returns {Promise<GuildMember>} + * @example + * // Set the member's roles to a single role + * guildMember.roles.set(['391156570408615936']) + * .then(console.log) + * .catch(console.error); + * @example + * // Remove all the roles from a member + * guildMember.roles.set([]) + * .then(member => console.log(`Member roles is now of ${member.roles.cache.size} size`)) + * .catch(console.error); + */ + set(roles, reason) { + return this.member.edit({ roles, reason }); + } + + clone() { + const clone = new this.constructor(this.member); + clone.member._roles = [...this.cache.keys()]; + return clone; + } +} + +module.exports = GuildMemberRoleManager; diff --git a/node_modules/discord.js/src/managers/GuildMessageManager.js b/node_modules/discord.js/src/managers/GuildMessageManager.js new file mode 100644 index 0000000..7a93c99 --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildMessageManager.js @@ -0,0 +1,17 @@ +'use strict'; + +const MessageManager = require('./MessageManager'); + +/** + * Manages API methods for messages in a guild and holds their cache. + * @extends {MessageManager} + */ +class GuildMessageManager extends MessageManager { + /** + * The channel that the messages belong to + * @name GuildMessageManager#channel + * @type {GuildTextBasedChannel} + */ +} + +module.exports = GuildMessageManager; diff --git a/node_modules/discord.js/src/managers/GuildScheduledEventManager.js b/node_modules/discord.js/src/managers/GuildScheduledEventManager.js new file mode 100644 index 0000000..9071b60 --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildScheduledEventManager.js @@ -0,0 +1,297 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { makeURLSearchParams } = require('@discordjs/rest'); +const { GuildScheduledEventEntityType, Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsTypeError, DiscordjsError, ErrorCodes } = require('../errors'); +const { GuildScheduledEvent } = require('../structures/GuildScheduledEvent'); +const DataResolver = require('../util/DataResolver'); + +/** + * Manages API methods for GuildScheduledEvents and stores their cache. + * @extends {CachedManager} + */ +class GuildScheduledEventManager extends CachedManager { + constructor(guild, iterable) { + super(guild.client, GuildScheduledEvent, iterable); + + /** + * The guild this manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of this manager + * @type {Collection<Snowflake, GuildScheduledEvent>} + * @name GuildScheduledEventManager#cache + */ + + /** + * Data that resolves to give a GuildScheduledEvent object. This can be: + * * A Snowflake + * * A GuildScheduledEvent object + * @typedef {Snowflake|GuildScheduledEvent} GuildScheduledEventResolvable + */ + + /** + * Options used to create a guild scheduled event. + * @typedef {Object} GuildScheduledEventCreateOptions + * @property {string} name The name of the guild scheduled event + * @property {DateResolvable} scheduledStartTime The time to schedule the event at + * @property {DateResolvable} [scheduledEndTime] The time to end the event at + * <warn>This is required if `entityType` is {@link GuildScheduledEventEntityType.External}</warn> + * @property {GuildScheduledEventPrivacyLevel} privacyLevel The privacy level of the guild scheduled event + * @property {GuildScheduledEventEntityType} entityType The scheduled entity type of the event + * @property {string} [description] The description of the guild scheduled event + * @property {GuildVoiceChannelResolvable} [channel] The channel of the guild scheduled event + * <warn>This is required if `entityType` is {@link GuildScheduledEventEntityType.StageInstance} or + * {@link GuildScheduledEventEntityType.Voice}</warn> + * @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the + * guild scheduled event + * <warn>This is required if `entityType` is {@link GuildScheduledEventEntityType.External}</warn> + * @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event + * @property {string} [reason] The reason for creating the guild scheduled event + */ + + /** + * Options used to set entity metadata of a guild scheduled event. + * @typedef {Object} GuildScheduledEventEntityMetadataOptions + * @property {string} [location] The location of the guild scheduled event + * <warn>This is required if `entityType` is {@link GuildScheduledEventEntityType.External}</warn> + */ + + /** + * Creates a new guild scheduled event. + * @param {GuildScheduledEventCreateOptions} options Options for creating the guild scheduled event + * @returns {Promise<GuildScheduledEvent>} + */ + async create(options) { + if (typeof options !== 'object') throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options', 'object', true); + let { + privacyLevel, + entityType, + channel, + name, + scheduledStartTime, + description, + scheduledEndTime, + entityMetadata, + reason, + image, + } = options; + + let entity_metadata, channel_id; + if (entityType === GuildScheduledEventEntityType.External) { + channel_id = channel === undefined ? channel : null; + entity_metadata = { location: entityMetadata?.location }; + } else { + channel_id = this.guild.channels.resolveId(channel); + if (!channel_id) throw new DiscordjsError(ErrorCodes.GuildVoiceChannelResolve); + entity_metadata = entityMetadata === undefined ? entityMetadata : null; + } + + const data = await this.client.rest.post(Routes.guildScheduledEvents(this.guild.id), { + body: { + channel_id, + name, + privacy_level: privacyLevel, + scheduled_start_time: new Date(scheduledStartTime).toISOString(), + scheduled_end_time: scheduledEndTime ? new Date(scheduledEndTime).toISOString() : scheduledEndTime, + description, + entity_type: entityType, + entity_metadata, + image: image && (await DataResolver.resolveImage(image)), + }, + reason, + }); + + return this._add(data); + } + + /** + * Options used to fetch a single guild scheduled event from a guild. + * @typedef {BaseFetchOptions} FetchGuildScheduledEventOptions + * @property {GuildScheduledEventResolvable} guildScheduledEvent The guild scheduled event to fetch + * @property {boolean} [withUserCount=true] Whether to fetch the number of users subscribed to the scheduled event + */ + + /** + * Options used to fetch multiple guild scheduled events from a guild. + * @typedef {Object} FetchGuildScheduledEventsOptions + * @property {boolean} [cache] Whether or not to cache the fetched guild scheduled events + * @property {boolean} [withUserCount=true] Whether to fetch the number of users subscribed to each scheduled event + * should be returned + */ + + /** + * Obtains one or more guild scheduled events from Discord, or the guild cache if it's already available. + * @param {GuildScheduledEventResolvable|FetchGuildScheduledEventOptions|FetchGuildScheduledEventsOptions} [options] + * The id of the guild scheduled event or options + * @returns {Promise<GuildScheduledEvent|Collection<Snowflake, GuildScheduledEvent>>} + */ + async fetch(options = {}) { + const id = this.resolveId(options.guildScheduledEvent ?? options); + + if (id) { + if (!options.force) { + const existing = this.cache.get(id); + if (existing) return existing; + } + + const data = await this.client.rest.get(Routes.guildScheduledEvent(this.guild.id, id), { + query: makeURLSearchParams({ with_user_count: options.withUserCount ?? true }), + }); + return this._add(data, options.cache); + } + + const data = await this.client.rest.get(Routes.guildScheduledEvents(this.guild.id), { + query: makeURLSearchParams({ with_user_count: options.withUserCount ?? true }), + }); + + return data.reduce( + (coll, rawGuildScheduledEventData) => + coll.set( + rawGuildScheduledEventData.id, + this.guild.scheduledEvents._add(rawGuildScheduledEventData, options.cache), + ), + new Collection(), + ); + } + + /** + * Options used to edit a guild scheduled event. + * @typedef {Object} GuildScheduledEventEditOptions + * @property {string} [name] The name of the guild scheduled event + * @property {DateResolvable} [scheduledStartTime] The time to schedule the event at + * @property {DateResolvable} [scheduledEndTime] The time to end the event at + * @property {GuildScheduledEventPrivacyLevel} [privacyLevel] The privacy level of the guild scheduled event + * @property {GuildScheduledEventEntityType} [entityType] The scheduled entity type of the event + * @property {string} [description] The description of the guild scheduled event + * @property {?GuildVoiceChannelResolvable} [channel] The channel of the guild scheduled event + * @property {GuildScheduledEventStatus} [status] The status of the guild scheduled event + * @property {GuildScheduledEventEntityMetadataOptions} [entityMetadata] The entity metadata of the + * guild scheduled event + * <warn>This can be modified only if `entityType` of the `GuildScheduledEvent` to be edited is + * {@link GuildScheduledEventEntityType.External}</warn> + * @property {?(BufferResolvable|Base64Resolvable)} [image] The cover image of the guild scheduled event + * @property {string} [reason] The reason for editing the guild scheduled event + */ + + /** + * Edits a guild scheduled event. + * @param {GuildScheduledEventResolvable} guildScheduledEvent The guild scheduled event to edit + * @param {GuildScheduledEventEditOptions} options Options to edit the guild scheduled event + * @returns {Promise<GuildScheduledEvent>} + */ + async edit(guildScheduledEvent, options) { + const guildScheduledEventId = this.resolveId(guildScheduledEvent); + if (!guildScheduledEventId) throw new DiscordjsError(ErrorCodes.GuildScheduledEventResolve); + + if (typeof options !== 'object') throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options', 'object', true); + let { + privacyLevel, + entityType, + channel, + status, + name, + scheduledStartTime, + description, + scheduledEndTime, + entityMetadata, + reason, + image, + } = options; + + let entity_metadata; + if (entityMetadata) { + entity_metadata = { + location: entityMetadata.location, + }; + } + + const data = await this.client.rest.patch(Routes.guildScheduledEvent(this.guild.id, guildScheduledEventId), { + body: { + channel_id: channel === undefined ? channel : this.guild.channels.resolveId(channel), + name, + privacy_level: privacyLevel, + scheduled_start_time: scheduledStartTime ? new Date(scheduledStartTime).toISOString() : undefined, + scheduled_end_time: scheduledEndTime ? new Date(scheduledEndTime).toISOString() : scheduledEndTime, + description, + entity_type: entityType, + status, + image: image && (await DataResolver.resolveImage(image)), + entity_metadata, + }, + reason, + }); + + return this._add(data); + } + + /** + * Deletes a guild scheduled event. + * @param {GuildScheduledEventResolvable} guildScheduledEvent The guild scheduled event to delete + * @returns {Promise<void>} + */ + async delete(guildScheduledEvent) { + const guildScheduledEventId = this.resolveId(guildScheduledEvent); + if (!guildScheduledEventId) throw new DiscordjsError(ErrorCodes.GuildScheduledEventResolve); + + await this.client.rest.delete(Routes.guildScheduledEvent(this.guild.id, guildScheduledEventId)); + } + + /** + * Options used to fetch subscribers of a guild scheduled event + * @typedef {Object} FetchGuildScheduledEventSubscribersOptions + * @property {number} [limit] The maximum numbers of users to fetch + * @property {boolean} [withMember] Whether to fetch guild member data of the users + * @property {Snowflake} [before] Consider only users before this user id + * @property {Snowflake} [after] Consider only users after this user id + * <warn>If both `before` and `after` are provided, only `before` is respected</warn> + */ + + /** + * Represents a subscriber of a {@link GuildScheduledEvent} + * @typedef {Object} GuildScheduledEventUser + * @property {Snowflake} guildScheduledEventId The id of the guild scheduled event which the user subscribed to + * @property {User} user The user that subscribed to the guild scheduled event + * @property {?GuildMember} member The guild member associated with the user, if any + */ + + /** + * Fetches subscribers of a guild scheduled event. + * @param {GuildScheduledEventResolvable} guildScheduledEvent The guild scheduled event to fetch subscribers of + * @param {FetchGuildScheduledEventSubscribersOptions} [options={}] Options for fetching the subscribers + * @returns {Promise<Collection<Snowflake, GuildScheduledEventUser>>} + */ + async fetchSubscribers(guildScheduledEvent, options = {}) { + const guildScheduledEventId = this.resolveId(guildScheduledEvent); + if (!guildScheduledEventId) throw new DiscordjsError(ErrorCodes.GuildScheduledEventResolve); + + const query = makeURLSearchParams({ + limit: options.limit, + with_member: options.withMember, + before: options.before, + after: options.after, + }); + + const data = await this.client.rest.get(Routes.guildScheduledEventUsers(this.guild.id, guildScheduledEventId), { + query, + }); + + return data.reduce( + (coll, rawData) => + coll.set(rawData.user.id, { + guildScheduledEventId: rawData.guild_scheduled_event_id, + user: this.client.users._add(rawData.user), + member: rawData.member ? this.guild.members._add({ ...rawData.member, user: rawData.user }) : null, + }), + new Collection(), + ); + } +} + +module.exports = GuildScheduledEventManager; diff --git a/node_modules/discord.js/src/managers/GuildStickerManager.js b/node_modules/discord.js/src/managers/GuildStickerManager.js new file mode 100644 index 0000000..a4974ec --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildStickerManager.js @@ -0,0 +1,182 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const MessagePayload = require('../structures/MessagePayload'); +const { Sticker } = require('../structures/Sticker'); + +/** + * Manages API methods for Guild Stickers and stores their cache. + * @extends {CachedManager} + */ +class GuildStickerManager extends CachedManager { + constructor(guild, iterable) { + super(guild.client, Sticker, iterable); + + /** + * The guild this manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of Guild Stickers + * @type {Collection<Snowflake, Sticker>} + * @name GuildStickerManager#cache + */ + + _add(data, cache) { + return super._add(data, cache, { extras: [this.guild] }); + } + + /** + * Options used to create a guild sticker. + * @typedef {Object} GuildStickerCreateOptions + * @property {AttachmentPayload|BufferResolvable|Stream} file The file for the sticker + * @property {string} name The name for the sticker + * @property {string} tags The Discord name of a unicode emoji representing the sticker's expression + * @property {?string} [description] The description for the sticker + * @property {string} [reason] Reason for creating the sticker + */ + + /** + * Creates a new custom sticker in the guild. + * @param {GuildStickerCreateOptions} options Options for creating a guild sticker + * @returns {Promise<Sticker>} The created sticker + * @example + * // Create a new sticker from a URL + * guild.stickers.create({ file: 'https://i.imgur.com/w3duR07.png', name: 'rip', tags: 'headstone' }) + * .then(sticker => console.log(`Created new sticker with name ${sticker.name}!`)) + * .catch(console.error); + * @example + * // Create a new sticker from a file on your computer + * guild.stickers.create({ file: './memes/banana.png', name: 'banana', tags: 'banana' }) + * .then(sticker => console.log(`Created new sticker with name ${sticker.name}!`)) + * .catch(console.error); + */ + async create({ file, name, tags, description, reason } = {}) { + const resolvedFile = await MessagePayload.resolveFile(file); + if (!resolvedFile) throw new DiscordjsTypeError(ErrorCodes.ReqResourceType); + file = { ...resolvedFile, key: 'file' }; + + const body = { name, tags, description: description ?? '' }; + + const sticker = await this.client.rest.post(Routes.guildStickers(this.guild.id), { + appendToFormData: true, + body, + files: [file], + reason, + }); + return this.client.actions.GuildStickerCreate.handle(this.guild, sticker).sticker; + } + + /** + * Data that resolves to give a Sticker object. This can be: + * * A Sticker object + * * A Snowflake + * @typedef {Sticker|Snowflake} StickerResolvable + */ + + /** + * Resolves a StickerResolvable to a Sticker object. + * @method resolve + * @memberof GuildStickerManager + * @instance + * @param {StickerResolvable} sticker The Sticker resolvable to identify + * @returns {?Sticker} + */ + + /** + * Resolves a StickerResolvable to a Sticker id string. + * @method resolveId + * @memberof GuildStickerManager + * @instance + * @param {StickerResolvable} sticker The Sticker resolvable to identify + * @returns {?Snowflake} + */ + + /** + * Edits a sticker. + * @param {StickerResolvable} sticker The sticker to edit + * @param {GuildStickerEditOptions} [options={}] The new data for the sticker + * @returns {Promise<Sticker>} + */ + async edit(sticker, options = {}) { + const stickerId = this.resolveId(sticker); + if (!stickerId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'sticker', 'StickerResolvable'); + + const d = await this.client.rest.patch(Routes.guildSticker(this.guild.id, stickerId), { + body: options, + reason: options.reason, + }); + + const existing = this.cache.get(stickerId); + if (existing) { + const clone = existing._clone(); + clone._patch(d); + return clone; + } + return this._add(d); + } + + /** + * Deletes a sticker. + * @param {StickerResolvable} sticker The sticker to delete + * @param {string} [reason] Reason for deleting this sticker + * @returns {Promise<void>} + */ + async delete(sticker, reason) { + sticker = this.resolveId(sticker); + if (!sticker) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'sticker', 'StickerResolvable'); + + await this.client.rest.delete(Routes.guildSticker(this.guild.id, sticker), { reason }); + } + + /** + * Obtains one or more stickers from Discord, or the sticker cache if they're already available. + * @param {Snowflake} [id] The Sticker's id + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise<Sticker|Collection<Snowflake, Sticker>>} + * @example + * // Fetch all stickers from the guild + * message.guild.stickers.fetch() + * .then(stickers => console.log(`There are ${stickers.size} stickers.`)) + * .catch(console.error); + * @example + * // Fetch a single sticker + * message.guild.stickers.fetch('222078108977594368') + * .then(sticker => console.log(`The sticker name is: ${sticker.name}`)) + * .catch(console.error); + */ + async fetch(id, { cache = true, force = false } = {}) { + if (id) { + if (!force) { + const existing = this.cache.get(id); + if (existing) return existing; + } + const sticker = await this.client.rest.get(Routes.guildSticker(this.guild.id, id)); + return this._add(sticker, cache); + } + + const data = await this.client.rest.get(Routes.guildStickers(this.guild.id)); + return new Collection(data.map(sticker => [sticker.id, this._add(sticker, cache)])); + } + + /** + * Fetches the user who uploaded this sticker, if this is a guild sticker. + * @param {StickerResolvable} sticker The sticker to fetch the user for + * @returns {Promise<?User>} + */ + async fetchUser(sticker) { + sticker = this.resolve(sticker); + if (!sticker) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'sticker', 'StickerResolvable'); + const data = await this.client.rest.get(Routes.guildSticker(this.guild.id, sticker.id)); + sticker._patch(data); + return sticker.user; + } +} + +module.exports = GuildStickerManager; diff --git a/node_modules/discord.js/src/managers/GuildTextThreadManager.js b/node_modules/discord.js/src/managers/GuildTextThreadManager.js new file mode 100644 index 0000000..5591845 --- /dev/null +++ b/node_modules/discord.js/src/managers/GuildTextThreadManager.js @@ -0,0 +1,91 @@ +'use strict'; + +const { ChannelType, Routes } = require('discord-api-types/v10'); +const ThreadManager = require('./ThreadManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); + +/** + * Manages API methods for {@link ThreadChannel} objects and stores their cache. + * @extends {ThreadManager} + */ +class GuildTextThreadManager extends ThreadManager { + /** + * The channel this Manager belongs to + * @name GuildTextThreadManager#channel + * @type {TextChannel|NewsChannel} + */ + + /** + * Options for creating a thread. <warn>Only one of `startMessage` or `type` can be defined.</warn> + * @typedef {StartThreadOptions} ThreadCreateOptions + * @property {MessageResolvable} [startMessage] The message to start a thread from. + * <warn>If this is defined, then the `type` of thread gets inferred automatically and cannot be changed.</warn> + * @property {ThreadChannelTypes} [type] The type of thread to create. + * Defaults to {@link ChannelType.PublicThread} if created in a {@link TextChannel} + * <warn>When creating threads in a {@link NewsChannel}, this is ignored and is always + * {@link ChannelType.AnnouncementThread}</warn> + * @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread + * <info>Can only be set when type will be {@link ChannelType.PrivateThread}</info> + */ + + /** + * Creates a new thread in the channel. + * @param {ThreadCreateOptions} [options] Options to create a new thread + * @returns {Promise<ThreadChannel>} + * @example + * // Create a new public thread + * channel.threads + * .create({ + * name: 'food-talk', + * autoArchiveDuration: ThreadAutoArchiveDuration.OneHour, + * reason: 'Needed a separate thread for food', + * }) + * .then(threadChannel => console.log(threadChannel)) + * .catch(console.error); + * @example + * // Create a new private thread + * channel.threads + * .create({ + * name: 'mod-talk', + * autoArchiveDuration: ThreadAutoArchiveDuration.OneHour, + * type: ChannelType.PrivateThread, + * reason: 'Needed a separate thread for moderation', + * }) + * .then(threadChannel => console.log(threadChannel)) + * .catch(console.error); + */ + async create({ + name, + autoArchiveDuration = this.channel.defaultAutoArchiveDuration, + startMessage, + type, + invitable, + reason, + rateLimitPerUser, + } = {}) { + let resolvedType = + this.channel.type === ChannelType.GuildAnnouncement ? ChannelType.AnnouncementThread : ChannelType.PublicThread; + let startMessageId; + if (startMessage) { + startMessageId = this.channel.messages.resolveId(startMessage); + if (!startMessageId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'startMessage', 'MessageResolvable'); + } else if (this.channel.type !== ChannelType.GuildAnnouncement) { + resolvedType = type ?? resolvedType; + } + + const data = await this.client.rest.post(Routes.threads(this.channel.id, startMessageId), { + body: { + name, + auto_archive_duration: autoArchiveDuration, + type: resolvedType, + invitable: resolvedType === ChannelType.PrivateThread ? invitable : undefined, + rate_limit_per_user: rateLimitPerUser, + }, + reason, + }); + + return this.client.actions.ThreadCreate.handle(data).thread; + } +} + +module.exports = GuildTextThreadManager; diff --git a/node_modules/discord.js/src/managers/MessageManager.js b/node_modules/discord.js/src/managers/MessageManager.js new file mode 100644 index 0000000..6fb95eb --- /dev/null +++ b/node_modules/discord.js/src/managers/MessageManager.js @@ -0,0 +1,263 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { makeURLSearchParams } = require('@discordjs/rest'); +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const { Message } = require('../structures/Message'); +const MessagePayload = require('../structures/MessagePayload'); +const { MakeCacheOverrideSymbol } = require('../util/Symbols'); +const { resolvePartialEmoji } = require('../util/Util'); + +/** + * Manages API methods for Messages and holds their cache. + * @extends {CachedManager} + * @abstract + */ +class MessageManager extends CachedManager { + static [MakeCacheOverrideSymbol] = MessageManager; + + 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<Snowflake, Message>} + * @name MessageManager#cache + */ + + _add(data, cache) { + return super._add(data, cache); + } + + /** + * Data that can be resolved to a Message object. This can be: + * * A Message + * * A Snowflake + * @typedef {Message|Snowflake} MessageResolvable + */ + + /** + * Options used to fetch a message. + * @typedef {BaseFetchOptions} FetchMessageOptions + * @property {MessageResolvable} message The message to fetch + */ + + /** + * Options used to fetch multiple messages. + * <info>The `before`, `after`, and `around` parameters are mutually exclusive.</info> + * @typedef {Object} FetchMessagesOptions + * @property {number} [limit] The maximum number of messages to return + * @property {Snowflake} [before] Consider only messages before this id + * @property {Snowflake} [after] Consider only messages after this id + * @property {Snowflake} [around] Consider only messages around this id + * @property {boolean} [cache] Whether to cache the fetched messages + */ + + /** + * Fetches message(s) from a channel. + * <info>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.</info> + * @param {MessageResolvable|FetchMessageOptions|FetchMessagesOptions} [options] Options for fetching message(s) + * @returns {Promise<Message|Collection<Snowflake, Message>>} + * @example + * // Fetch a message + * channel.messages.fetch('99539446449315840') + * .then(message => console.log(message.content)) + * .catch(console.error); + * @example + * // Fetch a maximum of 10 messages without caching + * channel.messages.fetch({ limit: 10, cache: false }) + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + * @example + * // Fetch a maximum of 10 messages without caching around a message id + * channel.messages.fetch({ limit: 10, cache: false, around: '99539446449315840' }) + * .then(messages => console.log(`Received ${messages.size} messages`)) + * .catch(console.error); + * @example + * // Fetch messages and filter by a user id + * channel.messages.fetch() + * .then(messages => console.log(`${messages.filter(m => m.author.id === '84484653687267328').size} messages`)) + * .catch(console.error); + */ + fetch(options) { + if (!options) return this._fetchMany(); + const { message, cache, force } = options; + const resolvedMessage = this.resolveId(message ?? options); + if (resolvedMessage) return this._fetchSingle({ message: resolvedMessage, cache, force }); + return this._fetchMany(options); + } + + async _fetchSingle({ message, cache, force = false }) { + if (!force) { + const existing = this.cache.get(message); + if (existing && !existing.partial) return existing; + } + + const data = await this.client.rest.get(Routes.channelMessage(this.channel.id, message)); + return this._add(data, cache); + } + + async _fetchMany(options = {}) { + const data = await this.client.rest.get(Routes.channelMessages(this.channel.id), { + query: makeURLSearchParams(options), + }); + + return data.reduce((_data, message) => _data.set(message.id, this._add(message, options.cache)), new Collection()); + } + + /** + * Fetches the pinned messages of this channel and returns a collection of them. + * <info>The returned Collection does not contain any reaction data of the messages. + * Those need to be fetched separately.</info> + * @param {boolean} [cache=true] Whether to cache the message(s) + * @returns {Promise<Collection<Snowflake, Message>>} + * @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.rest.get(Routes.channelPins(this.channel.id)); + const messages = new Collection(); + for (const message of data) messages.set(message.id, this._add(message, cache)); + return messages; + } + + /** + * 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} + */ + + /** + * Options that can be passed to edit a message. + * @typedef {BaseMessageOptions} MessageEditOptions + * @property {AttachmentPayload[]} [attachments] An array of attachments to keep, + * all attachments will be kept if omitted + * @property {MessageFlags} [flags] Which flags to set for the message + * <info>Only the {@link MessageFlags.SuppressEmbeds} flag can be modified.</info> + */ + + /** + * 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<Message>} + */ + async edit(message, options) { + const messageId = this.resolveId(message); + if (!messageId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'message', 'MessageResolvable'); + + const { body, files } = await (options instanceof MessagePayload + ? options + : MessagePayload.create(message instanceof Message ? message : this, options) + ) + .resolveBody() + .resolveFiles(); + const d = await this.client.rest.patch(Routes.channelMessage(this.channel.id, messageId), { body, 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<Message>} + */ + async crosspost(message) { + message = this.resolveId(message); + if (!message) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'message', 'MessageResolvable'); + + const data = await this.client.rest.post(Routes.channelMessageCrosspost(this.channel.id, message)); + 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 + * @param {string} [reason] Reason for pinning + * @returns {Promise<void>} + */ + async pin(message, reason) { + message = this.resolveId(message); + if (!message) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'message', 'MessageResolvable'); + + await this.client.rest.put(Routes.channelPin(this.channel.id, message), { reason }); + } + + /** + * Unpins a message from the channel's pinned messages, even if it's not cached. + * @param {MessageResolvable} message The message to unpin + * @param {string} [reason] Reason for unpinning + * @returns {Promise<void>} + */ + async unpin(message, reason) { + message = this.resolveId(message); + if (!message) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'message', 'MessageResolvable'); + + await this.client.rest.delete(Routes.channelPin(this.channel.id, message), { reason }); + } + + /** + * 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<void>} + */ + async react(message, emoji) { + message = this.resolveId(message); + if (!message) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'message', 'MessageResolvable'); + + emoji = resolvePartialEmoji(emoji); + if (!emoji) throw new DiscordjsTypeError(ErrorCodes.EmojiType, 'emoji', 'EmojiIdentifierResolvable'); + + const emojiId = emoji.id + ? `${emoji.animated ? 'a:' : ''}${emoji.name}:${emoji.id}` + : encodeURIComponent(emoji.name); + + await this.client.rest.put(Routes.channelMessageOwnReaction(this.channel.id, message, emojiId)); + } + + /** + * Deletes a message, even if it's not cached. + * @param {MessageResolvable} message The message to delete + * @returns {Promise<void>} + */ + async delete(message) { + message = this.resolveId(message); + if (!message) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'message', 'MessageResolvable'); + + await this.client.rest.delete(Routes.channelMessage(this.channel.id, message)); + } +} + +module.exports = MessageManager; diff --git a/node_modules/discord.js/src/managers/PermissionOverwriteManager.js b/node_modules/discord.js/src/managers/PermissionOverwriteManager.js new file mode 100644 index 0000000..011a649 --- /dev/null +++ b/node_modules/discord.js/src/managers/PermissionOverwriteManager.js @@ -0,0 +1,168 @@ +'use strict'; + +const process = require('node:process'); +const { Collection } = require('@discordjs/collection'); +const { OverwriteType, Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const PermissionOverwrites = require('../structures/PermissionOverwrites'); +const { Role } = require('../structures/Role'); + +let cacheWarningEmitted = false; + +/** + * Manages API methods for guild channel permission overwrites and stores their cache. + * @extends {CachedManager} + */ +class PermissionOverwriteManager extends CachedManager { + constructor(channel, iterable) { + super(channel.client, PermissionOverwrites); + if (!cacheWarningEmitted && this._cache.constructor.name !== 'Collection') { + cacheWarningEmitted = true; + process.emitWarning( + `Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`, + 'UnsupportedCacheOverwriteWarning', + ); + } + + /** + * The channel of the permission overwrite this manager belongs to + * @type {GuildChannel} + */ + this.channel = channel; + + if (iterable) { + for (const item of iterable) { + this._add(item); + } + } + } + + /** + * The cache of this Manager + * @type {Collection<Snowflake, PermissionOverwrites>} + * @name PermissionOverwriteManager#cache + */ + + _add(data, cache) { + return super._add(data, cache, { extras: [this.channel] }); + } + + /** + * Replaces the permission overwrites in this channel. + * @param {OverwriteResolvable[]|Collection<Snowflake, OverwriteResolvable>} overwrites + * Permission overwrites the channel gets updated with + * @param {string} [reason] Reason for updating the channel overwrites + * @returns {Promise<GuildChannel>} + * @example + * message.channel.permissionOverwrites.set([ + * { + * id: message.author.id, + * deny: [PermissionsFlagsBit.ViewChannel], + * }, + * ], 'Needed to change permissions'); + */ + set(overwrites, reason) { + if (!Array.isArray(overwrites) && !(overwrites instanceof Collection)) { + return Promise.reject( + new DiscordjsTypeError( + ErrorCodes.InvalidType, + 'overwrites', + 'Array or Collection of Permission Overwrites', + true, + ), + ); + } + return this.channel.edit({ permissionOverwrites: overwrites, reason }); + } + + /** + * Extra information about the overwrite. + * @typedef {Object} GuildChannelOverwriteOptions + * @property {string} [reason] The reason for creating/editing this overwrite + * @property {OverwriteType} [type] The type of overwrite. Use this to bypass automatic resolution of `type` + * that results in an error for an uncached structure + */ + + /** + * Creates or edits permission overwrites for a user or role in this channel. + * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update + * @param {PermissionOverwriteOptions} options The options for the update + * @param {GuildChannelOverwriteOptions} [overwriteOptions] The extra information for the update + * @param {PermissionOverwrites} [existing] The existing overwrites to merge with this update + * @returns {Promise<GuildChannel>} + * @private + */ + async upsert(userOrRole, options, overwriteOptions = {}, existing) { + let userOrRoleId = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole); + let { type, reason } = overwriteOptions; + if (typeof type !== 'number') { + userOrRole = this.channel.guild.roles.resolve(userOrRole) ?? this.client.users.resolve(userOrRole); + if (!userOrRole) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'parameter', 'User nor a Role'); + type = userOrRole instanceof Role ? OverwriteType.Role : OverwriteType.Member; + } + + const { allow, deny } = PermissionOverwrites.resolveOverwriteOptions(options, existing); + + await this.client.rest.put(Routes.channelPermission(this.channel.id, userOrRoleId), { + body: { id: userOrRoleId, type, allow, deny }, + reason, + }); + return this.channel; + } + + /** + * Creates permission overwrites for a user or role in this channel, or replaces them if already present. + * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update + * @param {PermissionOverwriteOptions} options The options for the update + * @param {GuildChannelOverwriteOptions} [overwriteOptions] The extra information for the update + * @returns {Promise<GuildChannel>} + * @example + * // Create or Replace permission overwrites for a message author + * message.channel.permissionOverwrites.create(message.author, { + * SendMessages: false + * }) + * .then(channel => console.log(channel.permissionOverwrites.cache.get(message.author.id))) + * .catch(console.error); + */ + create(userOrRole, options, overwriteOptions) { + return this.upsert(userOrRole, options, overwriteOptions); + } + + /** + * Edits permission overwrites for a user or role in this channel, or creates an entry if not already present. + * @param {RoleResolvable|UserResolvable} userOrRole The user or role to update + * @param {PermissionOverwriteOptions} options The options for the update + * @param {GuildChannelOverwriteOptions} [overwriteOptions] The extra information for the update + * @returns {Promise<GuildChannel>} + * @example + * // Edit or Create permission overwrites for a message author + * message.channel.permissionOverwrites.edit(message.author, { + * SendMessages: false + * }) + * .then(channel => console.log(channel.permissionOverwrites.cache.get(message.author.id))) + * .catch(console.error); + */ + edit(userOrRole, options, overwriteOptions) { + const existing = this.cache.get( + this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole), + ); + return this.upsert(userOrRole, options, overwriteOptions, existing); + } + + /** + * Deletes permission overwrites for a user or role in this channel. + * @param {UserResolvable|RoleResolvable} userOrRole The user or role to delete + * @param {string} [reason] The reason for deleting the overwrite + * @returns {Promise<GuildChannel>} + */ + async delete(userOrRole, reason) { + const userOrRoleId = this.channel.guild.roles.resolveId(userOrRole) ?? this.client.users.resolveId(userOrRole); + if (!userOrRoleId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'parameter', 'User nor a Role'); + + await this.client.rest.delete(Routes.channelPermission(this.channel.id, userOrRoleId), { reason }); + return this.channel; + } +} + +module.exports = PermissionOverwriteManager; diff --git a/node_modules/discord.js/src/managers/PresenceManager.js b/node_modules/discord.js/src/managers/PresenceManager.js new file mode 100644 index 0000000..2d64834 --- /dev/null +++ b/node_modules/discord.js/src/managers/PresenceManager.js @@ -0,0 +1,58 @@ +'use strict'; + +const CachedManager = require('./CachedManager'); +const { Presence } = require('../structures/Presence'); + +/** + * Manages API methods for Presences and holds their cache. + * @extends {CachedManager} + */ +class PresenceManager extends CachedManager { + constructor(client, iterable) { + super(client, Presence, iterable); + } + + /** + * The cache of Presences + * @type {Collection<Snowflake, Presence>} + * @name PresenceManager#cache + */ + + _add(data, cache) { + return super._add(data, cache, { id: data.user.id }); + } + + /** + * Data that can be resolved to a Presence object. This can be: + * * A Presence + * * A UserResolvable + * * A Snowflake + * @typedef {Presence|UserResolvable|Snowflake} PresenceResolvable + */ + + /** + * Resolves a {@link PresenceResolvable} to a {@link Presence} object. + * @param {PresenceResolvable} presence The presence resolvable to resolve + * @returns {?Presence} + */ + resolve(presence) { + const presenceResolvable = super.resolve(presence); + if (presenceResolvable) return presenceResolvable; + const UserResolvable = this.client.users.resolveId(presence); + return super.resolve(UserResolvable); + } + + /** + * Resolves a {@link PresenceResolvable} to a {@link Presence} id. + * @param {PresenceResolvable} presence The presence resolvable to resolve + * @returns {?Snowflake} + */ + resolveId(presence) { + const presenceResolvable = super.resolveId(presence); + if (presenceResolvable) return presenceResolvable; + const userResolvable = this.client.users.resolveId(presence); + return this.cache.has(userResolvable) ? userResolvable : null; + } +} + +module.exports = PresenceManager; diff --git a/node_modules/discord.js/src/managers/ReactionManager.js b/node_modules/discord.js/src/managers/ReactionManager.js new file mode 100644 index 0000000..5535882 --- /dev/null +++ b/node_modules/discord.js/src/managers/ReactionManager.js @@ -0,0 +1,68 @@ +'use strict'; + +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const MessageReaction = require('../structures/MessageReaction'); + +/** + * Manages API methods for reactions and holds their cache. + * @extends {CachedManager} + */ +class ReactionManager extends CachedManager { + constructor(message, iterable) { + super(message.client, MessageReaction, iterable); + + /** + * The message that this manager belongs to + * @type {Message} + */ + this.message = message; + } + + _add(data, cache) { + return super._add(data, cache, { id: data.emoji.id ?? data.emoji.name, extras: [this.message] }); + } + + /** + * The reaction cache of this manager + * @type {Collection<string|Snowflake, MessageReaction>} + * @name ReactionManager#cache + */ + + /** + * Data that can be resolved to a MessageReaction object. This can be: + * * A MessageReaction + * * A Snowflake + * * The Unicode representation of an emoji + * @typedef {MessageReaction|Snowflake} MessageReactionResolvable + */ + + /** + * Resolves a {@link MessageReactionResolvable} to a {@link MessageReaction} object. + * @method resolve + * @memberof ReactionManager + * @instance + * @param {MessageReactionResolvable} reaction The MessageReaction to resolve + * @returns {?MessageReaction} + */ + + /** + * Resolves a {@link MessageReactionResolvable} to a {@link MessageReaction} id. + * @method resolveId + * @memberof ReactionManager + * @instance + * @param {MessageReactionResolvable} reaction The MessageReaction to resolve + * @returns {?Snowflake} + */ + + /** + * Removes all reactions from a message. + * @returns {Promise<Message>} + */ + async removeAll() { + await this.client.rest.delete(Routes.channelMessageAllReactions(this.message.channelId, this.message.id)); + return this.message; + } +} + +module.exports = ReactionManager; diff --git a/node_modules/discord.js/src/managers/ReactionUserManager.js b/node_modules/discord.js/src/managers/ReactionUserManager.js new file mode 100644 index 0000000..014cea8 --- /dev/null +++ b/node_modules/discord.js/src/managers/ReactionUserManager.js @@ -0,0 +1,77 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { makeURLSearchParams } = require('@discordjs/rest'); +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsError, ErrorCodes } = require('../errors'); +const User = require('../structures/User'); + +/** + * 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); + + /** + * The reaction that this manager belongs to + * @type {MessageReaction} + */ + this.reaction = reaction; + } + + /** + * The cache of this manager + * @type {Collection<Snowflake, User>} + * @name ReactionUserManager#cache + */ + + /** + * Options used to fetch users who gave a reaction. + * @typedef {Object} FetchReactionUsersOptions + * @property {number} [limit=100] The maximum amount of users to fetch, defaults to `100` + * @property {Snowflake} [after] Limit fetching users to those with an id greater than the supplied id + */ + + /** + * 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<Collection<Snowflake, User>>} + */ + async fetch({ limit = 100, after } = {}) { + const message = this.reaction.message; + const query = makeURLSearchParams({ limit, after }); + const data = await this.client.rest.get( + Routes.channelMessageReaction(message.channelId, message.id, this.reaction.emoji.identifier), + { query }, + ); + const users = new Collection(); + for (const rawUser of data) { + const user = this.client.users._add(rawUser); + this.cache.set(user.id, user); + users.set(user.id, user); + } + return users; + } + + /** + * Removes a user from this reaction. + * @param {UserResolvable} [user=this.client.user] The user to remove the reaction of + * @returns {Promise<MessageReaction>} + */ + async remove(user = this.client.user) { + const userId = this.client.users.resolveId(user); + if (!userId) throw new DiscordjsError(ErrorCodes.ReactionResolveUser); + const message = this.reaction.message; + const route = + userId === this.client.user.id + ? Routes.channelMessageOwnReaction(message.channelId, message.id, this.reaction.emoji.identifier) + : Routes.channelMessageUserReaction(message.channelId, message.id, this.reaction.emoji.identifier, userId); + await this.client.rest.delete(route); + return this.reaction; + } +} + +module.exports = ReactionUserManager; diff --git a/node_modules/discord.js/src/managers/RoleManager.js b/node_modules/discord.js/src/managers/RoleManager.js new file mode 100644 index 0000000..e0c4ed7 --- /dev/null +++ b/node_modules/discord.js/src/managers/RoleManager.js @@ -0,0 +1,360 @@ +'use strict'; + +const process = require('node:process'); +const { Collection } = require('@discordjs/collection'); +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const { Role } = require('../structures/Role'); +const DataResolver = require('../util/DataResolver'); +const PermissionsBitField = require('../util/PermissionsBitField'); +const { setPosition, resolveColor } = require('../util/Util'); + +let cacheWarningEmitted = false; + +/** + * Manages API methods for roles and stores their cache. + * @extends {CachedManager} + */ +class RoleManager extends CachedManager { + constructor(guild, iterable) { + super(guild.client, Role, iterable); + if (!cacheWarningEmitted && this._cache.constructor.name !== 'Collection') { + cacheWarningEmitted = true; + process.emitWarning( + `Overriding the cache handling for ${this.constructor.name} is unsupported and breaks functionality.`, + 'UnsupportedCacheOverwriteWarning', + ); + } + + /** + * The guild belonging to this manager + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The role cache of this manager + * @type {Collection<Snowflake, Role>} + * @name RoleManager#cache + */ + + _add(data, cache) { + return super._add(data, cache, { extras: [this.guild] }); + } + + /** + * Obtains a role from Discord, or the role cache if they're already available. + * @param {Snowflake} [id] The role's id + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise<?Role|Collection<Snowflake, Role>>} + * @example + * // Fetch all roles from the guild + * message.guild.roles.fetch() + * .then(roles => console.log(`There are ${roles.size} roles.`)) + * .catch(console.error); + * @example + * // Fetch a single role + * message.guild.roles.fetch('222078108977594368') + * .then(role => console.log(`The role color is: ${role.color}`)) + * .catch(console.error); + */ + async fetch(id, { cache = true, force = false } = {}) { + if (id && !force) { + const existing = this.cache.get(id); + if (existing) return existing; + } + + // We cannot fetch a single role, as of this commit's date, Discord API throws with 405 + const data = await this.client.rest.get(Routes.guildRoles(this.guild.id)); + const roles = new Collection(); + for (const role of data) roles.set(role.id, this._add(role, cache)); + return id ? roles.get(id) ?? null : roles; + } + + /** + * Data that can be resolved to a Role object. This can be: + * * A Role + * * A Snowflake + * @typedef {Role|Snowflake} RoleResolvable + */ + + /** + * Resolves a {@link RoleResolvable} to a {@link Role} object. + * @method resolve + * @memberof RoleManager + * @instance + * @param {RoleResolvable} role The role resolvable to resolve + * @returns {?Role} + */ + + /** + * Resolves a {@link RoleResolvable} to a {@link Role} id. + * @method resolveId + * @memberof RoleManager + * @instance + * @param {RoleResolvable} role The role resolvable to resolve + * @returns {?Snowflake} + */ + + /** + * Options used to create a new role. + * @typedef {Object} RoleCreateOptions + * @property {string} [name] The name of the new role + * @property {ColorResolvable} [color] The data to create the role with + * @property {boolean} [hoist] Whether or not the new role should be hoisted + * @property {PermissionResolvable} [permissions] The permissions for the new role + * @property {number} [position] The position of the new role + * @property {boolean} [mentionable] Whether or not the new role should be mentionable + * @property {?(BufferResolvable|Base64Resolvable|EmojiResolvable)} [icon] The icon for the role + * <warn>The `EmojiResolvable` should belong to the same guild as the role. + * If not, pass the emoji's URL directly</warn> + * @property {?string} [unicodeEmoji] The unicode emoji for the role + * @property {string} [reason] The reason for creating this role + */ + + /** + * Creates a new role in the guild with given information. + * <warn>The position will silently reset to 1 if an invalid one is provided, or none.</warn> + * @param {RoleCreateOptions} [options] Options for creating the new role + * @returns {Promise<Role>} + * @example + * // Create a new role + * guild.roles.create() + * .then(console.log) + * .catch(console.error); + * @example + * // Create a new role with data and a reason + * guild.roles.create({ + * name: 'Super Cool Blue People', + * color: Colors.Blue, + * reason: 'we needed a role for Super Cool People', + * }) + * .then(console.log) + * .catch(console.error); + */ + async create(options = {}) { + let { name, color, hoist, permissions, position, mentionable, reason, icon, unicodeEmoji } = options; + color &&= resolveColor(color); + if (permissions !== undefined) permissions = new PermissionsBitField(permissions); + if (icon) { + const guildEmojiURL = this.guild.emojis.resolve(icon)?.url; + icon = guildEmojiURL ? await DataResolver.resolveImage(guildEmojiURL) : await DataResolver.resolveImage(icon); + if (typeof icon !== 'string') icon = undefined; + } + + const data = await this.client.rest.post(Routes.guildRoles(this.guild.id), { + body: { + name, + color, + hoist, + permissions, + mentionable, + icon, + unicode_emoji: unicodeEmoji, + }, + reason, + }); + const { role } = this.client.actions.GuildRoleCreate.handle({ + guild_id: this.guild.id, + role: data, + }); + if (position) return this.setPosition(role, position, { reason }); + return role; + } + + /** + * Options for editing a role + * @typedef {RoleData} RoleEditOptions + * @property {string} [reason] The reason for editing this role + */ + + /** + * Edits a role of the guild. + * @param {RoleResolvable} role The role to edit + * @param {RoleEditOptions} options The options to provide + * @returns {Promise<Role>} + * @example + * // Edit a role + * guild.roles.edit('222079219327434752', { name: 'buddies' }) + * .then(updated => console.log(`Edited role name to ${updated.name}`)) + * .catch(console.error); + */ + async edit(role, options) { + role = this.resolve(role); + if (!role) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'role', 'RoleResolvable'); + + if (typeof options.position === 'number') { + await this.setPosition(role, options.position, { reason: options.reason }); + } + + let icon = options.icon; + if (icon) { + const guildEmojiURL = this.guild.emojis.resolve(icon)?.url; + icon = guildEmojiURL ? await DataResolver.resolveImage(guildEmojiURL) : await DataResolver.resolveImage(icon); + if (typeof icon !== 'string') icon = undefined; + } + + const body = { + name: options.name, + color: options.color === undefined ? undefined : resolveColor(options.color), + hoist: options.hoist, + permissions: options.permissions === undefined ? undefined : new PermissionsBitField(options.permissions), + mentionable: options.mentionable, + icon, + unicode_emoji: options.unicodeEmoji, + }; + + const d = await this.client.rest.patch(Routes.guildRole(this.guild.id, role.id), { body, reason: options.reason }); + + const clone = role._clone(); + clone._patch(d); + return clone; + } + + /** + * Deletes a role. + * @param {RoleResolvable} role The role to delete + * @param {string} [reason] Reason for deleting the role + * @returns {Promise<void>} + * @example + * // Delete a role + * guild.roles.delete('222079219327434752', 'The role needed to go') + * .then(() => console.log('Deleted the role')) + * .catch(console.error); + */ + async delete(role, reason) { + const id = this.resolveId(role); + await this.client.rest.delete(Routes.guildRole(this.guild.id, id), { reason }); + this.client.actions.GuildRoleDelete.handle({ guild_id: this.guild.id, role_id: id }); + } + + /** + * Sets the new position of the role. + * @param {RoleResolvable} role The role to change the position of + * @param {number} position The new position for the role + * @param {SetRolePositionOptions} [options] Options for setting the position + * @returns {Promise<Role>} + * @example + * // Set the position of the role + * guild.roles.setPosition('222197033908436994', 1) + * .then(updated => console.log(`Role position: ${updated.position}`)) + * .catch(console.error); + */ + async setPosition(role, position, { relative, reason } = {}) { + role = this.resolve(role); + if (!role) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'role', 'RoleResolvable'); + const updatedRoles = await setPosition( + role, + position, + relative, + this.guild._sortedRoles(), + this.client, + Routes.guildRoles(this.guild.id), + reason, + ); + + this.client.actions.GuildRolesPositionUpdate.handle({ + guild_id: this.guild.id, + roles: updatedRoles, + }); + return role; + } + + /** + * The data needed for updating a guild role's position + * @typedef {Object} GuildRolePosition + * @property {RoleResolvable} role The role's id + * @property {number} position The position to update + */ + + /** + * Batch-updates the guild's role positions + * @param {GuildRolePosition[]} rolePositions Role positions to update + * @returns {Promise<Guild>} + * @example + * guild.roles.setPositions([{ role: roleId, position: updatedRoleIndex }]) + * .then(guild => console.log(`Role positions updated for ${guild}`)) + * .catch(console.error); + */ + async setPositions(rolePositions) { + // Make sure rolePositions are prepared for API + rolePositions = rolePositions.map(o => ({ + id: this.resolveId(o.role), + position: o.position, + })); + + // Call the API to update role positions + await this.client.rest.patch(Routes.guildRoles(this.guild.id), { body: rolePositions }); + return this.client.actions.GuildRolesPositionUpdate.handle({ + guild_id: this.guild.id, + roles: rolePositions, + }).guild; + } + + /** + * Compares the positions of two roles. + * @param {RoleResolvable} role1 First role to compare + * @param {RoleResolvable} role2 Second role to compare + * @returns {number} Negative number if the first role's position is lower (second role's is higher), + * positive number if the first's is higher (second's is lower), 0 if equal + */ + comparePositions(role1, role2) { + const resolvedRole1 = this.resolve(role1); + const resolvedRole2 = this.resolve(role2); + if (!resolvedRole1 || !resolvedRole2) { + throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'role', 'Role nor a Snowflake'); + } + + const role1Position = resolvedRole1.position; + const role2Position = resolvedRole2.position; + + if (role1Position === role2Position) { + return Number(BigInt(resolvedRole2.id) - BigInt(resolvedRole1.id)); + } + + return role1Position - role2Position; + } + + /** + * Gets the managed role a user created when joining the guild, if any + * <info>Only ever available for bots</info> + * @param {UserResolvable} user The user to access the bot role for + * @returns {?Role} + */ + botRoleFor(user) { + const userId = this.client.users.resolveId(user); + if (!userId) return null; + return this.cache.find(role => role.tags?.botId === userId) ?? null; + } + + /** + * The `@everyone` role of the guild + * @type {Role} + * @readonly + */ + get everyone() { + return this.cache.get(this.guild.id); + } + + /** + * The premium subscriber role of the guild, if any + * @type {?Role} + * @readonly + */ + get premiumSubscriberRole() { + return this.cache.find(role => role.tags?.premiumSubscriberRole) ?? null; + } + + /** + * The role with the highest position in the cache + * @type {Role} + * @readonly + */ + get highest() { + return this.cache.reduce((prev, role) => (role.comparePositionTo(prev) > 0 ? role : prev), this.cache.first()); + } +} + +module.exports = RoleManager; diff --git a/node_modules/discord.js/src/managers/StageInstanceManager.js b/node_modules/discord.js/src/managers/StageInstanceManager.js new file mode 100644 index 0000000..ea037cf --- /dev/null +++ b/node_modules/discord.js/src/managers/StageInstanceManager.js @@ -0,0 +1,154 @@ +'use strict'; + +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsTypeError, DiscordjsError, ErrorCodes } = require('../errors'); +const { StageInstance } = require('../structures/StageInstance'); + +/** + * Manages API methods for {@link StageInstance} objects and holds their cache. + * @extends {CachedManager} + */ +class StageInstanceManager extends CachedManager { + constructor(guild, iterable) { + super(guild.client, StageInstance, iterable); + + /** + * The guild this manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of this Manager + * @type {Collection<Snowflake, StageInstance>} + * @name StageInstanceManager#cache + */ + + /** + * Options used to create a stage instance. + * @typedef {Object} StageInstanceCreateOptions + * @property {string} topic The topic of the stage instance + * @property {StageInstancePrivacyLevel} [privacyLevel] The privacy level of the stage instance + * @property {boolean} [sendStartNotification] Whether to notify `@everyone` that the stage instance has started + */ + + /** + * Data that can be resolved to a Stage Channel object. This can be: + * * A StageChannel + * * A Snowflake + * @typedef {StageChannel|Snowflake} StageChannelResolvable + */ + + /** + * Creates a new stage instance. + * @param {StageChannelResolvable} channel The stage channel to associate the created stage instance to + * @param {StageInstanceCreateOptions} options The options to create the stage instance + * @returns {Promise<StageInstance>} + * @example + * // Create a stage instance + * guild.stageInstances.create('1234567890123456789', { + * topic: 'A very creative topic', + * privacyLevel: GuildPrivacyLevel.GuildOnly + * }) + * .then(stageInstance => console.log(stageInstance)) + * .catch(console.error); + */ + async create(channel, options) { + const channelId = this.guild.channels.resolveId(channel); + if (!channelId) throw new DiscordjsError(ErrorCodes.StageChannelResolve); + if (typeof options !== 'object') throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options', 'object', true); + let { topic, privacyLevel, sendStartNotification } = options; + + const data = await this.client.rest.post(Routes.stageInstances(), { + body: { + channel_id: channelId, + topic, + privacy_level: privacyLevel, + send_start_notification: sendStartNotification, + }, + }); + + return this._add(data); + } + + /** + * Fetches the stage instance associated with a stage channel, if it exists. + * @param {StageChannelResolvable} channel The stage channel whose associated stage instance is to be fetched + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise<StageInstance>} + * @example + * // Fetch a stage instance + * guild.stageInstances.fetch('1234567890123456789') + * .then(stageInstance => console.log(stageInstance)) + * .catch(console.error); + */ + async fetch(channel, { cache = true, force = false } = {}) { + const channelId = this.guild.channels.resolveId(channel); + if (!channelId) throw new DiscordjsError(ErrorCodes.StageChannelResolve); + + if (!force) { + const existing = this.cache.find(stageInstance => stageInstance.channelId === channelId); + if (existing) return existing; + } + + const data = await this.client.rest.get(Routes.stageInstance(channelId)); + return this._add(data, cache); + } + + /** + * Options used to edit an existing stage instance. + * @typedef {Object} StageInstanceEditOptions + * @property {string} [topic] The new topic of the stage instance + * @property {StageInstancePrivacyLevel} [privacyLevel] The new privacy level of the stage instance + */ + + /** + * Edits an existing stage instance. + * @param {StageChannelResolvable} channel The stage channel whose associated stage instance is to be edited + * @param {StageInstanceEditOptions} options The options to edit the stage instance + * @returns {Promise<StageInstance>} + * @example + * // Edit a stage instance + * guild.stageInstances.edit('1234567890123456789', { topic: 'new topic' }) + * .then(stageInstance => console.log(stageInstance)) + * .catch(console.error); + */ + async edit(channel, options) { + if (typeof options !== 'object') throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options', 'object', true); + const channelId = this.guild.channels.resolveId(channel); + if (!channelId) throw new DiscordjsError(ErrorCodes.StageChannelResolve); + + let { topic, privacyLevel } = options; + + const data = await this.client.rest.patch(Routes.stageInstance(channelId), { + body: { + topic, + privacy_level: privacyLevel, + }, + }); + + if (this.cache.has(data.id)) { + const clone = this.cache.get(data.id)._clone(); + clone._patch(data); + return clone; + } + + return this._add(data); + } + + /** + * Deletes an existing stage instance. + * @param {StageChannelResolvable} channel The stage channel whose associated stage instance is to be deleted + * @returns {Promise<void>} + */ + async delete(channel) { + const channelId = this.guild.channels.resolveId(channel); + if (!channelId) throw new DiscordjsError(ErrorCodes.StageChannelResolve); + + await this.client.rest.delete(Routes.stageInstance(channelId)); + } +} + +module.exports = StageInstanceManager; diff --git a/node_modules/discord.js/src/managers/ThreadManager.js b/node_modules/discord.js/src/managers/ThreadManager.js new file mode 100644 index 0000000..17569f5 --- /dev/null +++ b/node_modules/discord.js/src/managers/ThreadManager.js @@ -0,0 +1,207 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { makeURLSearchParams } = require('@discordjs/rest'); +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const ThreadChannel = require('../structures/ThreadChannel'); +const { MakeCacheOverrideSymbol } = require('../util/Symbols'); + +/** + * Manages API methods for thread-based channels and stores their cache. + * @extends {CachedManager} + */ +class ThreadManager extends CachedManager { + static [MakeCacheOverrideSymbol] = ThreadManager; + + constructor(channel, iterable) { + super(channel.client, ThreadChannel, iterable); + + /** + * The channel this Manager belongs to + * @type {TextChannel|NewsChannel|ForumChannel} + */ + this.channel = channel; + } + + /** + * Data that can be resolved to a Thread Channel object. This can be: + * * A ThreadChannel object + * * A Snowflake + * @typedef {ThreadChannel|Snowflake} ThreadChannelResolvable + */ + + /** + * The cache of this Manager + * @type {Collection<Snowflake, ThreadChannel>} + * @name ThreadManager#cache + */ + + _add(thread) { + const existing = this.cache.get(thread.id); + if (existing) return existing; + this.cache.set(thread.id, thread); + return thread; + } + + /** + * Resolves a {@link ThreadChannelResolvable} to a {@link ThreadChannel} object. + * @method resolve + * @memberof ThreadManager + * @instance + * @param {ThreadChannelResolvable} thread The ThreadChannel resolvable to resolve + * @returns {?ThreadChannel} + */ + + /** + * Resolves a {@link ThreadChannelResolvable} to a {@link ThreadChannel} id. + * @method resolveId + * @memberof ThreadManager + * @instance + * @param {ThreadChannelResolvable} thread The ThreadChannel resolvable to resolve + * @returns {?Snowflake} + */ + + /** + * Options for creating a thread. <warn>Only one of `startMessage` or `type` can be defined.</warn> + * @typedef {StartThreadOptions} ThreadCreateOptions + * @property {MessageResolvable} [startMessage] The message to start a thread from. <warn>If this is defined then type + * of thread gets automatically defined and cannot be changed. The provided `type` field will be ignored</warn> + * @property {ChannelType.AnnouncementThread|ChannelType.PublicThread|ChannelType.PrivateThread} [type] + * The type of thread to create. + * Defaults to {@link ChannelType.PublicThread} if created in a {@link TextChannel} + * <warn>When creating threads in a {@link NewsChannel} this is ignored and is always + * {@link ChannelType.AnnouncementThread}</warn> + * @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread + * <info>Can only be set when type will be {@link ChannelType.PrivateThread}</info> + */ + + /** + * Options for fetching multiple threads. + * @typedef {Object} FetchThreadsOptions + * @property {FetchArchivedThreadOptions} [archived] Options used to fetch archived threads + */ + + /** + * Obtains a thread from Discord, or the channel cache if it's already available. + * @param {ThreadChannelResolvable|FetchThreadsOptions} [options] The options to fetch threads. If it is a + * ThreadChannelResolvable then the specified thread will be fetched. Fetches all active threads if `undefined` + * @param {BaseFetchOptions} [cacheOptions] Additional options for this fetch. <warn>The `force` field gets ignored + * if `options` is not a {@link ThreadChannelResolvable}</warn> + * @returns {Promise<?(ThreadChannel|FetchedThreads|FetchedThreadsMore)>} + * {@link FetchedThreads} if active & {@link FetchedThreadsMore} if archived. + * @example + * // Fetch a thread by its id + * channel.threads.fetch('831955138126104859') + * .then(channel => console.log(channel.name)) + * .catch(console.error); + */ + fetch(options, { cache, force } = {}) { + if (!options) return this.fetchActive(cache); + const channel = this.client.channels.resolveId(options); + if (channel) return this.client.channels.fetch(channel, { cache, force }); + if (options.archived) { + return this.fetchArchived(options.archived, cache); + } + return this.fetchActive(cache); + } + + /** + * Data that can be resolved to a Date object. This can be: + * * A Date object + * * A number representing a timestamp + * * An [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) string + * @typedef {Date|number|string} DateResolvable + */ + + /** + * The options used to fetch archived threads. + * @typedef {Object} FetchArchivedThreadOptions + * @property {string} [type='public'] The type of threads to fetch (`public` or `private`) + * @property {boolean} [fetchAll=false] Whether to fetch **all** archived threads when `type` is `private` + * <info>This property requires the {@link PermissionFlagsBits.ManageThreads} permission if `true`.</info> + * @property {DateResolvable|ThreadChannelResolvable} [before] Only return threads that were archived before this Date + * or Snowflake + * <warn>Must be a {@link ThreadChannelResolvable} when `type` is `private` and `fetchAll` is `false`.</warn> + * @property {number} [limit] Maximum number of threads to return + */ + + /** + * Data returned from fetching multiple threads. + * @typedef {FetchedThreads} FetchedThreadsMore + * @property {?boolean} hasMore Whether there are potentially additional threads that require a subsequent call + */ + + /** + * Obtains a set of archived threads from Discord. + * <info>This method requires the {@link PermissionFlagsBits.ReadMessageHistory} permission + * in the parent channel.</info> + * @param {FetchArchivedThreadOptions} [options] The options to fetch archived threads + * @param {boolean} [cache=true] Whether to cache the new thread objects if they aren't already + * @returns {Promise<FetchedThreadsMore>} + */ + async fetchArchived({ type = 'public', fetchAll = false, before, limit } = {}, cache = true) { + let path = Routes.channelThreads(this.channel.id, type); + if (type === 'private' && !fetchAll) { + path = Routes.channelJoinedArchivedThreads(this.channel.id); + } + let timestamp; + let id; + const query = makeURLSearchParams({ limit }); + if (before !== undefined) { + if (before instanceof ThreadChannel || /^\d{17,19}$/.test(String(before))) { + id = this.resolveId(before); + timestamp = this.resolve(before)?.archivedAt?.toISOString(); + const toUse = type === 'private' && !fetchAll ? id : timestamp; + if (toUse) { + query.set('before', toUse); + } + } else { + try { + timestamp = new Date(before).toISOString(); + if (type === 'public' || fetchAll) { + query.set('before', timestamp); + } + } catch { + throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'before', 'DateResolvable or ThreadChannelResolvable'); + } + } + } + + const raw = await this.client.rest.get(path, { query }); + return this.constructor._mapThreads(raw, this.client, { parent: this.channel, cache }); + } + + /** + * Obtains all active threads in the channel. + * @param {boolean} [cache=true] Whether to cache the fetched data + * @returns {Promise<FetchedThreads>} + */ + async fetchActive(cache = true) { + const data = await this.channel.guild.channels.rawFetchGuildActiveThreads(); + return this.constructor._mapThreads(data, this.client, { parent: this.channel, cache }); + } + + static _mapThreads(rawThreads, client, { parent, guild, cache }) { + const threads = rawThreads.threads.reduce((coll, raw) => { + const thread = client.channels._add(raw, guild ?? parent?.guild, { cache }); + if (parent && thread.parentId !== parent.id) return coll; + return coll.set(thread.id, thread); + }, new Collection()); + + // Discord sends the thread id as id in this object + const threadMembers = rawThreads.members.reduce((coll, raw) => { + const thread = threads.get(raw.id); + return thread ? coll.set(raw.user_id, thread.members._add(raw)) : coll; + }, new Collection()); + + const response = { threads, members: threadMembers }; + + // The GET `/guilds/{guild.id}/threads/active` route does not return `has_more`. + if ('has_more' in rawThreads) response.hasMore = rawThreads.has_more; + return response; + } +} + +module.exports = ThreadManager; diff --git a/node_modules/discord.js/src/managers/ThreadMemberManager.js b/node_modules/discord.js/src/managers/ThreadMemberManager.js new file mode 100644 index 0000000..c138aa3 --- /dev/null +++ b/node_modules/discord.js/src/managers/ThreadMemberManager.js @@ -0,0 +1,182 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { makeURLSearchParams } = require('@discordjs/rest'); +const { Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const ThreadMember = require('../structures/ThreadMember'); + +/** + * Manages API methods for GuildMembers and stores their cache. + * @extends {CachedManager} + */ +class ThreadMemberManager extends CachedManager { + constructor(thread, iterable) { + super(thread.client, ThreadMember, iterable); + + /** + * The thread this manager belongs to + * @type {ThreadChannel} + */ + this.thread = thread; + } + + /** + * The cache of this Manager + * @type {Collection<Snowflake, ThreadMember>} + * @name ThreadMemberManager#cache + */ + + _add(data, cache = true) { + const existing = this.cache.get(data.user_id); + if (cache) existing?._patch(data, { cache }); + if (existing) return existing; + + const member = new ThreadMember(this.thread, data, { cache }); + if (cache) this.cache.set(data.user_id, member); + return member; + } + + /** + * Fetches the client user as a ThreadMember of the thread. + * @param {BaseFetchOptions} [options] The options for fetching the member + * @returns {Promise<ThreadMember>} + */ + fetchMe(options) { + return this.fetch({ ...options, member: this.client.user.id }); + } + + /** + * The client user as a ThreadMember of this ThreadChannel + * @type {?ThreadMember} + * @readonly + */ + get me() { + return this.resolve(this.client.user.id); + } + + /** + * Data that resolves to give a ThreadMember object. This can be: + * * A ThreadMember object + * * A User resolvable + * @typedef {ThreadMember|UserResolvable} ThreadMemberResolvable + */ + + /** + * Resolves a {@link ThreadMemberResolvable} to a {@link ThreadMember} object. + * @param {ThreadMemberResolvable} member The user that is part of the thread + * @returns {?GuildMember} + */ + resolve(member) { + const memberResolvable = super.resolve(member); + if (memberResolvable) return memberResolvable; + const userResolvable = this.client.users.resolveId(member); + if (userResolvable) return super.resolve(userResolvable); + return null; + } + + /** + * Resolves a {@link ThreadMemberResolvable} to a {@link ThreadMember} id string. + * @param {ThreadMemberResolvable} member The user that is part of the guild + * @returns {?Snowflake} + */ + resolveId(member) { + const memberResolvable = super.resolveId(member); + if (memberResolvable) return memberResolvable; + const userResolvable = this.client.users.resolveId(member); + return this.cache.has(userResolvable) ? userResolvable : null; + } + + /** + * Adds a member to the thread. + * @param {UserResolvable|'@me'} member The member to add + * @param {string} [reason] The reason for adding this member + * @returns {Promise<Snowflake>} + */ + async add(member, reason) { + const id = member === '@me' ? member : this.client.users.resolveId(member); + if (!id) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'member', 'UserResolvable'); + await this.client.rest.put(Routes.threadMembers(this.thread.id, id), { reason }); + return id; + } + + /** + * Remove a user from the thread. + * @param {Snowflake|'@me'} id The id of the member to remove + * @param {string} [reason] The reason for removing this member from the thread + * @returns {Promise<Snowflake>} + */ + async remove(id, reason) { + await this.client.rest.delete(Routes.threadMembers(this.thread.id, id), { reason }); + return id; + } + + /** + * Options used to fetch a thread member. + * @typedef {BaseFetchOptions} FetchThreadMemberOptions + * @property {ThreadMemberResolvable} member The thread member to fetch + * @property {boolean} [withMember] Whether to also return the guild member associated with this thread member + */ + + /** + * Options used to fetch multiple thread members with guild member data. + * <info>With `withMember` set to `true`, pagination is enabled.</info> + * @typedef {Object} FetchThreadMembersWithGuildMemberDataOptions + * @property {true} withMember Whether to also return the guild member data + * @property {Snowflake} [after] Consider only thread members after this id + * @property {number} [limit] The maximum number of thread members to return + * @property {boolean} [cache] Whether to cache the fetched thread members and guild members + */ + + /** + * Options used to fetch multiple thread members without guild member data. + * @typedef {Object} FetchThreadMembersWithoutGuildMemberDataOptions + * @property {false} [withMember] Whether to also return the guild member data + * @property {boolean} [cache] Whether to cache the fetched thread members + */ + + /** + * Options used to fetch multiple thread members. + * @typedef {FetchThreadMembersWithGuildMemberDataOptions| + * FetchThreadMembersWithoutGuildMemberDataOptions} FetchThreadMembersOptions + */ + + /** + * Fetches thread member(s) from Discord. + * <info>This method requires the {@link GatewayIntentBits.GuildMembers} privileged gateway intent.</info> + * @param {ThreadMemberResolvable|FetchThreadMemberOptions|FetchThreadMembersOptions} [options] + * Options for fetching thread member(s) + * @returns {Promise<ThreadMember|Collection<Snowflake, ThreadMember>>} + */ + fetch(options) { + if (!options) return this._fetchMany(); + const { member, withMember, cache, force } = options; + const resolvedMember = this.resolveId(member ?? options); + if (resolvedMember) return this._fetchSingle({ member: resolvedMember, withMember, cache, force }); + return this._fetchMany(options); + } + + async _fetchSingle({ member, withMember, cache, force = false }) { + if (!force) { + const existing = this.cache.get(member); + if (existing) return existing; + } + + const data = await this.client.rest.get(Routes.threadMembers(this.thread.id, member), { + query: makeURLSearchParams({ with_member: withMember }), + }); + + return this._add(data, cache); + } + + async _fetchMany({ withMember, after, limit, cache } = {}) { + const data = await this.client.rest.get(Routes.threadMembers(this.thread.id), { + query: makeURLSearchParams({ with_member: withMember, after, limit }), + }); + + return data.reduce((col, member) => col.set(member.user_id, this._add(member, cache)), new Collection()); + } +} + +module.exports = ThreadMemberManager; diff --git a/node_modules/discord.js/src/managers/UserManager.js b/node_modules/discord.js/src/managers/UserManager.js new file mode 100644 index 0000000..24478f6 --- /dev/null +++ b/node_modules/discord.js/src/managers/UserManager.js @@ -0,0 +1,139 @@ +'use strict'; + +const { ChannelType, Routes } = require('discord-api-types/v10'); +const CachedManager = require('./CachedManager'); +const { DiscordjsError, ErrorCodes } = require('../errors'); +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 UserManager extends CachedManager { + constructor(client, iterable) { + super(client, User, iterable); + } + + /** + * The cache of this manager + * @type {Collection<Snowflake, User>} + * @name UserManager#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 + */ + + /** + * The DM between the client's user and a user + * @param {Snowflake} userId The user id + * @returns {?DMChannel} + * @private + */ + dmChannel(userId) { + return this.client.channels.cache.find(c => c.type === ChannelType.DM && c.recipientId === userId) ?? null; + } + + /** + * Creates a {@link DMChannel} between the client and a user. + * @param {UserResolvable} user The UserResolvable to identify + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise<DMChannel>} + */ + async createDM(user, { cache = true, force = false } = {}) { + const id = this.resolveId(user); + + if (!force) { + const dmChannel = this.dmChannel(id); + if (dmChannel && !dmChannel.partial) return dmChannel; + } + + const data = await this.client.rest.post(Routes.userChannels(), { body: { recipient_id: id } }); + return this.client.channels._add(data, null, { cache }); + } + + /** + * Deletes a {@link DMChannel} (if one exists) between the client and a user. Resolves with the channel if successful. + * @param {UserResolvable} user The UserResolvable to identify + * @returns {Promise<DMChannel>} + */ + async deleteDM(user) { + const id = this.resolveId(user); + const dmChannel = this.dmChannel(id); + if (!dmChannel) throw new DiscordjsError(ErrorCodes.UserNoDMChannel); + await this.client.rest.delete(Routes.channel(dmChannel.id)); + this.client.channels._remove(dmChannel.id); + return dmChannel; + } + + /** + * 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<User>} + */ + 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.rest.get(Routes.user(id)); + return this._add(data, cache); + } + + /** + * Fetches a user's flags. + * @param {UserResolvable} user The UserResolvable to identify + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise<UserFlagsBitField>} + */ + async fetchFlags(user, options) { + return (await this.fetch(user, options)).flags; + } + + /** + * Sends a message to a user. + * @param {UserResolvable} user The UserResolvable to identify + * @param {string|MessagePayload|MessageCreateOptions} options The options to provide + * @returns {Promise<Message>} + */ + async send(user, options) { + return (await this.createDM(user)).send(options); + } + + /** + * 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); + } +} + +module.exports = UserManager; diff --git a/node_modules/discord.js/src/managers/VoiceStateManager.js b/node_modules/discord.js/src/managers/VoiceStateManager.js new file mode 100644 index 0000000..c42fdd2 --- /dev/null +++ b/node_modules/discord.js/src/managers/VoiceStateManager.js @@ -0,0 +1,37 @@ +'use strict'; + +const CachedManager = require('./CachedManager'); +const VoiceState = require('../structures/VoiceState'); + +/** + * Manages API methods for VoiceStates and stores their cache. + * @extends {CachedManager} + */ +class VoiceStateManager extends CachedManager { + constructor(guild, iterable) { + super(guild.client, VoiceState, iterable); + + /** + * The guild this manager belongs to + * @type {Guild} + */ + this.guild = guild; + } + + /** + * The cache of this manager + * @type {Collection<Snowflake, VoiceState>} + * @name VoiceStateManager#cache + */ + + _add(data, cache = true) { + const existing = this.cache.get(data.user_id); + if (existing) return existing._patch(data); + + const entry = new this.holds(this.guild, data); + if (cache) this.cache.set(data.user_id, entry); + return entry; + } +} + +module.exports = VoiceStateManager; |