diff options
Diffstat (limited to 'node_modules/discord.js/src/structures')
122 files changed, 19225 insertions, 0 deletions
diff --git a/node_modules/discord.js/src/structures/ActionRow.js b/node_modules/discord.js/src/structures/ActionRow.js new file mode 100644 index 0000000..3f39691 --- /dev/null +++ b/node_modules/discord.js/src/structures/ActionRow.js @@ -0,0 +1,46 @@ +'use strict'; + +const { deprecate } = require('node:util'); +const { isJSONEncodable } = require('@discordjs/util'); +const Component = require('./Component'); +const { createComponent } = require('../util/Components'); + +/** + * Represents an action row + * @extends {Component} + */ +class ActionRow extends Component { + constructor({ components, ...data }) { + super(data); + + /** + * The components in this action row + * @type {Component[]} + * @readonly + */ + this.components = components.map(c => createComponent(c)); + } + + /** + * Creates a new action row builder from JSON data + * @method from + * @memberof ActionRow + * @param {ActionRowBuilder|ActionRow|APIActionRowComponent} other The other data + * @returns {ActionRowBuilder} + * @deprecated Use {@link ActionRowBuilder.from} instead. + */ + static from = deprecate( + other => new this(isJSONEncodable(other) ? other.toJSON() : other), + 'ActionRow.from() is deprecated. Use ActionRowBuilder.from() instead.', + ); + + /** + * Returns the API-compatible JSON for this component + * @returns {APIActionRowComponent} + */ + toJSON() { + return { ...this.data, components: this.components.map(c => c.toJSON()) }; + } +} + +module.exports = ActionRow; diff --git a/node_modules/discord.js/src/structures/ActionRowBuilder.js b/node_modules/discord.js/src/structures/ActionRowBuilder.js new file mode 100644 index 0000000..962a378 --- /dev/null +++ b/node_modules/discord.js/src/structures/ActionRowBuilder.js @@ -0,0 +1,35 @@ +'use strict'; + +const { ActionRowBuilder: BuildersActionRow } = require('@discordjs/builders'); +const { isJSONEncodable } = require('@discordjs/util'); +const { createComponentBuilder } = require('../util/Components'); +const { toSnakeCase } = require('../util/Transformers'); + +/** + * Represents an action row builder. + * @extends {BuildersActionRow} + */ +class ActionRowBuilder extends BuildersActionRow { + constructor({ components, ...data } = {}) { + super({ + ...toSnakeCase(data), + components: components?.map(c => createComponentBuilder(c)), + }); + } + + /** + * Creates a new action row builder from JSON data + * @param {ActionRow|ActionRowBuilder|APIActionRowComponent} other The other data + * @returns {ActionRowBuilder} + */ + static from(other) { + return new this(isJSONEncodable(other) ? other.toJSON() : other); + } +} + +module.exports = ActionRowBuilder; + +/** + * @external BuildersActionRow + * @see {@link https://discord.js.org/docs/packages/builders/stable/ActionRowBuilder:Class} + */ diff --git a/node_modules/discord.js/src/structures/AnonymousGuild.js b/node_modules/discord.js/src/structures/AnonymousGuild.js new file mode 100644 index 0000000..70931bd --- /dev/null +++ b/node_modules/discord.js/src/structures/AnonymousGuild.js @@ -0,0 +1,97 @@ +'use strict'; + +const BaseGuild = require('./BaseGuild'); + +/** + * Bundles common attributes and methods between {@link Guild} and {@link InviteGuild} + * @extends {BaseGuild} + * @abstract + */ +class AnonymousGuild extends BaseGuild { + constructor(client, data, immediatePatch = true) { + super(client, data); + if (immediatePatch) this._patch(data); + } + + _patch(data) { + if ('features' in data) this.features = data.features; + + if ('splash' in data) { + /** + * The hash of the guild invite splash image + * @type {?string} + */ + this.splash = data.splash; + } + + if ('banner' in data) { + /** + * The hash of the guild banner + * @type {?string} + */ + this.banner = data.banner; + } + + if ('description' in data) { + /** + * The description of the guild, if any + * @type {?string} + */ + this.description = data.description; + } + + if ('verification_level' in data) { + /** + * The verification level of the guild + * @type {GuildVerificationLevel} + */ + this.verificationLevel = data.verification_level; + } + + if ('vanity_url_code' in data) { + /** + * The vanity invite code of the guild, if any + * @type {?string} + */ + this.vanityURLCode = data.vanity_url_code; + } + + if ('nsfw_level' in data) { + /** + * The NSFW level of this guild + * @type {GuildNSFWLevel} + */ + this.nsfwLevel = data.nsfw_level; + } + + if ('premium_subscription_count' in data) { + /** + * The total number of boosts for this server + * @type {?number} + */ + this.premiumSubscriptionCount = data.premium_subscription_count; + } else { + this.premiumSubscriptionCount ??= null; + } + } + + /** + * The URL to this guild's banner. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + bannerURL(options = {}) { + return this.banner && this.client.rest.cdn.banner(this.id, this.banner, options); + } + + /** + * The URL to this guild's invite splash image. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + splashURL(options = {}) { + return this.splash && this.client.rest.cdn.splash(this.id, this.splash, options); + } +} + +module.exports = AnonymousGuild; diff --git a/node_modules/discord.js/src/structures/ApplicationCommand.js b/node_modules/discord.js/src/structures/ApplicationCommand.js new file mode 100644 index 0000000..bd87281 --- /dev/null +++ b/node_modules/discord.js/src/structures/ApplicationCommand.js @@ -0,0 +1,606 @@ +'use strict'; + +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { ApplicationCommandOptionType } = require('discord-api-types/v10'); +const isEqual = require('fast-deep-equal'); +const Base = require('./Base'); +const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager'); +const PermissionsBitField = require('../util/PermissionsBitField'); + +/** + * Represents an application command. + * @extends {Base} + */ +class ApplicationCommand extends Base { + constructor(client, data, guild, guildId) { + super(client); + + /** + * The command's id + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The parent application's id + * @type {Snowflake} + */ + this.applicationId = data.application_id; + + /** + * The guild this command is part of + * @type {?Guild} + */ + this.guild = guild ?? null; + + /** + * The guild's id this command is part of, this may be non-null when `guild` is `null` if the command + * was fetched from the `ApplicationCommandManager` + * @type {?Snowflake} + */ + this.guildId = guild?.id ?? guildId ?? null; + + /** + * The manager for permissions of this command on its guild or arbitrary guilds when the command is global + * @type {ApplicationCommandPermissionsManager} + */ + this.permissions = new ApplicationCommandPermissionsManager(this); + + /** + * The type of this application command + * @type {ApplicationCommandType} + */ + this.type = data.type; + + /** + * Whether this command is age-restricted (18+) + * @type {boolean} + */ + this.nsfw = data.nsfw ?? false; + + this._patch(data); + } + + _patch(data) { + if ('name' in data) { + /** + * The name of this command + * @type {string} + */ + this.name = data.name; + } + + if ('name_localizations' in data) { + /** + * The name localizations for this command + * @type {?Object<Locale, string>} + */ + this.nameLocalizations = data.name_localizations; + } else { + this.nameLocalizations ??= null; + } + + if ('name_localized' in data) { + /** + * The localized name for this command + * @type {?string} + */ + this.nameLocalized = data.name_localized; + } else { + this.nameLocalized ??= null; + } + + if ('description' in data) { + /** + * The description of this command + * @type {string} + */ + this.description = data.description; + } + + if ('description_localizations' in data) { + /** + * The description localizations for this command + * @type {?Object<Locale, string>} + */ + this.descriptionLocalizations = data.description_localizations; + } else { + this.descriptionLocalizations ??= null; + } + + if ('description_localized' in data) { + /** + * The localized description for this command + * @type {?string} + */ + this.descriptionLocalized = data.description_localized; + } else { + this.descriptionLocalized ??= null; + } + + if ('options' in data) { + /** + * The options of this command + * @type {ApplicationCommandOption[]} + */ + this.options = data.options.map(o => this.constructor.transformOption(o, true)); + } else { + this.options ??= []; + } + + if ('default_member_permissions' in data) { + /** + * The default bitfield used to determine whether this command be used in a guild + * @type {?Readonly<PermissionsBitField>} + */ + this.defaultMemberPermissions = data.default_member_permissions + ? new PermissionsBitField(BigInt(data.default_member_permissions)).freeze() + : null; + } else { + this.defaultMemberPermissions ??= null; + } + + if ('dm_permission' in data) { + /** + * Whether the command can be used in DMs + * <info>This property is always `null` on guild commands</info> + * @type {boolean|null} + */ + this.dmPermission = data.dm_permission; + } else { + this.dmPermission ??= null; + } + + if ('version' in data) { + /** + * Autoincrementing version identifier updated during substantial record changes + * @type {Snowflake} + */ + this.version = data.version; + } + } + + /** + * The timestamp the command was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the command was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The manager that this command belongs to + * @type {ApplicationCommandManager} + * @readonly + */ + get manager() { + return (this.guild ?? this.client.application).commands; + } + + /** + * Data for creating or editing an application command. + * @typedef {Object} ApplicationCommandData + * @property {string} name The name of the command, must be in all lowercase if type is + * {@link ApplicationCommandType.ChatInput} + * @property {Object<Locale, string>} [nameLocalizations] The localizations for the command name + * @property {string} description The description of the command, if type is {@link ApplicationCommandType.ChatInput} + * @property {boolean} [nsfw] Whether the command is age-restricted + * @property {Object<Locale, string>} [descriptionLocalizations] The localizations for the command description, + * if type is {@link ApplicationCommandType.ChatInput} + * @property {ApplicationCommandType} [type=ApplicationCommandType.ChatInput] The type of the command + * @property {ApplicationCommandOptionData[]} [options] Options for the command + * @property {?PermissionResolvable} [defaultMemberPermissions] The bitfield used to determine the default permissions + * a member needs in order to run the command + * @property {boolean} [dmPermission] Whether the command is enabled in DMs + */ + + /** + * An option for an application command or subcommand. + * <info>In addition to the listed properties, when used as a parameter, + * API style `snake_case` properties can be used for compatibility with generators like `@discordjs/builders`.</info> + * <warn>Note that providing a value for the `camelCase` counterpart for any `snake_case` property + * will discard the provided `snake_case` property.</warn> + * @typedef {Object} ApplicationCommandOptionData + * @property {ApplicationCommandOptionType} type The type of the option + * @property {string} name The name of the option + * @property {Object<Locale, string>} [nameLocalizations] The name localizations for the option + * @property {string} description The description of the option + * @property {Object<Locale, string>} [descriptionLocalizations] The description localizations for the option + * @property {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a + * {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or + * {@link ApplicationCommandOptionType.Number} option + * @property {boolean} [required] Whether the option is required + * @property {ApplicationCommandOptionChoiceData[]} [choices] The choices of the option for the user to pick from + * @property {ApplicationCommandOptionData[]} [options] Additional options if this option is a subcommand (group) + * @property {ChannelType[]} [channelTypes] When the option type is channel, + * the allowed types of channels that can be selected + * @property {number} [minValue] The minimum value for an {@link ApplicationCommandOptionType.Integer} or + * {@link ApplicationCommandOptionType.Number} option + * @property {number} [maxValue] The maximum value for an {@link ApplicationCommandOptionType.Integer} or + * {@link ApplicationCommandOptionType.Number} option + * @property {number} [minLength] The minimum length for an {@link ApplicationCommandOptionType.String} option + * (maximum of `6000`) + * @property {number} [maxLength] The maximum length for an {@link ApplicationCommandOptionType.String} option + * (maximum of `6000`) + */ + + /** + * @typedef {Object} ApplicationCommandOptionChoiceData + * @property {string} name The name of the choice + * @property {Object<Locale, string>} [nameLocalizations] The localized names for this choice + * @property {string|number} value The value of the choice + */ + + /** + * Edits this application command. + * @param {Partial<ApplicationCommandData>} data The data to update the command with + * @returns {Promise<ApplicationCommand>} + * @example + * // Edit the description of this command + * command.edit({ + * description: 'New description', + * }) + * .then(console.log) + * .catch(console.error); + */ + edit(data) { + return this.manager.edit(this, data, this.guildId); + } + + /** + * Edits the name of this ApplicationCommand + * @param {string} name The new name of the command + * @returns {Promise<ApplicationCommand>} + */ + setName(name) { + return this.edit({ name }); + } + + /** + * Edits the localized names of this ApplicationCommand + * @param {Object<Locale, string>} nameLocalizations The new localized names for the command + * @returns {Promise<ApplicationCommand>} + * @example + * // Edit the name localizations of this command + * command.setLocalizedNames({ + * 'en-GB': 'test', + * 'pt-BR': 'teste', + * }) + * .then(console.log) + * .catch(console.error) + */ + setNameLocalizations(nameLocalizations) { + return this.edit({ nameLocalizations }); + } + + /** + * Edits the description of this ApplicationCommand + * @param {string} description The new description of the command + * @returns {Promise<ApplicationCommand>} + */ + setDescription(description) { + return this.edit({ description }); + } + + /** + * Edits the localized descriptions of this ApplicationCommand + * @param {Object<Locale, string>} descriptionLocalizations The new localized descriptions for the command + * @returns {Promise<ApplicationCommand>} + * @example + * // Edit the description localizations of this command + * command.setDescriptionLocalizations({ + * 'en-GB': 'A test command', + * 'pt-BR': 'Um comando de teste', + * }) + * .then(console.log) + * .catch(console.error) + */ + setDescriptionLocalizations(descriptionLocalizations) { + return this.edit({ descriptionLocalizations }); + } + + /** + * Edits the default member permissions of this ApplicationCommand + * @param {?PermissionResolvable} defaultMemberPermissions The default member permissions required to run this command + * @returns {Promise<ApplicationCommand>} + */ + setDefaultMemberPermissions(defaultMemberPermissions) { + return this.edit({ defaultMemberPermissions }); + } + + /** + * Edits the DM permission of this ApplicationCommand + * @param {boolean} [dmPermission=true] Whether the command can be used in DMs + * @returns {Promise<ApplicationCommand>} + */ + setDMPermission(dmPermission = true) { + return this.edit({ dmPermission }); + } + + /** + * Edits the options of this ApplicationCommand + * @param {ApplicationCommandOptionData[]} options The options to set for this command + * @returns {Promise<ApplicationCommand>} + */ + setOptions(options) { + return this.edit({ options }); + } + + /** + * Deletes this command. + * @returns {Promise<ApplicationCommand>} + * @example + * // Delete this command + * command.delete() + * .then(console.log) + * .catch(console.error); + */ + delete() { + return this.manager.delete(this, this.guildId); + } + + /** + * Whether this command equals another command. It compares all properties, so for most operations + * it is advisable to just compare `command.id === command2.id` as it is much faster and is often + * what most users need. + * @param {ApplicationCommand|ApplicationCommandData|APIApplicationCommand} command The command to compare with + * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options and choices are in the same + * order in the array <info>The client may not always respect this ordering!</info> + * @returns {boolean} + */ + equals(command, enforceOptionOrder = false) { + // If given an id, check if the id matches + if (command.id && this.id !== command.id) return false; + + let defaultMemberPermissions = null; + let dmPermission = command.dmPermission ?? command.dm_permission; + + if ('default_member_permissions' in command) { + defaultMemberPermissions = command.default_member_permissions + ? new PermissionsBitField(BigInt(command.default_member_permissions)).bitfield + : null; + } + + if ('defaultMemberPermissions' in command) { + defaultMemberPermissions = + command.defaultMemberPermissions !== null + ? new PermissionsBitField(command.defaultMemberPermissions).bitfield + : null; + } + + // Check top level parameters + if ( + command.name !== this.name || + ('description' in command && command.description !== this.description) || + ('version' in command && command.version !== this.version) || + (command.type && command.type !== this.type) || + ('nsfw' in command && command.nsfw !== this.nsfw) || + // Future proof for options being nullable + // TODO: remove ?? 0 on each when nullable + (command.options?.length ?? 0) !== (this.options?.length ?? 0) || + defaultMemberPermissions !== (this.defaultMemberPermissions?.bitfield ?? null) || + (dmPermission !== undefined && dmPermission !== this.dmPermission) || + !isEqual(command.nameLocalizations ?? command.name_localizations ?? {}, this.nameLocalizations ?? {}) || + !isEqual( + command.descriptionLocalizations ?? command.description_localizations ?? {}, + this.descriptionLocalizations ?? {}, + ) + ) { + return false; + } + + if (command.options) { + return this.constructor.optionsEqual(this.options, command.options, enforceOptionOrder); + } + return true; + } + + /** + * Recursively checks that all options for an {@link ApplicationCommand} are equal to the provided options. + * In most cases it is better to compare using {@link ApplicationCommand#equals} + * @param {ApplicationCommandOptionData[]} existing The options on the existing command, + * should be {@link ApplicationCommand#options} + * @param {ApplicationCommandOptionData[]|APIApplicationCommandOption[]} options The options to compare against + * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options and choices are in the same + * order in the array <info>The client may not always respect this ordering!</info> + * @returns {boolean} + */ + static optionsEqual(existing, options, enforceOptionOrder = false) { + if (existing.length !== options.length) return false; + if (enforceOptionOrder) { + return existing.every((option, index) => this._optionEquals(option, options[index], enforceOptionOrder)); + } + const newOptions = new Map(options.map(option => [option.name, option])); + for (const option of existing) { + const foundOption = newOptions.get(option.name); + if (!foundOption || !this._optionEquals(option, foundOption)) return false; + } + return true; + } + + /** + * Checks that an option for an {@link ApplicationCommand} is equal to the provided option + * In most cases it is better to compare using {@link ApplicationCommand#equals} + * @param {ApplicationCommandOptionData} existing The option on the existing command, + * should be from {@link ApplicationCommand#options} + * @param {ApplicationCommandOptionData|APIApplicationCommandOption} option The option to compare against + * @param {boolean} [enforceOptionOrder=false] Whether to strictly check that options or choices are in the same + * order in their array <info>The client may not always respect this ordering!</info> + * @returns {boolean} + * @private + */ + static _optionEquals(existing, option, enforceOptionOrder = false) { + if ( + option.name !== existing.name || + option.type !== existing.type || + option.description !== existing.description || + option.autocomplete !== existing.autocomplete || + (option.required ?? + ([ApplicationCommandOptionType.Subcommand, ApplicationCommandOptionType.SubcommandGroup].includes(option.type) + ? undefined + : false)) !== existing.required || + option.choices?.length !== existing.choices?.length || + option.options?.length !== existing.options?.length || + (option.channelTypes ?? option.channel_types)?.length !== existing.channelTypes?.length || + (option.minValue ?? option.min_value) !== existing.minValue || + (option.maxValue ?? option.max_value) !== existing.maxValue || + (option.minLength ?? option.min_length) !== existing.minLength || + (option.maxLength ?? option.max_length) !== existing.maxLength || + !isEqual(option.nameLocalizations ?? option.name_localizations ?? {}, existing.nameLocalizations ?? {}) || + !isEqual( + option.descriptionLocalizations ?? option.description_localizations ?? {}, + existing.descriptionLocalizations ?? {}, + ) + ) { + return false; + } + + if (existing.choices) { + if ( + enforceOptionOrder && + !existing.choices.every( + (choice, index) => + choice.name === option.choices[index].name && + choice.value === option.choices[index].value && + isEqual( + choice.nameLocalizations ?? {}, + option.choices[index].nameLocalizations ?? option.choices[index].name_localizations ?? {}, + ), + ) + ) { + return false; + } + if (!enforceOptionOrder) { + const newChoices = new Map(option.choices.map(choice => [choice.name, choice])); + for (const choice of existing.choices) { + const foundChoice = newChoices.get(choice.name); + if (!foundChoice || foundChoice.value !== choice.value) return false; + } + } + } + + if (existing.channelTypes) { + const newTypes = option.channelTypes ?? option.channel_types; + for (const type of existing.channelTypes) { + if (!newTypes.includes(type)) return false; + } + } + + if (existing.options) { + return this.optionsEqual(existing.options, option.options, enforceOptionOrder); + } + return true; + } + + /** + * An option for an application command or subcommand. + * @typedef {Object} ApplicationCommandOption + * @property {ApplicationCommandOptionType} type The type of the option + * @property {string} name The name of the option + * @property {Object<Locale, string>} [nameLocalizations] The localizations for the option name + * @property {string} [nameLocalized] The localized name for this option + * @property {string} description The description of the option + * @property {Object<Locale, string>} [descriptionLocalizations] The localizations for the option description + * @property {string} [descriptionLocalized] The localized description for this option + * @property {boolean} [required] Whether the option is required + * @property {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a + * {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or + * {@link ApplicationCommandOptionType.Number} option + * @property {ApplicationCommandOptionChoice[]} [choices] The choices of the option for the user to pick from + * @property {ApplicationCommandOption[]} [options] Additional options if this option is a subcommand (group) + * @property {ApplicationCommandOptionAllowedChannelTypes[]} [channelTypes] When the option type is channel, + * the allowed types of channels that can be selected + * @property {number} [minValue] The minimum value for an {@link ApplicationCommandOptionType.Integer} or + * {@link ApplicationCommandOptionType.Number} option + * @property {number} [maxValue] The maximum value for an {@link ApplicationCommandOptionType.Integer} or + * {@link ApplicationCommandOptionType.Number} option + * @property {number} [minLength] The minimum length for an {@link ApplicationCommandOptionType.String} option + * (maximum of `6000`) + * @property {number} [maxLength] The maximum length for an {@link ApplicationCommandOptionType.String} option + * (maximum of `6000`) + */ + + /** + * A choice for an application command option. + * @typedef {Object} ApplicationCommandOptionChoice + * @property {string} name The name of the choice + * @property {?string} nameLocalized The localized name of the choice in the provided locale, if any + * @property {?Object<string, string>} [nameLocalizations] The localized names for this choice + * @property {string|number} value The value of the choice + */ + + /** + * Transforms an {@link ApplicationCommandOptionData} object into something that can be used with the API. + * @param {ApplicationCommandOptionData|ApplicationCommandOption} option The option to transform + * @param {boolean} [received] Whether this option has been received from Discord + * @returns {APIApplicationCommandOption} + * @private + */ + static transformOption(option, received) { + const channelTypesKey = received ? 'channelTypes' : 'channel_types'; + const minValueKey = received ? 'minValue' : 'min_value'; + const maxValueKey = received ? 'maxValue' : 'max_value'; + const minLengthKey = received ? 'minLength' : 'min_length'; + const maxLengthKey = received ? 'maxLength' : 'max_length'; + const nameLocalizationsKey = received ? 'nameLocalizations' : 'name_localizations'; + const nameLocalizedKey = received ? 'nameLocalized' : 'name_localized'; + const descriptionLocalizationsKey = received ? 'descriptionLocalizations' : 'description_localizations'; + const descriptionLocalizedKey = received ? 'descriptionLocalized' : 'description_localized'; + return { + type: option.type, + name: option.name, + [nameLocalizationsKey]: option.nameLocalizations ?? option.name_localizations, + [nameLocalizedKey]: option.nameLocalized ?? option.name_localized, + description: option.description, + [descriptionLocalizationsKey]: option.descriptionLocalizations ?? option.description_localizations, + [descriptionLocalizedKey]: option.descriptionLocalized ?? option.description_localized, + required: + option.required ?? + (option.type === ApplicationCommandOptionType.Subcommand || + option.type === ApplicationCommandOptionType.SubcommandGroup + ? undefined + : false), + autocomplete: option.autocomplete, + choices: option.choices?.map(choice => ({ + name: choice.name, + [nameLocalizedKey]: choice.nameLocalized ?? choice.name_localized, + [nameLocalizationsKey]: choice.nameLocalizations ?? choice.name_localizations, + value: choice.value, + })), + options: option.options?.map(o => this.transformOption(o, received)), + [channelTypesKey]: option.channelTypes ?? option.channel_types, + [minValueKey]: option.minValue ?? option.min_value, + [maxValueKey]: option.maxValue ?? option.max_value, + [minLengthKey]: option.minLength ?? option.min_length, + [maxLengthKey]: option.maxLength ?? option.max_length, + }; + } +} + +module.exports = ApplicationCommand; + +/* eslint-disable max-len */ +/** + * @external APIApplicationCommand + * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure} + */ + +/** + * @external APIApplicationCommandOption + * @see {@link https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure} + */ + +/** + * @external ApplicationCommandOptionAllowedChannelTypes + * @see {@link https://discord.js.org/docs/packages/builders/stable/ApplicationCommandOptionAllowedChannelTypes:TypeAlias} + */ diff --git a/node_modules/discord.js/src/structures/ApplicationRoleConnectionMetadata.js b/node_modules/discord.js/src/structures/ApplicationRoleConnectionMetadata.js new file mode 100644 index 0000000..7ed9b33 --- /dev/null +++ b/node_modules/discord.js/src/structures/ApplicationRoleConnectionMetadata.js @@ -0,0 +1,46 @@ +'use strict'; + +/** + * Role connection metadata object for an application. + */ +class ApplicationRoleConnectionMetadata { + constructor(data) { + /** + * The name of this metadata field + * @type {string} + */ + this.name = data.name; + + /** + * The name localizations for this metadata field + * @type {?Object<Locale, string>} + */ + this.nameLocalizations = data.name_localizations ?? null; + + /** + * The description of this metadata field + * @type {string} + */ + this.description = data.description; + + /** + * The description localizations for this metadata field + * @type {?Object<Locale, string>} + */ + this.descriptionLocalizations = data.description_localizations ?? null; + + /** + * The dictionary key for this metadata field + * @type {string} + */ + this.key = data.key; + + /** + * The type of this metadata field + * @type {ApplicationRoleConnectionMetadataType} + */ + this.type = data.type; + } +} + +exports.ApplicationRoleConnectionMetadata = ApplicationRoleConnectionMetadata; diff --git a/node_modules/discord.js/src/structures/Attachment.js b/node_modules/discord.js/src/structures/Attachment.js new file mode 100644 index 0000000..2576ff5 --- /dev/null +++ b/node_modules/discord.js/src/structures/Attachment.js @@ -0,0 +1,151 @@ +'use strict'; + +const AttachmentFlagsBitField = require('../util/AttachmentFlagsBitField.js'); +const { basename, flatten } = require('../util/Util'); + +/** + * @typedef {Object} AttachmentPayload + * @property {?string} name The name of the attachment + * @property {Stream|BufferResolvable} attachment The attachment in this payload + * @property {?string} description The description of the attachment + */ + +/** + * Represents an attachment + */ +class Attachment { + constructor(data) { + this.attachment = data.url; + /** + * The name of this attachment + * @type {string} + */ + this.name = data.filename; + this._patch(data); + } + + _patch(data) { + /** + * The attachment's id + * @type {Snowflake} + */ + this.id = data.id; + + if ('size' in data) { + /** + * The size of this attachment in bytes + * @type {number} + */ + this.size = data.size; + } + + if ('url' in data) { + /** + * The URL to this attachment + * @type {string} + */ + this.url = data.url; + } + + if ('proxy_url' in data) { + /** + * The Proxy URL to this attachment + * @type {string} + */ + this.proxyURL = data.proxy_url; + } + + if ('height' in data) { + /** + * The height of this attachment (if an image or video) + * @type {?number} + */ + this.height = data.height; + } else { + this.height ??= null; + } + + if ('width' in data) { + /** + * The width of this attachment (if an image or video) + * @type {?number} + */ + this.width = data.width; + } else { + this.width ??= null; + } + + if ('content_type' in data) { + /** + * The media type of this attachment + * @type {?string} + */ + this.contentType = data.content_type; + } else { + this.contentType ??= null; + } + + if ('description' in data) { + /** + * The description (alt text) of this attachment + * @type {?string} + */ + this.description = data.description; + } else { + this.description ??= null; + } + + /** + * Whether this attachment is ephemeral + * @type {boolean} + */ + this.ephemeral = data.ephemeral ?? false; + + if ('duration_secs' in data) { + /** + * The duration of this attachment in seconds + * <info>This will only be available if the attachment is an audio file.</info> + * @type {?number} + */ + this.duration = data.duration_secs; + } else { + this.duration ??= null; + } + + if ('waveform' in data) { + /** + * The base64 encoded byte array representing a sampled waveform + * <info>This will only be available if the attachment is an audio file.</info> + * @type {?string} + */ + this.waveform = data.waveform; + } else { + this.waveform ??= null; + } + + if ('flags' in data) { + /** + * The flags of this attachment + * @type {Readonly<AttachmentFlagsBitField>} + */ + this.flags = new AttachmentFlagsBitField(data.flags).freeze(); + } else { + this.flags ??= new AttachmentFlagsBitField().freeze(); + } + } + + /** + * Whether or not this attachment has been marked as a spoiler + * @type {boolean} + * @readonly + */ + get spoiler() { + return basename(this.url ?? this.name).startsWith('SPOILER_'); + } + + toJSON() { + return flatten(this); + } +} + +module.exports = Attachment; diff --git a/node_modules/discord.js/src/structures/AttachmentBuilder.js b/node_modules/discord.js/src/structures/AttachmentBuilder.js new file mode 100644 index 0000000..6c63810 --- /dev/null +++ b/node_modules/discord.js/src/structures/AttachmentBuilder.js @@ -0,0 +1,116 @@ +'use strict'; + +const { basename, flatten } = require('../util/Util'); + +/** + * Represents an attachment builder + */ +class AttachmentBuilder { + /** + * @param {BufferResolvable|Stream} attachment The file + * @param {AttachmentData} [data] Extra data + */ + constructor(attachment, data = {}) { + /** + * The file associated with this attachment. + * @type {BufferResolvable|Stream} + */ + this.attachment = attachment; + /** + * The name of this attachment + * @type {?string} + */ + this.name = data.name; + /** + * The description of the attachment + * @type {?string} + */ + this.description = data.description; + } + + /** + * Sets the description of this attachment. + * @param {string} description The description of the file + * @returns {AttachmentBuilder} This attachment + */ + setDescription(description) { + this.description = description; + return this; + } + + /** + * Sets the file of this attachment. + * @param {BufferResolvable|Stream} attachment The file + * @returns {AttachmentBuilder} This attachment + */ + setFile(attachment) { + this.attachment = attachment; + return this; + } + + /** + * Sets the name of this attachment. + * @param {string} name The name of the file + * @returns {AttachmentBuilder} This attachment + */ + setName(name) { + this.name = name; + return this; + } + + /** + * Sets whether this attachment is a spoiler + * @param {boolean} [spoiler=true] Whether the attachment should be marked as a spoiler + * @returns {AttachmentBuilder} This attachment + */ + setSpoiler(spoiler = true) { + if (spoiler === this.spoiler) return this; + + if (!spoiler) { + while (this.spoiler) { + this.name = this.name.slice('SPOILER_'.length); + } + return this; + } + this.name = `SPOILER_${this.name}`; + return this; + } + + /** + * Whether or not this attachment has been marked as a spoiler + * @type {boolean} + * @readonly + */ + get spoiler() { + return basename(this.name).startsWith('SPOILER_'); + } + + toJSON() { + return flatten(this); + } + + /** + * Makes a new builder instance from a preexisting attachment structure. + * @param {AttachmentBuilder|Attachment|AttachmentPayload} other The builder to construct a new instance from + * @returns {AttachmentBuilder} + */ + static from(other) { + return new AttachmentBuilder(other.attachment, { + name: other.name, + description: other.description, + }); + } +} + +module.exports = AttachmentBuilder; + +/** + * @external APIAttachment + * @see {@link https://discord.com/developers/docs/resources/channel#attachment-object} + */ + +/** + * @typedef {Object} AttachmentData + * @property {string} [name] The name of the attachment + * @property {string} [description] The description of the attachment + */ diff --git a/node_modules/discord.js/src/structures/AutoModerationActionExecution.js b/node_modules/discord.js/src/structures/AutoModerationActionExecution.js new file mode 100644 index 0000000..fcbc617 --- /dev/null +++ b/node_modules/discord.js/src/structures/AutoModerationActionExecution.js @@ -0,0 +1,116 @@ +'use strict'; + +const { _transformAPIAutoModerationAction } = require('../util/Transformers'); + +/** + * Represents the structure of an executed action when an {@link AutoModerationRule} is triggered. + */ +class AutoModerationActionExecution { + constructor(data, guild) { + /** + * The guild where this action was executed from. + * @type {Guild} + */ + this.guild = guild; + + /** + * The action that was executed. + * @type {AutoModerationAction} + */ + this.action = _transformAPIAutoModerationAction(data.action); + + /** + * The id of the auto moderation rule this action belongs to. + * @type {Snowflake} + */ + this.ruleId = data.rule_id; + + /** + * The trigger type of the auto moderation rule which was triggered. + * @type {AutoModerationRuleTriggerType} + */ + this.ruleTriggerType = data.rule_trigger_type; + + /** + * The id of the user that triggered this action. + * @type {Snowflake} + */ + this.userId = data.user_id; + + /** + * The id of the channel where this action was triggered from. + * @type {?Snowflake} + */ + this.channelId = data.channel_id ?? null; + + /** + * The id of the message that triggered this action. + * <info>This will not be present if the message was blocked or the content was not part of any message.</info> + * @type {?Snowflake} + */ + this.messageId = data.message_id ?? null; + + /** + * The id of any system auto moderation messages posted as a result of this action. + * @type {?Snowflake} + */ + this.alertSystemMessageId = data.alert_system_message_id ?? null; + + /** + * The content that triggered this action. + * <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged gateway intent.</info> + * @type {string} + */ + this.content = data.content; + + /** + * The word or phrase configured in the rule that triggered this action. + * @type {?string} + */ + this.matchedKeyword = data.matched_keyword ?? null; + + /** + * The substring in content that triggered this action. + * @type {?string} + */ + this.matchedContent = data.matched_content ?? null; + } + + /** + * The auto moderation rule this action belongs to. + * @type {?AutoModerationRule} + * @readonly + */ + get autoModerationRule() { + return this.guild.autoModerationRules.cache.get(this.ruleId) ?? null; + } + + /** + * The channel where this action was triggered from. + * @type {?(GuildTextBasedChannel|ForumChannel)} + * @readonly + */ + get channel() { + return this.guild.channels.cache.get(this.channelId) ?? null; + } + + /** + * The user that triggered this action. + * @type {?User} + * @readonly + */ + get user() { + return this.guild.client.users.cache.get(this.userId) ?? null; + } + + /** + * The guild member that triggered this action. + * @type {?GuildMember} + * @readonly + */ + get member() { + return this.guild.members.cache.get(this.userId) ?? null; + } +} + +module.exports = AutoModerationActionExecution; diff --git a/node_modules/discord.js/src/structures/AutoModerationRule.js b/node_modules/discord.js/src/structures/AutoModerationRule.js new file mode 100644 index 0000000..e87f547 --- /dev/null +++ b/node_modules/discord.js/src/structures/AutoModerationRule.js @@ -0,0 +1,284 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const Base = require('./Base'); +const { _transformAPIAutoModerationAction } = require('../util/Transformers'); + +/** + * Represents an auto moderation rule. + * @extends {Base} + */ +class AutoModerationRule extends Base { + constructor(client, data, guild) { + super(client); + + /** + * The id of this auto moderation rule. + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The guild this auto moderation rule is for. + * @type {Guild} + */ + this.guild = guild; + + /** + * The user that created this auto moderation rule. + * @type {Snowflake} + */ + this.creatorId = data.creator_id; + + /** + * The trigger type of this auto moderation rule. + * @type {AutoModerationRuleTriggerType} + */ + this.triggerType = data.trigger_type; + + this._patch(data); + } + + _patch(data) { + if ('name' in data) { + /** + * The name of this auto moderation rule. + * @type {string} + */ + this.name = data.name; + } + + if ('event_type' in data) { + /** + * The event type of this auto moderation rule. + * @type {AutoModerationRuleEventType} + */ + this.eventType = data.event_type; + } + + if ('trigger_metadata' in data) { + /** + * Additional data used to determine whether an auto moderation rule should be triggered. + * @typedef {Object} AutoModerationTriggerMetadata + * @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 mention raid protection is enabled + */ + + /** + * The trigger metadata of the rule. + * @type {AutoModerationTriggerMetadata} + */ + this.triggerMetadata = { + keywordFilter: data.trigger_metadata.keyword_filter ?? [], + regexPatterns: data.trigger_metadata.regex_patterns ?? [], + presets: data.trigger_metadata.presets ?? [], + allowList: data.trigger_metadata.allow_list ?? [], + mentionTotalLimit: data.trigger_metadata.mention_total_limit ?? null, + mentionRaidProtectionEnabled: data.trigger_metadata.mention_raid_protection_enabled ?? false, + }; + } + + if ('actions' in data) { + /** + * An object containing information about an auto moderation rule action. + * @typedef {Object} AutoModerationAction + * @property {AutoModerationActionType} type The type of this auto moderation rule action + * @property {AutoModerationActionMetadata} metadata Additional metadata needed during execution + */ + + /** + * Additional data used when an auto moderation rule is executed. + * @typedef {Object} AutoModerationActionMetadata + * @property {?Snowflake} channelId The id of 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 + */ + + /** + * The actions of this auto moderation rule. + * @type {AutoModerationAction[]} + */ + this.actions = data.actions.map(action => _transformAPIAutoModerationAction(action)); + } + + if ('enabled' in data) { + /** + * Whether this auto moderation rule is enabled. + * @type {boolean} + */ + this.enabled = data.enabled; + } + + if ('exempt_roles' in data) { + /** + * The roles exempt by this auto moderation rule. + * @type {Collection<Snowflake, Role>} + */ + this.exemptRoles = new Collection( + data.exempt_roles.map(exemptRole => [exemptRole, this.guild.roles.cache.get(exemptRole)]), + ); + } + + if ('exempt_channels' in data) { + /** + * The channels exempt by this auto moderation rule. + * @type {Collection<Snowflake, GuildChannel|ThreadChannel>} + */ + this.exemptChannels = new Collection( + data.exempt_channels.map(exemptChannel => [exemptChannel, this.guild.channels.cache.get(exemptChannel)]), + ); + } + } + + /** + * Edits this auto moderation rule. + * @param {AutoModerationRuleEditOptions} options Options for editing this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + edit(options) { + return this.guild.autoModerationRules.edit(this.id, options); + } + + /** + * Deletes this auto moderation rule. + * @param {string} [reason] The reason for deleting this auto moderation rule + * @returns {Promise<void>} + */ + delete(reason) { + return this.guild.autoModerationRules.delete(this.id, reason); + } + + /** + * Sets the name for this auto moderation rule. + * @param {string} name The name of this auto moderation rule + * @param {string} [reason] The reason for changing the name of this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + setName(name, reason) { + return this.edit({ name, reason }); + } + + /** + * Sets the event type for this auto moderation rule. + * @param {AutoModerationRuleEventType} eventType The event type of this auto moderation rule + * @param {string} [reason] The reason for changing the event type of this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + setEventType(eventType, reason) { + return this.edit({ eventType, reason }); + } + + /** + * Sets the keyword filter for this auto moderation rule. + * @param {string[]} keywordFilter The keyword filter of this auto moderation rule + * @param {string} [reason] The reason for changing the keyword filter of this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + setKeywordFilter(keywordFilter, reason) { + return this.edit({ triggerMetadata: { ...this.triggerMetadata, keywordFilter }, reason }); + } + + /** + * Sets the regular expression patterns for this auto moderation rule. + * @param {string[]} regexPatterns The regular expression patterns of this auto moderation rule + * <info>Only Rust-flavored regular expressions are supported.</info> + * @param {string} [reason] The reason for changing the regular expression patterns of this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + setRegexPatterns(regexPatterns, reason) { + return this.edit({ triggerMetadata: { ...this.triggerMetadata, regexPatterns }, reason }); + } + + /** + * Sets the presets for this auto moderation rule. + * @param {AutoModerationRuleKeywordPresetType[]} presets The presets of this auto moderation rule + * @param {string} [reason] The reason for changing the presets of this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + setPresets(presets, reason) { + return this.edit({ triggerMetadata: { ...this.triggerMetadata, presets }, reason }); + } + + /** + * Sets the allow list for this auto moderation rule. + * @param {string[]} allowList The substrings that will be exempt from triggering + * {@link AutoModerationRuleTriggerType.Keyword} and {@link AutoModerationRuleTriggerType.KeywordPreset} + * @param {string} [reason] The reason for changing the allow list of this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + setAllowList(allowList, reason) { + return this.edit({ triggerMetadata: { ...this.triggerMetadata, allowList }, reason }); + } + + /** + * Sets the mention total limit for this auto moderation rule. + * @param {number} mentionTotalLimit The total number of unique role and user mentions allowed per message + * @param {string} [reason] The reason for changing the mention total limit of this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + setMentionTotalLimit(mentionTotalLimit, reason) { + return this.edit({ triggerMetadata: { ...this.triggerMetadata, mentionTotalLimit }, reason }); + } + + /** + * Sets whether to enable mention raid protection for this auto moderation rule. + * @param {boolean} mentionRaidProtectionEnabled + * Whether to enable mention raid protection for this auto moderation rule + * @param {string} [reason] The reason for changing the mention raid protection of this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + setMentionRaidProtectionEnabled(mentionRaidProtectionEnabled, reason) { + return this.edit({ triggerMetadata: { ...this.triggerMetadata, mentionRaidProtectionEnabled }, reason }); + } + + /** + * Sets the actions for this auto moderation rule. + * @param {AutoModerationActionOptions[]} actions The actions of this auto moderation rule + * @param {string} [reason] The reason for changing the actions of this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + setActions(actions, reason) { + return this.edit({ actions, reason }); + } + + /** + * Sets whether this auto moderation rule should be enabled. + * @param {boolean} [enabled=true] Whether to enable this auto moderation rule + * @param {string} [reason] The reason for enabling or disabling this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + setEnabled(enabled = true, reason) { + return this.edit({ enabled, reason }); + } + + /** + * Sets the exempt roles for this auto moderation rule. + * @param {Collection<Snowflake, Role>|RoleResolvable[]} [exemptRoles] + * The roles that should not be affected by the auto moderation rule + * @param {string} [reason] The reason for changing the exempt roles of this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + setExemptRoles(exemptRoles, reason) { + return this.edit({ exemptRoles, reason }); + } + + /** + * Sets the exempt channels for this auto moderation rule. + * @param {Collection<Snowflake, GuildChannel|ThreadChannel>|GuildChannelResolvable[]} [exemptChannels] + * The channels that should not be affected by the auto moderation rule + * @param {string} [reason] The reason for changing the exempt channels of this auto moderation rule + * @returns {Promise<AutoModerationRule>} + */ + setExemptChannels(exemptChannels, reason) { + return this.edit({ exemptChannels, reason }); + } +} + +module.exports = AutoModerationRule; diff --git a/node_modules/discord.js/src/structures/AutocompleteInteraction.js b/node_modules/discord.js/src/structures/AutocompleteInteraction.js new file mode 100644 index 0000000..4b7e39e --- /dev/null +++ b/node_modules/discord.js/src/structures/AutocompleteInteraction.js @@ -0,0 +1,102 @@ +'use strict'; + +const { InteractionResponseType, Routes } = require('discord-api-types/v10'); +const BaseInteraction = require('./BaseInteraction'); +const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver'); +const { DiscordjsError, ErrorCodes } = require('../errors'); + +/** + * Represents an autocomplete interaction. + * @extends {BaseInteraction} + */ +class AutocompleteInteraction extends BaseInteraction { + constructor(client, data) { + super(client, data); + + /** + * The id of the channel this interaction was sent in + * @type {Snowflake} + * @name AutocompleteInteraction#channelId + */ + + /** + * The invoked application command's id + * @type {Snowflake} + */ + this.commandId = data.data.id; + + /** + * The invoked application command's name + * @type {string} + */ + this.commandName = data.data.name; + + /** + * The invoked application command's type + * @type {ApplicationCommandType} + */ + this.commandType = data.data.type; + + /** + * The id of the guild the invoked application command is registered to + * @type {?Snowflake} + */ + this.commandGuildId = data.data.guild_id ?? null; + + /** + * Whether this interaction has already received a response + * @type {boolean} + */ + this.responded = false; + + /** + * The options passed to the command + * @type {CommandInteractionOptionResolver} + */ + this.options = new CommandInteractionOptionResolver(this.client, data.data.options ?? []); + } + + /** + * The invoked application command, if it was fetched before + * @type {?ApplicationCommand} + */ + get command() { + const id = this.commandId; + return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null; + } + + /** + * Sends results for the autocomplete of this interaction. + * @param {ApplicationCommandOptionChoiceData[]} options The options for the autocomplete + * @returns {Promise<void>} + * @example + * // respond to autocomplete interaction + * interaction.respond([ + * { + * name: 'Option 1', + * value: 'option1', + * }, + * ]) + * .then(() => console.log('Successfully responded to the autocomplete interaction')) + * .catch(console.error); + */ + async respond(options) { + if (this.responded) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied); + + await this.client.rest.post(Routes.interactionCallback(this.id, this.token), { + body: { + type: InteractionResponseType.ApplicationCommandAutocompleteResult, + data: { + choices: options.map(({ nameLocalizations, ...option }) => ({ + ...this.client.options.jsonTransformer(option), + name_localizations: nameLocalizations, + })), + }, + }, + auth: false, + }); + this.responded = true; + } +} + +module.exports = AutocompleteInteraction; diff --git a/node_modules/discord.js/src/structures/Base.js b/node_modules/discord.js/src/structures/Base.js new file mode 100644 index 0000000..102fb21 --- /dev/null +++ b/node_modules/discord.js/src/structures/Base.js @@ -0,0 +1,43 @@ +'use strict'; + +const { flatten } = require('../util/Util'); + +/** + * Represents a data model that is identifiable by a Snowflake (i.e. Discord API data models). + * @abstract + */ +class Base { + constructor(client) { + /** + * The client that instantiated this + * @name Base#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + } + + _clone() { + return Object.assign(Object.create(this), this); + } + + _patch(data) { + return data; + } + + _update(data) { + const clone = this._clone(); + this._patch(data); + return clone; + } + + toJSON(...props) { + return flatten(this, ...props); + } + + valueOf() { + return this.id; + } +} + +module.exports = Base; diff --git a/node_modules/discord.js/src/structures/BaseChannel.js b/node_modules/discord.js/src/structures/BaseChannel.js new file mode 100644 index 0000000..346f763 --- /dev/null +++ b/node_modules/discord.js/src/structures/BaseChannel.js @@ -0,0 +1,155 @@ +'use strict'; + +const { channelLink } = require('@discordjs/builders'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { ChannelType, Routes } = require('discord-api-types/v10'); +const Base = require('./Base'); +const ChannelFlagsBitField = require('../util/ChannelFlagsBitField'); +const { ThreadChannelTypes } = require('../util/Constants'); + +/** + * Represents any channel on Discord. + * @extends {Base} + * @abstract + */ +class BaseChannel extends Base { + constructor(client, data, immediatePatch = true) { + super(client); + + /** + * The type of the channel + * @type {ChannelType} + */ + this.type = data.type; + + if (data && immediatePatch) this._patch(data); + } + + _patch(data) { + if ('flags' in data) { + /** + * The flags that are applied to the channel. + * <info>This is only `null` in a {@link PartialGroupDMChannel}. In all other cases, it is not `null`.</info> + * @type {?Readonly<ChannelFlagsBitField>} + */ + this.flags = new ChannelFlagsBitField(data.flags).freeze(); + } else { + this.flags ??= new ChannelFlagsBitField().freeze(); + } + + /** + * The channel's id + * @type {Snowflake} + */ + this.id = data.id; + } + + /** + * The timestamp the channel was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the channel was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The URL to the channel + * @type {string} + * @readonly + */ + get url() { + return this.isDMBased() ? channelLink(this.id) : channelLink(this.id, this.guildId); + } + + /** + * Whether this Channel is a partial + * <info>This is always false outside of DM channels.</info> + * @type {boolean} + * @readonly + */ + get partial() { + return false; + } + + /** + * When concatenated with a string, this automatically returns the channel's mention instead of the Channel object. + * @returns {string} + * @example + * // Logs: Hello from <#123456789012345678>! + * console.log(`Hello from ${channel}!`); + */ + toString() { + return `<#${this.id}>`; + } + + /** + * Deletes this channel. + * @returns {Promise<BaseChannel>} + * @example + * // Delete the channel + * channel.delete() + * .then(console.log) + * .catch(console.error); + */ + async delete() { + await this.client.rest.delete(Routes.channel(this.id)); + return this; + } + + /** + * Fetches this channel. + * @param {boolean} [force=true] Whether to skip the cache check and request the API + * @returns {Promise<BaseChannel>} + */ + fetch(force = true) { + return this.client.channels.fetch(this.id, { force }); + } + + /** + * Indicates whether this channel is a {@link ThreadChannel}. + * @returns {boolean} + */ + isThread() { + return ThreadChannelTypes.includes(this.type); + } + + /** + * Indicates whether this channel is {@link TextBasedChannels text-based}. + * @returns {boolean} + */ + isTextBased() { + return 'messages' in this; + } + + /** + * Indicates whether this channel is DM-based (either a {@link DMChannel} or a {@link PartialGroupDMChannel}). + * @returns {boolean} + */ + isDMBased() { + return [ChannelType.DM, ChannelType.GroupDM].includes(this.type); + } + + /** + * Indicates whether this channel is {@link BaseGuildVoiceChannel voice-based}. + * @returns {boolean} + */ + isVoiceBased() { + return 'bitrate' in this; + } + + toJSON(...props) { + return super.toJSON({ createdTimestamp: true }, ...props); + } +} + +exports.BaseChannel = BaseChannel; diff --git a/node_modules/discord.js/src/structures/BaseGuild.js b/node_modules/discord.js/src/structures/BaseGuild.js new file mode 100644 index 0000000..b12ca44 --- /dev/null +++ b/node_modules/discord.js/src/structures/BaseGuild.js @@ -0,0 +1,119 @@ +'use strict'; + +const { makeURLSearchParams } = require('@discordjs/rest'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { Routes, GuildFeature } = require('discord-api-types/v10'); +const Base = require('./Base'); + +/** + * The base class for {@link Guild}, {@link OAuth2Guild} and {@link InviteGuild}. + * @extends {Base} + * @abstract + */ +class BaseGuild extends Base { + constructor(client, data) { + super(client); + + /** + * The guild's id + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The name of this guild + * @type {string} + */ + this.name = data.name; + + /** + * The icon hash of this guild + * @type {?string} + */ + this.icon = data.icon; + + /** + * An array of features available to this guild + * @type {GuildFeature[]} + */ + this.features = data.features; + } + + /** + * The timestamp this guild was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time this guild was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The acronym that shows up in place of a guild icon + * @type {string} + * @readonly + */ + get nameAcronym() { + return this.name + .replace(/'s /g, ' ') + .replace(/\w+/g, e => e[0]) + .replace(/\s/g, ''); + } + + /** + * Whether this guild is partnered + * @type {boolean} + * @readonly + */ + get partnered() { + return this.features.includes(GuildFeature.Partnered); + } + + /** + * Whether this guild is verified + * @type {boolean} + * @readonly + */ + get verified() { + return this.features.includes(GuildFeature.Verified); + } + + /** + * The URL to this guild's icon. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + iconURL(options = {}) { + return this.icon && this.client.rest.cdn.icon(this.id, this.icon, options); + } + + /** + * Fetches this guild. + * @returns {Promise<Guild>} + */ + async fetch() { + const data = await this.client.rest.get(Routes.guild(this.id), { + query: makeURLSearchParams({ with_counts: true }), + }); + return this.client.guilds._add(data); + } + + /** + * When concatenated with a string, this automatically returns the guild's name instead of the Guild object. + * @returns {string} + */ + toString() { + return this.name; + } +} + +module.exports = BaseGuild; diff --git a/node_modules/discord.js/src/structures/BaseGuildEmoji.js b/node_modules/discord.js/src/structures/BaseGuildEmoji.js new file mode 100644 index 0000000..5a12bd9 --- /dev/null +++ b/node_modules/discord.js/src/structures/BaseGuildEmoji.js @@ -0,0 +1,56 @@ +'use strict'; + +const { Emoji } = require('./Emoji'); + +/** + * Parent class for {@link GuildEmoji} and {@link GuildPreviewEmoji}. + * @extends {Emoji} + * @abstract + */ +class BaseGuildEmoji extends Emoji { + constructor(client, data, guild) { + super(client, data); + + /** + * The guild this emoji is a part of + * @type {Guild|GuildPreview} + */ + this.guild = guild; + + this.requiresColons = null; + this.managed = null; + this.available = null; + + this._patch(data); + } + + _patch(data) { + if ('name' in data) this.name = data.name; + + if ('require_colons' in data) { + /** + * Whether or not this emoji requires colons surrounding it + * @type {?boolean} + */ + this.requiresColons = data.require_colons; + } + + if ('managed' in data) { + /** + * Whether this emoji is managed by an external service + * @type {?boolean} + */ + this.managed = data.managed; + } + + if ('available' in data) { + /** + * Whether this emoji is available + * @type {?boolean} + */ + this.available = data.available; + } + } +} + +module.exports = BaseGuildEmoji; diff --git a/node_modules/discord.js/src/structures/BaseGuildTextChannel.js b/node_modules/discord.js/src/structures/BaseGuildTextChannel.js new file mode 100644 index 0000000..f7d9d69 --- /dev/null +++ b/node_modules/discord.js/src/structures/BaseGuildTextChannel.js @@ -0,0 +1,186 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const GuildMessageManager = require('../managers/GuildMessageManager'); +const GuildTextThreadManager = require('../managers/GuildTextThreadManager'); + +/** + * Represents a text-based guild channel on Discord. + * @extends {GuildChannel} + * @implements {TextBasedChannel} + */ +class BaseGuildTextChannel extends GuildChannel { + constructor(guild, data, client) { + super(guild, data, client, false); + + /** + * A manager of the messages sent to this channel + * @type {GuildMessageManager} + */ + this.messages = new GuildMessageManager(this); + + /** + * A manager of the threads belonging to this channel + * @type {GuildTextThreadManager} + */ + this.threads = new GuildTextThreadManager(this); + + /** + * If the guild considers this channel NSFW + * @type {boolean} + */ + this.nsfw = Boolean(data.nsfw); + + this._patch(data); + } + + _patch(data) { + super._patch(data); + + if ('topic' in data) { + /** + * The topic of the text channel + * @type {?string} + */ + this.topic = data.topic; + } + + if ('nsfw' in data) { + this.nsfw = Boolean(data.nsfw); + } + + if ('last_message_id' in data) { + /** + * The last message id sent in the channel, if one was sent + * @type {?Snowflake} + */ + this.lastMessageId = data.last_message_id; + } + + if ('last_pin_timestamp' in data) { + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = data.last_pin_timestamp ? Date.parse(data.last_pin_timestamp) : null; + } + + if ('default_auto_archive_duration' in data) { + /** + * The default auto archive duration for newly created threads in this channel + * @type {?ThreadAutoArchiveDuration} + */ + this.defaultAutoArchiveDuration = data.default_auto_archive_duration; + } + + if ('messages' in data) { + for (const message of data.messages) this.messages._add(message); + } + } + + /** + * Sets the default auto archive duration for all newly created threads in this channel. + * @param {ThreadAutoArchiveDuration} defaultAutoArchiveDuration The new default auto archive duration + * @param {string} [reason] Reason for changing the channel's default auto archive duration + * @returns {Promise<TextChannel>} + */ + setDefaultAutoArchiveDuration(defaultAutoArchiveDuration, reason) { + return this.edit({ defaultAutoArchiveDuration, reason }); + } + + /** + * Sets the type of this channel. + * <info>Only conversion between {@link TextChannel} and {@link NewsChannel} is supported.</info> + * @param {ChannelType.GuildText|ChannelType.GuildAnnouncement} type The new channel type + * @param {string} [reason] Reason for changing the channel's type + * @returns {Promise<GuildChannel>} + */ + setType(type, reason) { + return this.edit({ type, reason }); + } + + /** + * Sets a new topic for the guild channel. + * @param {?string} topic The new topic for the guild channel + * @param {string} [reason] Reason for changing the guild channel's topic + * @returns {Promise<GuildChannel>} + * @example + * // Set a new channel topic + * channel.setTopic('needs more rate limiting') + * .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`)) + * .catch(console.error); + */ + setTopic(topic, reason) { + return this.edit({ topic, reason }); + } + + /** + * Data that can be resolved to an Application. This can be: + * * An Application + * * An Activity with associated Application + * * A Snowflake + * @typedef {Application|Snowflake} ApplicationResolvable + */ + + /** + * Options used to create an invite to a guild channel. + * @typedef {Object} InviteCreateOptions + * @property {boolean} [temporary] Whether members that joined via the invite should be automatically + * kicked after 24 hours if they have not yet received a role + * @property {number} [maxAge] How long the invite should last (in seconds, 0 for forever) + * @property {number} [maxUses] Maximum number of uses + * @property {boolean} [unique] Create a unique invite, or use an existing one with similar settings + * @property {UserResolvable} [targetUser] The user whose stream to display for this invite, + * required if `targetType` is {@link InviteTargetType.Stream}, the user must be streaming in the channel + * @property {ApplicationResolvable} [targetApplication] The embedded application to open for this invite, + * required if `targetType` is {@link InviteTargetType.Stream}, the application must have the + * {@link InviteTargetType.EmbeddedApplication} flag + * @property {InviteTargetType} [targetType] The type of the target for this voice channel invite + * @property {string} [reason] The reason for creating the invite + */ + + /** + * Creates an invite to this guild channel. + * @param {InviteCreateOptions} [options={}] The options for creating the invite + * @returns {Promise<Invite>} + * @example + * // Create an invite to a channel + * channel.createInvite() + * .then(invite => console.log(`Created an invite with a code of ${invite.code}`)) + * .catch(console.error); + */ + createInvite(options) { + return this.guild.invites.create(this.id, options); + } + + /** + * Fetches a collection of invites to this guild channel. + * Resolves with a collection mapping invites by their codes. + * @param {boolean} [cache=true] Whether or not to cache the fetched invites + * @returns {Promise<Collection<string, Invite>>} + */ + fetchInvites(cache = true) { + return this.guild.invites.fetch({ channelId: this.id, cache }); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + get lastMessage() {} + get lastPinAt() {} + send() {} + sendTyping() {} + createMessageCollector() {} + awaitMessages() {} + createMessageComponentCollector() {} + awaitMessageComponent() {} + bulkDelete() {} + fetchWebhooks() {} + createWebhook() {} + setRateLimitPerUser() {} + setNSFW() {} +} + +TextBasedChannel.applyToClass(BaseGuildTextChannel, true); + +module.exports = BaseGuildTextChannel; diff --git a/node_modules/discord.js/src/structures/BaseGuildVoiceChannel.js b/node_modules/discord.js/src/structures/BaseGuildVoiceChannel.js new file mode 100644 index 0000000..220ac6c --- /dev/null +++ b/node_modules/discord.js/src/structures/BaseGuildVoiceChannel.js @@ -0,0 +1,234 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { PermissionFlagsBits } = require('discord-api-types/v10'); +const GuildChannel = require('./GuildChannel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const GuildMessageManager = require('../managers/GuildMessageManager'); + +/** + * Represents a voice-based guild channel on Discord. + * @extends {GuildChannel} + * @implements {TextBasedChannel} + */ +class BaseGuildVoiceChannel extends GuildChannel { + constructor(guild, data, client) { + super(guild, data, client, false); + /** + * A manager of the messages sent to this channel + * @type {GuildMessageManager} + */ + this.messages = new GuildMessageManager(this); + + /** + * If the guild considers this channel NSFW + * @type {boolean} + */ + this.nsfw = Boolean(data.nsfw); + + this._patch(data); + } + + _patch(data) { + super._patch(data); + + if ('rtc_region' in data) { + /** + * The RTC region for this voice-based channel. This region is automatically selected if `null`. + * @type {?string} + */ + this.rtcRegion = data.rtc_region; + } + + if ('bitrate' in data) { + /** + * The bitrate of this voice-based channel + * @type {number} + */ + this.bitrate = data.bitrate; + } + + if ('user_limit' in data) { + /** + * The maximum amount of users allowed in this channel. + * @type {number} + */ + this.userLimit = data.user_limit; + } + + if ('video_quality_mode' in data) { + /** + * The camera video quality mode of the channel. + * @type {?VideoQualityMode} + */ + this.videoQualityMode = data.video_quality_mode; + } else { + this.videoQualityMode ??= null; + } + + if ('last_message_id' in data) { + /** + * The last message id sent in the channel, if one was sent + * @type {?Snowflake} + */ + this.lastMessageId = data.last_message_id; + } + + if ('messages' in data) { + for (const message of data.messages) this.messages._add(message); + } + + if ('rate_limit_per_user' in data) { + /** + * The rate limit per user (slowmode) for this channel in seconds + * @type {number} + */ + this.rateLimitPerUser = data.rate_limit_per_user; + } + + if ('nsfw' in data) { + this.nsfw = data.nsfw; + } + } + + /** + * The members in this voice-based channel + * @type {Collection<Snowflake, GuildMember>} + * @readonly + */ + get members() { + const coll = new Collection(); + for (const state of this.guild.voiceStates.cache.values()) { + if (state.channelId === this.id && state.member) { + coll.set(state.id, state.member); + } + } + return coll; + } + + /** + * Checks if the voice-based channel is full + * @type {boolean} + * @readonly + */ + get full() { + return this.userLimit > 0 && this.members.size >= this.userLimit; + } + + /** + * Whether the channel is joinable by the client user + * @type {boolean} + * @readonly + */ + get joinable() { + if (!this.viewable) return false; + const permissions = this.permissionsFor(this.client.user); + if (!permissions) return false; + + // This flag allows joining even if timed out + if (permissions.has(PermissionFlagsBits.Administrator, false)) return true; + + return ( + this.guild.members.me.communicationDisabledUntilTimestamp < Date.now() && + permissions.has(PermissionFlagsBits.Connect, false) + ); + } + + /** + * Creates an invite to this guild channel. + * @param {InviteCreateOptions} [options={}] The options for creating the invite + * @returns {Promise<Invite>} + * @example + * // Create an invite to a channel + * channel.createInvite() + * .then(invite => console.log(`Created an invite with a code of ${invite.code}`)) + * .catch(console.error); + */ + createInvite(options) { + return this.guild.invites.create(this.id, options); + } + + /** + * Fetches a collection of invites to this guild channel. + * @param {boolean} [cache=true] Whether to cache the fetched invites + * @returns {Promise<Collection<string, Invite>>} + */ + fetchInvites(cache = true) { + return this.guild.invites.fetch({ channelId: this.id, cache }); + } + + /** + * Sets the bitrate of the channel. + * @param {number} bitrate The new bitrate + * @param {string} [reason] Reason for changing the channel's bitrate + * @returns {Promise<BaseGuildVoiceChannel>} + * @example + * // Set the bitrate of a voice channel + * channel.setBitrate(48_000) + * .then(channel => console.log(`Set bitrate to ${channel.bitrate}bps for ${channel.name}`)) + * .catch(console.error); + */ + setBitrate(bitrate, reason) { + return this.edit({ bitrate, reason }); + } + + /** + * Sets the RTC region of the channel. + * @param {?string} rtcRegion The new region of the channel. Set to `null` to remove a specific region for the channel + * @param {string} [reason] The reason for modifying this region. + * @returns {Promise<BaseGuildVoiceChannel>} + * @example + * // Set the RTC region to sydney + * channel.setRTCRegion('sydney'); + * @example + * // Remove a fixed region for this channel - let Discord decide automatically + * channel.setRTCRegion(null, 'We want to let Discord decide.'); + */ + setRTCRegion(rtcRegion, reason) { + return this.edit({ rtcRegion, reason }); + } + + /** + * Sets the user limit of the channel. + * @param {number} userLimit The new user limit + * @param {string} [reason] Reason for changing the user limit + * @returns {Promise<BaseGuildVoiceChannel>} + * @example + * // Set the user limit of a voice channel + * channel.setUserLimit(42) + * .then(channel => console.log(`Set user limit to ${channel.userLimit} for ${channel.name}`)) + * .catch(console.error); + */ + setUserLimit(userLimit, reason) { + return this.edit({ userLimit, reason }); + } + + /** + * Sets the camera video quality mode of the channel. + * @param {VideoQualityMode} videoQualityMode The new camera video quality mode. + * @param {string} [reason] Reason for changing the camera video quality mode. + * @returns {Promise<BaseGuildVoiceChannel>} + */ + setVideoQualityMode(videoQualityMode, reason) { + return this.edit({ videoQualityMode, reason }); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + get lastMessage() {} + send() {} + sendTyping() {} + createMessageCollector() {} + awaitMessages() {} + createMessageComponentCollector() {} + awaitMessageComponent() {} + bulkDelete() {} + fetchWebhooks() {} + createWebhook() {} + setRateLimitPerUser() {} + setNSFW() {} +} + +TextBasedChannel.applyToClass(BaseGuildVoiceChannel, true, ['lastPinAt']); + +module.exports = BaseGuildVoiceChannel; diff --git a/node_modules/discord.js/src/structures/BaseInteraction.js b/node_modules/discord.js/src/structures/BaseInteraction.js new file mode 100644 index 0000000..967350f --- /dev/null +++ b/node_modules/discord.js/src/structures/BaseInteraction.js @@ -0,0 +1,344 @@ +'use strict'; + +const { deprecate } = require('node:util'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { InteractionType, ApplicationCommandType, ComponentType } = require('discord-api-types/v10'); +const Base = require('./Base'); +const { SelectMenuTypes } = require('../util/Constants'); +const PermissionsBitField = require('../util/PermissionsBitField'); + +/** + * Represents an interaction. + * @extends {Base} + * @abstract + */ +class BaseInteraction extends Base { + constructor(client, data) { + super(client); + + /** + * The interaction's type + * @type {InteractionType} + */ + this.type = data.type; + + /** + * The interaction's id + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The interaction's token + * @type {string} + * @name BaseInteraction#token + * @readonly + */ + Object.defineProperty(this, 'token', { value: data.token }); + + /** + * The application's id + * @type {Snowflake} + */ + this.applicationId = data.application_id; + + /** + * The id of the channel this interaction was sent in + * @type {?Snowflake} + */ + this.channelId = data.channel?.id ?? null; + + /** + * The id of the guild this interaction was sent in + * @type {?Snowflake} + */ + this.guildId = data.guild_id ?? null; + + /** + * The user who created this interaction + * @type {User} + */ + this.user = this.client.users._add(data.user ?? data.member.user); + + /** + * If this interaction was sent in a guild, the member which sent it + * @type {?(GuildMember|APIGuildMember)} + */ + this.member = data.member ? this.guild?.members._add(data.member) ?? data.member : null; + + /** + * The version + * @type {number} + */ + this.version = data.version; + + /** + * Set of permissions the application or bot has within the channel the interaction was sent from + * @type {?Readonly<PermissionsBitField>} + */ + this.appPermissions = data.app_permissions ? new PermissionsBitField(data.app_permissions).freeze() : null; + + /** + * The permissions of the member, if one exists, in the channel this interaction was executed in + * @type {?Readonly<PermissionsBitField>} + */ + this.memberPermissions = data.member?.permissions + ? new PermissionsBitField(data.member.permissions).freeze() + : null; + + /** + * A Discord locale string, possible values are: + * * en-US (English, US) + * * en-GB (English, UK) + * * bg (Bulgarian) + * * zh-CN (Chinese, China) + * * zh-TW (Chinese, Taiwan) + * * hr (Croatian) + * * cs (Czech) + * * da (Danish) + * * nl (Dutch) + * * fi (Finnish) + * * fr (French) + * * de (German) + * * el (Greek) + * * hi (Hindi) + * * hu (Hungarian) + * * it (Italian) + * * ja (Japanese) + * * ko (Korean) + * * lt (Lithuanian) + * * no (Norwegian) + * * pl (Polish) + * * pt-BR (Portuguese, Brazilian) + * * ro (Romanian, Romania) + * * ru (Russian) + * * es-ES (Spanish) + * * sv-SE (Swedish) + * * th (Thai) + * * tr (Turkish) + * * uk (Ukrainian) + * * vi (Vietnamese) + * @see {@link https://discord.com/developers/docs/reference#locales} + * @typedef {string} Locale + */ + + /** + * The locale of the user who invoked this interaction + * @type {Locale} + */ + this.locale = data.locale; + + /** + * The preferred locale from the guild this interaction was sent in + * @type {?Locale} + */ + this.guildLocale = data.guild_locale ?? null; + } + + /** + * The timestamp the interaction was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the interaction was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The channel this interaction was sent in + * @type {?TextBasedChannels} + * @readonly + */ + get channel() { + return this.client.channels.cache.get(this.channelId) ?? null; + } + + /** + * The guild this interaction was sent in + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.cache.get(this.guildId) ?? null; + } + + /** + * Indicates whether this interaction is received from a guild. + * @returns {boolean} + */ + inGuild() { + return Boolean(this.guildId && this.member); + } + + /** + * Indicates whether or not this interaction is both cached and received from a guild. + * @returns {boolean} + */ + inCachedGuild() { + return Boolean(this.guild && this.member); + } + + /** + * Indicates whether or not this interaction is received from an uncached guild. + * @returns {boolean} + */ + inRawGuild() { + return Boolean(this.guildId && !this.guild && this.member); + } + + /** + * Indicates whether this interaction is an {@link AutocompleteInteraction} + * @returns {boolean} + */ + isAutocomplete() { + return this.type === InteractionType.ApplicationCommandAutocomplete; + } + + /** + * Indicates whether this interaction is a {@link CommandInteraction} + * @returns {boolean} + */ + isCommand() { + return this.type === InteractionType.ApplicationCommand; + } + + /** + * Indicates whether this interaction is a {@link ChatInputCommandInteraction}. + * @returns {boolean} + */ + isChatInputCommand() { + return this.type === InteractionType.ApplicationCommand && this.commandType === ApplicationCommandType.ChatInput; + } + + /** + * Indicates whether this interaction is a {@link ContextMenuCommandInteraction} + * @returns {boolean} + */ + isContextMenuCommand() { + return ( + this.type === InteractionType.ApplicationCommand && + [ApplicationCommandType.User, ApplicationCommandType.Message].includes(this.commandType) + ); + } + + /** + * Indicates whether this interaction is a {@link MessageComponentInteraction} + * @returns {boolean} + */ + isMessageComponent() { + return this.type === InteractionType.MessageComponent; + } + + /** + * Indicates whether this interaction is a {@link ModalSubmitInteraction} + * @returns {boolean} + */ + isModalSubmit() { + return this.type === InteractionType.ModalSubmit; + } + + /** + * Indicates whether this interaction is a {@link UserContextMenuCommandInteraction} + * @returns {boolean} + */ + isUserContextMenuCommand() { + return this.isContextMenuCommand() && this.commandType === ApplicationCommandType.User; + } + + /** + * Indicates whether this interaction is a {@link MessageContextMenuCommandInteraction} + * @returns {boolean} + */ + isMessageContextMenuCommand() { + return this.isContextMenuCommand() && this.commandType === ApplicationCommandType.Message; + } + + /** + * Indicates whether this interaction is a {@link ButtonInteraction}. + * @returns {boolean} + */ + isButton() { + return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.Button; + } + + /** + * Indicates whether this interaction is a {@link StringSelectMenuInteraction}. + * @returns {boolean} + * @deprecated Use {@link BaseInteraction#isStringSelectMenu} instead. + */ + isSelectMenu() { + return this.isStringSelectMenu(); + } + + /** + * Indicates whether this interaction is a select menu of any known type. + * @returns {boolean} + */ + isAnySelectMenu() { + return this.type === InteractionType.MessageComponent && SelectMenuTypes.includes(this.componentType); + } + + /** + * Indicates whether this interaction is a {@link StringSelectMenuInteraction}. + * @returns {boolean} + */ + isStringSelectMenu() { + return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.StringSelect; + } + + /** + * Indicates whether this interaction is a {@link UserSelectMenuInteraction} + * @returns {boolean} + */ + isUserSelectMenu() { + return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.UserSelect; + } + + /** + * Indicates whether this interaction is a {@link RoleSelectMenuInteraction} + * @returns {boolean} + */ + isRoleSelectMenu() { + return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.RoleSelect; + } + + /** + * Indicates whether this interaction is a {@link ChannelSelectMenuInteraction} + * @returns {boolean} + */ + isChannelSelectMenu() { + return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.ChannelSelect; + } + + /** + * Indicates whether this interaction is a {@link MentionableSelectMenuInteraction} + * @returns {boolean} + */ + isMentionableSelectMenu() { + return this.type === InteractionType.MessageComponent && this.componentType === ComponentType.MentionableSelect; + } + + /** + * Indicates whether this interaction can be replied to. + * @returns {boolean} + */ + isRepliable() { + return ![InteractionType.Ping, InteractionType.ApplicationCommandAutocomplete].includes(this.type); + } +} + +BaseInteraction.prototype.isSelectMenu = deprecate( + BaseInteraction.prototype.isSelectMenu, + 'BaseInteraction#isSelectMenu() is deprecated. Use BaseInteraction#isStringSelectMenu() instead.', +); + +module.exports = BaseInteraction; diff --git a/node_modules/discord.js/src/structures/BaseSelectMenuComponent.js b/node_modules/discord.js/src/structures/BaseSelectMenuComponent.js new file mode 100644 index 0000000..7177f43 --- /dev/null +++ b/node_modules/discord.js/src/structures/BaseSelectMenuComponent.js @@ -0,0 +1,56 @@ +'use strict'; + +const Component = require('./Component'); + +/** + * Represents a select menu component + * @extends {Component} + */ +class BaseSelectMenuComponent extends Component { + /** + * The placeholder for this select menu + * @type {?string} + * @readonly + */ + get placeholder() { + return this.data.placeholder ?? null; + } + + /** + * The maximum amount of options that can be selected + * @type {?number} + * @readonly + */ + get maxValues() { + return this.data.max_values ?? null; + } + + /** + * The minimum amount of options that must be selected + * @type {?number} + * @readonly + */ + get minValues() { + return this.data.min_values ?? null; + } + + /** + * The custom id of this select menu + * @type {string} + * @readonly + */ + get customId() { + return this.data.custom_id; + } + + /** + * Whether this select menu is disabled + * @type {boolean} + * @readonly + */ + get disabled() { + return this.data.disabled ?? false; + } +} + +module.exports = BaseSelectMenuComponent; diff --git a/node_modules/discord.js/src/structures/ButtonBuilder.js b/node_modules/discord.js/src/structures/ButtonBuilder.js new file mode 100644 index 0000000..ada4188 --- /dev/null +++ b/node_modules/discord.js/src/structures/ButtonBuilder.js @@ -0,0 +1,44 @@ +'use strict'; + +const { ButtonBuilder: BuildersButton } = require('@discordjs/builders'); +const { isJSONEncodable } = require('@discordjs/util'); +const { toSnakeCase } = require('../util/Transformers'); +const { resolvePartialEmoji } = require('../util/Util'); + +/** + * Represents a button builder. + * @extends {BuildersButton} + */ +class ButtonBuilder extends BuildersButton { + constructor({ emoji, ...data } = {}) { + super(toSnakeCase({ ...data, emoji: emoji && typeof emoji === 'string' ? resolvePartialEmoji(emoji) : emoji })); + } + + /** + * Sets the emoji to display on this button + * @param {string|APIMessageComponentEmoji} emoji The emoji to display on this button + * @returns {ButtonBuilder} + */ + setEmoji(emoji) { + if (typeof emoji === 'string') { + return super.setEmoji(resolvePartialEmoji(emoji)); + } + return super.setEmoji(emoji); + } + + /** + * Creates a new button builder from JSON data + * @param {ButtonBuilder|ButtonComponent|APIButtonComponent} other The other data + * @returns {ButtonBuilder} + */ + static from(other) { + return new this(isJSONEncodable(other) ? other.toJSON() : other); + } +} + +module.exports = ButtonBuilder; + +/** + * @external BuildersButton + * @see {@link https://discord.js.org/docs/packages/builders/stable/ButtonBuilder:Class} + */ diff --git a/node_modules/discord.js/src/structures/ButtonComponent.js b/node_modules/discord.js/src/structures/ButtonComponent.js new file mode 100644 index 0000000..7319c3a --- /dev/null +++ b/node_modules/discord.js/src/structures/ButtonComponent.js @@ -0,0 +1,65 @@ +'use strict'; + +const Component = require('./Component'); + +/** + * Represents a button component + * @extends {Component} + */ +class ButtonComponent extends Component { + /** + * The style of this button + * @type {ButtonStyle} + * @readonly + */ + get style() { + return this.data.style; + } + + /** + * The label of this button + * @type {?string} + * @readonly + */ + get label() { + return this.data.label ?? null; + } + + /** + * The emoji used in this button + * @type {?APIMessageComponentEmoji} + * @readonly + */ + get emoji() { + return this.data.emoji ?? null; + } + + /** + * Whether this button is disabled + * @type {boolean} + * @readonly + */ + get disabled() { + return this.data.disabled ?? false; + } + + /** + * The custom id of this button (only defined on non-link buttons) + * @type {?string} + * @readonly + */ + get customId() { + return this.data.custom_id ?? null; + } + + /** + * The URL of this button (only defined on link buttons) + * @type {?string} + * @readonly + */ + get url() { + return this.data.url ?? null; + } +} + +module.exports = ButtonComponent; diff --git a/node_modules/discord.js/src/structures/ButtonInteraction.js b/node_modules/discord.js/src/structures/ButtonInteraction.js new file mode 100644 index 0000000..db57592 --- /dev/null +++ b/node_modules/discord.js/src/structures/ButtonInteraction.js @@ -0,0 +1,11 @@ +'use strict'; + +const MessageComponentInteraction = require('./MessageComponentInteraction'); + +/** + * Represents a button interaction. + * @extends {MessageComponentInteraction} + */ +class ButtonInteraction extends MessageComponentInteraction {} + +module.exports = ButtonInteraction; diff --git a/node_modules/discord.js/src/structures/CategoryChannel.js b/node_modules/discord.js/src/structures/CategoryChannel.js new file mode 100644 index 0000000..d038044 --- /dev/null +++ b/node_modules/discord.js/src/structures/CategoryChannel.js @@ -0,0 +1,45 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); +const CategoryChannelChildManager = require('../managers/CategoryChannelChildManager'); + +/** + * Represents a guild category channel on Discord. + * @extends {GuildChannel} + */ +class CategoryChannel extends GuildChannel { + /** + * The id of the parent of this channel. + * @name CategoryChannel#parentId + * @type {null} + */ + + /** + * The parent of this channel. + * @name CategoryChannel#parent + * @type {null} + * @readonly + */ + + /** + * Sets the category parent of this channel. + * <warn>It is not possible to set the parent of a CategoryChannel.</warn> + * @method setParent + * @memberof CategoryChannel + * @instance + * @param {?CategoryChannelResolvable} channel The channel to set as parent + * @param {SetParentOptions} [options={}] The options for setting the parent + * @returns {Promise<GuildChannel>} + */ + + /** + * A manager of the channels belonging to this category + * @type {CategoryChannelChildManager} + * @readonly + */ + get children() { + return new CategoryChannelChildManager(this); + } +} + +module.exports = CategoryChannel; diff --git a/node_modules/discord.js/src/structures/ChannelSelectMenuBuilder.js b/node_modules/discord.js/src/structures/ChannelSelectMenuBuilder.js new file mode 100644 index 0000000..6d99474 --- /dev/null +++ b/node_modules/discord.js/src/structures/ChannelSelectMenuBuilder.js @@ -0,0 +1,31 @@ +'use strict'; + +const { ChannelSelectMenuBuilder: BuildersChannelSelectMenu } = require('@discordjs/builders'); +const { isJSONEncodable } = require('@discordjs/util'); +const { toSnakeCase } = require('../util/Transformers'); + +/** + * Class used to build select menu components to be sent through the API + * @extends {BuildersChannelSelectMenu} + */ +class ChannelSelectMenuBuilder extends BuildersChannelSelectMenu { + constructor(data = {}) { + super(toSnakeCase(data)); + } + + /** + * Creates a new select menu builder from JSON data + * @param {ChannelSelectMenuBuilder|ChannelSelectMenuComponent|APIChannelSelectComponent} other The other data + * @returns {ChannelSelectMenuBuilder} + */ + static from(other) { + return new this(isJSONEncodable(other) ? other.toJSON() : other); + } +} + +module.exports = ChannelSelectMenuBuilder; + +/** + * @external BuildersChannelSelectMenu + * @see {@link https://discord.js.org/docs/packages/builders/stable/ChannelSelectMenuBuilder:Class} + */ diff --git a/node_modules/discord.js/src/structures/ChannelSelectMenuComponent.js b/node_modules/discord.js/src/structures/ChannelSelectMenuComponent.js new file mode 100644 index 0000000..90a7063 --- /dev/null +++ b/node_modules/discord.js/src/structures/ChannelSelectMenuComponent.js @@ -0,0 +1,20 @@ +'use strict'; + +const BaseSelectMenuComponent = require('./BaseSelectMenuComponent'); + +/** + * Represents a channel select menu component + * @extends {BaseSelectMenuComponent} + */ +class ChannelSelectMenuComponent extends BaseSelectMenuComponent { + /** + * The options in this select menu + * @type {?(ChannelType[])} + * @readonly + */ + get channelTypes() { + return this.data.channel_types ?? null; + } +} + +module.exports = ChannelSelectMenuComponent; diff --git a/node_modules/discord.js/src/structures/ChannelSelectMenuInteraction.js b/node_modules/discord.js/src/structures/ChannelSelectMenuInteraction.js new file mode 100644 index 0000000..a5e9c99 --- /dev/null +++ b/node_modules/discord.js/src/structures/ChannelSelectMenuInteraction.js @@ -0,0 +1,33 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const MessageComponentInteraction = require('./MessageComponentInteraction'); + +/** + * Represents a {@link ComponentType.ChannelSelect} select menu interaction. + * @extends {MessageComponentInteraction} + */ +class ChannelSelectMenuInteraction extends MessageComponentInteraction { + constructor(client, data) { + super(client, data); + const { resolved, values } = data.data; + + /** + * An array of the selected channel ids + * @type {Snowflake[]} + */ + this.values = values ?? []; + + /** + * Collection of the selected channels + * @type {Collection<Snowflake, BaseChannel|APIChannel>} + */ + this.channels = new Collection(); + + for (const channel of Object.values(resolved?.channels ?? {})) { + this.channels.set(channel.id, this.client.channels._add(channel, this.guild) ?? channel); + } + } +} + +module.exports = ChannelSelectMenuInteraction; diff --git a/node_modules/discord.js/src/structures/ChatInputCommandInteraction.js b/node_modules/discord.js/src/structures/ChatInputCommandInteraction.js new file mode 100644 index 0000000..35175e4 --- /dev/null +++ b/node_modules/discord.js/src/structures/ChatInputCommandInteraction.js @@ -0,0 +1,41 @@ +'use strict'; + +const CommandInteraction = require('./CommandInteraction'); +const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver'); + +/** + * Represents a command interaction. + * @extends {CommandInteraction} + */ +class ChatInputCommandInteraction extends CommandInteraction { + constructor(client, data) { + super(client, data); + + /** + * The options passed to the command. + * @type {CommandInteractionOptionResolver} + */ + this.options = new CommandInteractionOptionResolver( + this.client, + data.data.options?.map(option => this.transformOption(option, data.data.resolved)) ?? [], + this.transformResolved(data.data.resolved ?? {}), + ); + } + + /** + * Returns a string representation of the command interaction. + * This can then be copied by a user and executed again in a new command while keeping the option order. + * @returns {string} + */ + toString() { + const properties = [ + this.commandName, + this.options._group, + this.options._subcommand, + ...this.options._hoistedOptions.map(o => `${o.name}:${o.value}`), + ]; + return `/${properties.filter(Boolean).join(' ')}`; + } +} + +module.exports = ChatInputCommandInteraction; diff --git a/node_modules/discord.js/src/structures/ClientApplication.js b/node_modules/discord.js/src/structures/ClientApplication.js new file mode 100644 index 0000000..69f5134 --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientApplication.js @@ -0,0 +1,222 @@ +'use strict'; + +const { Routes } = require('discord-api-types/v10'); +const { ApplicationRoleConnectionMetadata } = require('./ApplicationRoleConnectionMetadata'); +const Team = require('./Team'); +const Application = require('./interfaces/Application'); +const ApplicationCommandManager = require('../managers/ApplicationCommandManager'); +const ApplicationFlagsBitField = require('../util/ApplicationFlagsBitField'); +const PermissionsBitField = require('../util/PermissionsBitField'); + +/** + * @typedef {Object} ClientApplicationInstallParams + * @property {OAuth2Scopes[]} scopes The scopes to add the application to the server with + * @property {Readonly<PermissionsBitField>} permissions The permissions this bot will request upon joining + */ + +/** + * Represents a client application. + * @extends {Application} + */ +class ClientApplication extends Application { + constructor(client, data) { + super(client, data); + + /** + * The application command manager for this application + * @type {ApplicationCommandManager} + */ + this.commands = new ApplicationCommandManager(this.client); + } + + _patch(data) { + super._patch(data); + + /** + * The tags this application has (max of 5) + * @type {string[]} + */ + this.tags = data.tags ?? []; + + if ('install_params' in data) { + /** + * Settings for this application's default in-app authorization + * @type {?ClientApplicationInstallParams} + */ + this.installParams = { + scopes: data.install_params.scopes, + permissions: new PermissionsBitField(data.install_params.permissions).freeze(), + }; + } else { + this.installParams ??= null; + } + + if ('custom_install_url' in data) { + /** + * This application's custom installation URL + * @type {?string} + */ + this.customInstallURL = data.custom_install_url; + } else { + this.customInstallURL = null; + } + + if ('flags' in data) { + /** + * The flags this application has + * @type {ApplicationFlagsBitField} + */ + this.flags = new ApplicationFlagsBitField(data.flags).freeze(); + } + + if ('approximate_guild_count' in data) { + /** + * An approximate amount of guilds this application is in. + * @type {?number} + */ + this.approximateGuildCount = data.approximate_guild_count; + } else { + this.approximateGuildCount ??= null; + } + + if ('guild_id' in data) { + /** + * The id of the guild associated with this application. + * @type {?Snowflake} + */ + this.guildId = data.guild_id; + } else { + this.guildId ??= null; + } + + if ('cover_image' in data) { + /** + * The hash of the application's cover image + * @type {?string} + */ + this.cover = data.cover_image; + } else { + this.cover ??= null; + } + + if ('rpc_origins' in data) { + /** + * The application's RPC origins, if enabled + * @type {string[]} + */ + this.rpcOrigins = data.rpc_origins; + } else { + this.rpcOrigins ??= []; + } + + if ('bot_require_code_grant' in data) { + /** + * If this application's bot requires a code grant when using the OAuth2 flow + * @type {?boolean} + */ + this.botRequireCodeGrant = data.bot_require_code_grant; + } else { + this.botRequireCodeGrant ??= null; + } + + if ('bot_public' in data) { + /** + * If this application's bot is public + * @type {?boolean} + */ + this.botPublic = data.bot_public; + } else { + this.botPublic ??= null; + } + + if ('role_connections_verification_url' in data) { + /** + * This application's role connection verification entry point URL + * @type {?string} + */ + this.roleConnectionsVerificationURL = data.role_connections_verification_url; + } else { + this.roleConnectionsVerificationURL ??= null; + } + + /** + * The owner of this OAuth application + * @type {?(User|Team)} + */ + this.owner = data.team + ? new Team(this.client, data.team) + : data.owner + ? this.client.users._add(data.owner) + : this.owner ?? null; + } + + /** + * The guild associated with this application. + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.cache.get(this.guildId) ?? null; + } + + /** + * Whether this application is partial + * @type {boolean} + * @readonly + */ + get partial() { + return !this.name; + } + + /** + * Obtains this application from Discord. + * @returns {Promise<ClientApplication>} + */ + async fetch() { + const data = await this.client.rest.get(Routes.currentApplication()); + this._patch(data); + return this; + } + + /** + * Gets this application's role connection metadata records + * @returns {Promise<ApplicationRoleConnectionMetadata[]>} + */ + async fetchRoleConnectionMetadataRecords() { + const metadata = await this.client.rest.get(Routes.applicationRoleConnectionMetadata(this.client.user.id)); + return metadata.map(data => new ApplicationRoleConnectionMetadata(data)); + } + + /** + * Data for creating or editing an application role connection metadata. + * @typedef {Object} ApplicationRoleConnectionMetadataEditOptions + * @property {string} name The name of the metadata field + * @property {?Object<Locale, string>} [nameLocalizations] The name localizations for the metadata field + * @property {string} description The description of the metadata field + * @property {?Object<Locale, string>} [descriptionLocalizations] The description localizations for the metadata field + * @property {string} key The dictionary key of the metadata field + * @property {ApplicationRoleConnectionMetadataType} type The type of the metadata field + */ + + /** + * Updates this application's role connection metadata records + * @param {ApplicationRoleConnectionMetadataEditOptions[]} records The new role connection metadata records + * @returns {Promise<ApplicationRoleConnectionMetadata[]>} + */ + async editRoleConnectionMetadataRecords(records) { + const newRecords = await this.client.rest.put(Routes.applicationRoleConnectionMetadata(this.client.user.id), { + body: records.map(record => ({ + type: record.type, + key: record.key, + name: record.name, + name_localizations: record.nameLocalizations, + description: record.description, + description_localizations: record.descriptionLocalizations, + })), + }); + + return newRecords.map(data => new ApplicationRoleConnectionMetadata(data)); + } +} + +module.exports = ClientApplication; diff --git a/node_modules/discord.js/src/structures/ClientPresence.js b/node_modules/discord.js/src/structures/ClientPresence.js new file mode 100644 index 0000000..6dd72ee --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientPresence.js @@ -0,0 +1,90 @@ +'use strict'; + +const { GatewayOpcodes, ActivityType } = require('discord-api-types/v10'); +const { Presence } = require('./Presence'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); + +/** + * Represents the client's presence. + * @extends {Presence} + */ +class ClientPresence extends Presence { + constructor(client, data = {}) { + super(client, Object.assign(data, { status: data.status ?? 'online', user: { id: null } })); + } + + /** + * Sets the client's presence + * @param {PresenceData} presence The data to set the presence to + * @returns {ClientPresence} + */ + set(presence) { + const packet = this._parse(presence); + this._patch(packet); + if (presence.shardId === undefined) { + this.client.ws.broadcast({ op: GatewayOpcodes.PresenceUpdate, d: packet }); + } else if (Array.isArray(presence.shardId)) { + for (const shardId of presence.shardId) { + this.client.ws.shards.get(shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet }); + } + } else { + this.client.ws.shards.get(presence.shardId).send({ op: GatewayOpcodes.PresenceUpdate, d: packet }); + } + return this; + } + + /** + * Parses presence data into a packet ready to be sent to Discord + * @param {PresenceData} presence The data to parse + * @returns {APIPresence} + * @private + */ + _parse({ status, since, afk, activities }) { + const data = { + activities: [], + afk: typeof afk === 'boolean' ? afk : false, + since: typeof since === 'number' && !Number.isNaN(since) ? since : null, + status: status ?? this.status, + }; + if (activities?.length) { + for (const [i, activity] of activities.entries()) { + if (typeof activity.name !== 'string') { + throw new DiscordjsTypeError(ErrorCodes.InvalidType, `activities[${i}].name`, 'string'); + } + + activity.type ??= ActivityType.Playing; + + if (activity.type === ActivityType.Custom && !activity.state) { + activity.state = activity.name; + activity.name = 'Custom Status'; + } + + data.activities.push({ + type: activity.type, + name: activity.name, + state: activity.state, + url: activity.url, + }); + } + } else if (!activities && (status || afk || since) && this.activities.length) { + data.activities.push( + ...this.activities.map(a => ({ + name: a.name, + state: a.state ?? undefined, + type: a.type, + url: a.url ?? undefined, + })), + ); + } + + return data; + } +} + +module.exports = ClientPresence; + +/* eslint-disable max-len */ +/** + * @external APIPresence + * @see {@link https://discord.com/developers/docs/rich-presence/how-to#updating-presence-update-presence-payload-fields} + */ diff --git a/node_modules/discord.js/src/structures/ClientUser.js b/node_modules/discord.js/src/structures/ClientUser.js new file mode 100644 index 0000000..b93904c --- /dev/null +++ b/node_modules/discord.js/src/structures/ClientUser.js @@ -0,0 +1,187 @@ +'use strict'; + +const { Routes } = require('discord-api-types/v10'); +const User = require('./User'); +const DataResolver = require('../util/DataResolver'); + +/** + * Represents the logged in client's Discord user. + * @extends {User} + */ +class ClientUser extends User { + _patch(data) { + super._patch(data); + + if ('verified' in data) { + /** + * Whether or not this account has been verified + * @type {boolean} + */ + this.verified = data.verified; + } + + if ('mfa_enabled' in data) { + /** + * If the bot's {@link ClientApplication#owner Owner} has MFA enabled on their account + * @type {?boolean} + */ + this.mfaEnabled = typeof data.mfa_enabled === 'boolean' ? data.mfa_enabled : null; + } else { + this.mfaEnabled ??= null; + } + + if ('token' in data) this.client.token = data.token; + } + + /** + * Represents the client user's presence + * @type {ClientPresence} + * @readonly + */ + get presence() { + return this.client.presence; + } + + /** + * Data used to edit the logged in client + * @typedef {Object} ClientUserEditOptions + * @property {string} [username] The new username + * @property {?(BufferResolvable|Base64Resolvable)} [avatar] The new avatar + */ + + /** + * Edits the logged in client. + * @param {ClientUserEditOptions} options The options to provide + * @returns {Promise<ClientUser>} + */ + async edit({ username, avatar }) { + const data = await this.client.rest.patch(Routes.user(), { + body: { username, avatar: avatar && (await DataResolver.resolveImage(avatar)) }, + }); + + this.client.token = data.token; + this.client.rest.setToken(data.token); + const { updated } = this.client.actions.UserUpdate.handle(data); + return updated ?? this; + } + + /** + * Sets the username of the logged in client. + * <info>Changing usernames in Discord is heavily rate limited, with only 2 requests + * every hour. Use this sparingly!</info> + * @param {string} username The new username + * @returns {Promise<ClientUser>} + * @example + * // Set username + * client.user.setUsername('discordjs') + * .then(user => console.log(`My new username is ${user.username}`)) + * .catch(console.error); + */ + setUsername(username) { + return this.edit({ username }); + } + + /** + * Sets the avatar of the logged in client. + * @param {?(BufferResolvable|Base64Resolvable)} avatar The new avatar + * @returns {Promise<ClientUser>} + * @example + * // Set avatar + * client.user.setAvatar('./avatar.png') + * .then(user => console.log(`New avatar set!`)) + * .catch(console.error); + */ + setAvatar(avatar) { + return this.edit({ avatar }); + } + + /** + * Options for setting activities + * @typedef {Object} ActivitiesOptions + * @property {string} name Name of the activity + * @property {string} [state] State of the activity + * @property {ActivityType} [type] Type of the activity + * @property {string} [url] Twitch / YouTube stream URL + */ + + /** + * Data resembling a raw Discord presence. + * @typedef {Object} PresenceData + * @property {PresenceStatusData} [status] Status of the user + * @property {boolean} [afk] Whether the user is AFK + * @property {ActivitiesOptions[]} [activities] Activity the user is playing + * @property {number|number[]} [shardId] Shard id(s) to have the activity set on + */ + + /** + * Sets the full presence of the client user. + * @param {PresenceData} data Data for the presence + * @returns {ClientPresence} + * @example + * // Set the client user's presence + * client.user.setPresence({ activities: [{ name: 'with discord.js' }], status: 'idle' }); + */ + setPresence(data) { + return this.client.presence.set(data); + } + + /** + * A user's status. Must be one of: + * * `online` + * * `idle` + * * `invisible` + * * `dnd` (do not disturb) + * @typedef {string} PresenceStatusData + */ + + /** + * Sets the status of the client user. + * @param {PresenceStatusData} status Status to change to + * @param {number|number[]} [shardId] Shard id(s) to have the activity set on + * @returns {ClientPresence} + * @example + * // Set the client user's status + * client.user.setStatus('idle'); + */ + setStatus(status, shardId) { + return this.setPresence({ status, shardId }); + } + + /** + * Options for setting an activity. + * @typedef {Object} ActivityOptions + * @property {string} name Name of the activity + * @property {string} [state] State of the activity + * @property {string} [url] Twitch / YouTube stream URL + * @property {ActivityType} [type] Type of the activity + * @property {number|number[]} [shardId] Shard Id(s) to have the activity set on + */ + + /** + * Sets the activity the client user is playing. + * @param {string|ActivityOptions} name Activity being played, or options for setting the activity + * @param {ActivityOptions} [options] Options for setting the activity + * @returns {ClientPresence} + * @example + * // Set the client user's activity + * client.user.setActivity('discord.js', { type: ActivityType.Watching }); + */ + setActivity(name, options = {}) { + if (!name) return this.setPresence({ activities: [], shardId: options.shardId }); + + const activity = Object.assign({}, options, typeof name === 'object' ? name : { name }); + return this.setPresence({ activities: [activity], shardId: activity.shardId }); + } + + /** + * Sets/removes the AFK flag for the client user. + * @param {boolean} [afk=true] Whether or not the user is AFK + * @param {number|number[]} [shardId] Shard Id(s) to have the AFK flag set on + * @returns {ClientPresence} + */ + setAFK(afk = true, shardId) { + return this.setPresence({ afk, shardId }); + } +} + +module.exports = ClientUser; diff --git a/node_modules/discord.js/src/structures/CommandInteraction.js b/node_modules/discord.js/src/structures/CommandInteraction.js new file mode 100644 index 0000000..ec6ef40 --- /dev/null +++ b/node_modules/discord.js/src/structures/CommandInteraction.js @@ -0,0 +1,224 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const Attachment = require('./Attachment'); +const BaseInteraction = require('./BaseInteraction'); +const InteractionWebhook = require('./InteractionWebhook'); +const InteractionResponses = require('./interfaces/InteractionResponses'); + +/** + * Represents a command interaction. + * @extends {BaseInteraction} + * @implements {InteractionResponses} + * @abstract + */ +class CommandInteraction extends BaseInteraction { + constructor(client, data) { + super(client, data); + + /** + * The id of the channel this interaction was sent in + * @type {Snowflake} + * @name CommandInteraction#channelId + */ + + /** + * The invoked application command's id + * @type {Snowflake} + */ + this.commandId = data.data.id; + + /** + * The invoked application command's name + * @type {string} + */ + this.commandName = data.data.name; + + /** + * The invoked application command's type + * @type {ApplicationCommandType} + */ + this.commandType = data.data.type; + + /** + * The id of the guild the invoked application command is registered to + * @type {?Snowflake} + */ + this.commandGuildId = data.data.guild_id ?? null; + + /** + * Whether the reply to this interaction has been deferred + * @type {boolean} + */ + this.deferred = false; + + /** + * Whether this interaction has already been replied to + * @type {boolean} + */ + this.replied = false; + + /** + * Whether the reply to this interaction is ephemeral + * @type {?boolean} + */ + this.ephemeral = null; + + /** + * An associated interaction webhook, can be used to further interact with this interaction + * @type {InteractionWebhook} + */ + this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token); + } + + /** + * The invoked application command, if it was fetched before + * @type {?ApplicationCommand} + */ + get command() { + const id = this.commandId; + return this.guild?.commands.cache.get(id) ?? this.client.application.commands.cache.get(id) ?? null; + } + + /** + * Represents the resolved data of a received command interaction. + * @typedef {Object} CommandInteractionResolvedData + * @property {Collection<Snowflake, User>} [users] The resolved users + * @property {Collection<Snowflake, GuildMember|APIGuildMember>} [members] The resolved guild members + * @property {Collection<Snowflake, Role|APIRole>} [roles] The resolved roles + * @property {Collection<Snowflake, BaseChannel|APIChannel>} [channels] The resolved channels + * @property {Collection<Snowflake, Message|APIMessage>} [messages] The resolved messages + * @property {Collection<Snowflake, Attachment>} [attachments] The resolved attachments + */ + + /** + * Transforms the resolved received from the API. + * @param {APIInteractionDataResolved} resolved The received resolved objects + * @returns {CommandInteractionResolvedData} + * @private + */ + transformResolved({ members, users, channels, roles, messages, attachments }) { + const result = {}; + + if (members) { + result.members = new Collection(); + for (const [id, member] of Object.entries(members)) { + const user = users[id]; + result.members.set(id, this.guild?.members._add({ user, ...member }) ?? member); + } + } + + if (users) { + result.users = new Collection(); + for (const user of Object.values(users)) { + result.users.set(user.id, this.client.users._add(user)); + } + } + + if (roles) { + result.roles = new Collection(); + for (const role of Object.values(roles)) { + result.roles.set(role.id, this.guild?.roles._add(role) ?? role); + } + } + + if (channels) { + result.channels = new Collection(); + for (const channel of Object.values(channels)) { + result.channels.set(channel.id, this.client.channels._add(channel, this.guild) ?? channel); + } + } + + if (messages) { + result.messages = new Collection(); + for (const message of Object.values(messages)) { + result.messages.set(message.id, this.channel?.messages?._add(message) ?? message); + } + } + + if (attachments) { + result.attachments = new Collection(); + for (const attachment of Object.values(attachments)) { + const patched = new Attachment(attachment); + result.attachments.set(attachment.id, patched); + } + } + + return result; + } + + /** + * Represents an option of a received command interaction. + * @typedef {Object} CommandInteractionOption + * @property {string} name The name of the option + * @property {ApplicationCommandOptionType} type The type of the option + * @property {boolean} [autocomplete] Whether the autocomplete interaction is enabled for a + * {@link ApplicationCommandOptionType.String}, {@link ApplicationCommandOptionType.Integer} or + * {@link ApplicationCommandOptionType.Number} option + * @property {string|number|boolean} [value] The value of the option + * @property {CommandInteractionOption[]} [options] Additional options if this option is a + * subcommand (group) + * @property {User} [user] The resolved user + * @property {GuildMember|APIGuildMember} [member] The resolved member + * @property {GuildChannel|ThreadChannel|APIChannel} [channel] The resolved channel + * @property {Role|APIRole} [role] The resolved role + * @property {Attachment} [attachment] The resolved attachment + */ + + /** + * Transforms an option received from the API. + * @param {APIApplicationCommandOption} option The received option + * @param {APIInteractionDataResolved} resolved The resolved interaction data + * @returns {CommandInteractionOption} + * @private + */ + transformOption(option, resolved) { + const result = { + name: option.name, + type: option.type, + }; + + if ('value' in option) result.value = option.value; + if ('options' in option) result.options = option.options.map(opt => this.transformOption(opt, resolved)); + + if (resolved) { + const user = resolved.users?.[option.value]; + if (user) result.user = this.client.users._add(user); + + const member = resolved.members?.[option.value]; + if (member) result.member = this.guild?.members._add({ user, ...member }) ?? member; + + const channel = resolved.channels?.[option.value]; + if (channel) result.channel = this.client.channels._add(channel, this.guild) ?? channel; + + const role = resolved.roles?.[option.value]; + if (role) result.role = this.guild?.roles._add(role) ?? role; + + const attachment = resolved.attachments?.[option.value]; + if (attachment) result.attachment = new Attachment(attachment); + } + + return result; + } + + // These are here only for documentation purposes - they are implemented by InteractionResponses + /* eslint-disable no-empty-function */ + deferReply() {} + reply() {} + fetchReply() {} + editReply() {} + deleteReply() {} + followUp() {} + showModal() {} + awaitModalSubmit() {} +} + +InteractionResponses.applyToClass(CommandInteraction, ['deferUpdate', 'update']); + +module.exports = CommandInteraction; + +/* eslint-disable max-len */ +/** + * @external APIInteractionDataResolved + * @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-resolved-data-structure} + */ diff --git a/node_modules/discord.js/src/structures/CommandInteractionOptionResolver.js b/node_modules/discord.js/src/structures/CommandInteractionOptionResolver.js new file mode 100644 index 0000000..621dbf4 --- /dev/null +++ b/node_modules/discord.js/src/structures/CommandInteractionOptionResolver.js @@ -0,0 +1,308 @@ +'use strict'; + +const { ApplicationCommandOptionType } = require('discord-api-types/v10'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); + +/** + * A resolver for command interaction options. + */ +class CommandInteractionOptionResolver { + constructor(client, options, resolved) { + /** + * The client that instantiated this. + * @name CommandInteractionOptionResolver#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The name of the subcommand group. + * @type {?string} + * @private + */ + this._group = null; + + /** + * The name of the subcommand. + * @type {?string} + * @private + */ + this._subcommand = null; + + /** + * The bottom-level options for the interaction. + * If there is a subcommand (or subcommand and group), this is the options for the subcommand. + * @type {CommandInteractionOption[]} + * @private + */ + this._hoistedOptions = options; + + // Hoist subcommand group if present + if (this._hoistedOptions[0]?.type === ApplicationCommandOptionType.SubcommandGroup) { + this._group = this._hoistedOptions[0].name; + this._hoistedOptions = this._hoistedOptions[0].options ?? []; + } + // Hoist subcommand if present + if (this._hoistedOptions[0]?.type === ApplicationCommandOptionType.Subcommand) { + this._subcommand = this._hoistedOptions[0].name; + this._hoistedOptions = this._hoistedOptions[0].options ?? []; + } + + /** + * The interaction options array. + * @name CommandInteractionOptionResolver#data + * @type {ReadonlyArray<CommandInteractionOption>} + * @readonly + */ + Object.defineProperty(this, 'data', { value: Object.freeze([...options]) }); + + /** + * The interaction resolved data + * @name CommandInteractionOptionResolver#resolved + * @type {?Readonly<CommandInteractionResolvedData>} + */ + Object.defineProperty(this, 'resolved', { value: resolved ? Object.freeze(resolved) : null }); + } + + /** + * Gets an option by its name. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?CommandInteractionOption} The option, if found. + */ + get(name, required = false) { + const option = this._hoistedOptions.find(opt => opt.name === name); + if (!option) { + if (required) { + throw new DiscordjsTypeError(ErrorCodes.CommandInteractionOptionNotFound, name); + } + return null; + } + return option; + } + + /** + * Gets an option by name and property and checks its type. + * @param {string} name The name of the option. + * @param {ApplicationCommandOptionType[]} allowedTypes The allowed types of the option. + * @param {string[]} properties The properties to check for for `required`. + * @param {boolean} required Whether to throw an error if the option is not found. + * @returns {?CommandInteractionOption} The option, if found. + * @private + */ + _getTypedOption(name, allowedTypes, properties, required) { + const option = this.get(name, required); + if (!option) { + return null; + } else if (!allowedTypes.includes(option.type)) { + throw new DiscordjsTypeError(ErrorCodes.CommandInteractionOptionType, name, option.type, allowedTypes.join(', ')); + } else if (required && properties.every(prop => option[prop] === null || option[prop] === undefined)) { + throw new DiscordjsTypeError(ErrorCodes.CommandInteractionOptionEmpty, name, option.type); + } + return option; + } + + /** + * Gets the selected subcommand. + * @param {boolean} [required=true] Whether to throw an error if there is no subcommand. + * @returns {?string} The name of the selected subcommand, or null if not set and not required. + */ + getSubcommand(required = true) { + if (required && !this._subcommand) { + throw new DiscordjsTypeError(ErrorCodes.CommandInteractionOptionNoSubcommand); + } + return this._subcommand; + } + + /** + * Gets the selected subcommand group. + * @param {boolean} [required=false] Whether to throw an error if there is no subcommand group. + * @returns {?string} The name of the selected subcommand group, or null if not set and not required. + */ + getSubcommandGroup(required = false) { + if (required && !this._group) { + throw new DiscordjsTypeError(ErrorCodes.CommandInteractionOptionNoSubcommandGroup); + } + return this._group; + } + + /** + * Gets a boolean option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?boolean} The value of the option, or null if not set and not required. + */ + getBoolean(name, required = false) { + const option = this._getTypedOption(name, [ApplicationCommandOptionType.Boolean], ['value'], required); + return option?.value ?? null; + } + + /** + * Gets a channel option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @param {ChannelType[]} [channelTypes=[]] The allowed types of channels. If empty, all channel types are allowed. + * @returns {?(GuildChannel|ThreadChannel|APIChannel)} + * The value of the option, or null if not set and not required. + */ + getChannel(name, required = false, channelTypes = []) { + const option = this._getTypedOption(name, [ApplicationCommandOptionType.Channel], ['channel'], required); + const channel = option?.channel ?? null; + + if (channel && channelTypes.length > 0 && !channelTypes.includes(channel.type)) { + throw new DiscordjsTypeError( + ErrorCodes.CommandInteractionOptionInvalidChannelType, + name, + channel.type, + channelTypes.join(', '), + ); + } + + return channel; + } + + /** + * Gets a string option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?string} The value of the option, or null if not set and not required. + */ + getString(name, required = false) { + const option = this._getTypedOption(name, [ApplicationCommandOptionType.String], ['value'], required); + return option?.value ?? null; + } + + /** + * Gets an integer option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?number} The value of the option, or null if not set and not required. + */ + getInteger(name, required = false) { + const option = this._getTypedOption(name, [ApplicationCommandOptionType.Integer], ['value'], required); + return option?.value ?? null; + } + + /** + * Gets a number option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?number} The value of the option, or null if not set and not required. + */ + getNumber(name, required = false) { + const option = this._getTypedOption(name, [ApplicationCommandOptionType.Number], ['value'], required); + return option?.value ?? null; + } + + /** + * Gets a user option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?User} The value of the option, or null if not set and not required. + */ + getUser(name, required = false) { + const option = this._getTypedOption( + name, + [ApplicationCommandOptionType.User, ApplicationCommandOptionType.Mentionable], + ['user'], + required, + ); + return option?.user ?? null; + } + + /** + * Gets a member option. + * @param {string} name The name of the option. + * @returns {?(GuildMember|APIGuildMember)} + * The value of the option, or null if the user is not present in the guild or the option is not set. + */ + getMember(name) { + const option = this._getTypedOption( + name, + [ApplicationCommandOptionType.User, ApplicationCommandOptionType.Mentionable], + ['member'], + false, + ); + return option?.member ?? null; + } + + /** + * Gets a role option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?(Role|APIRole)} The value of the option, or null if not set and not required. + */ + getRole(name, required = false) { + const option = this._getTypedOption( + name, + [ApplicationCommandOptionType.Role, ApplicationCommandOptionType.Mentionable], + ['role'], + required, + ); + return option?.role ?? null; + } + + /** + * Gets an attachment option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?Attachment} The value of the option, or null if not set and not required. + */ + getAttachment(name, required = false) { + const option = this._getTypedOption(name, [ApplicationCommandOptionType.Attachment], ['attachment'], required); + return option?.attachment ?? null; + } + + /** + * Gets a mentionable option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?(User|GuildMember|APIGuildMember|Role|APIRole)} + * The value of the option, or null if not set and not required. + */ + getMentionable(name, required = false) { + const option = this._getTypedOption( + name, + [ApplicationCommandOptionType.Mentionable], + ['user', 'member', 'role'], + required, + ); + return option?.member ?? option?.user ?? option?.role ?? null; + } + + /** + * Gets a message option. + * @param {string} name The name of the option. + * @param {boolean} [required=false] Whether to throw an error if the option is not found. + * @returns {?Message} + * The value of the option, or null if not set and not required. + */ + getMessage(name, required = false) { + const option = this._getTypedOption(name, ['_MESSAGE'], ['message'], required); + return option?.message ?? null; + } + + /** + * The full autocomplete option object. + * @typedef {Object} AutocompleteFocusedOption + * @property {string} name The name of the option + * @property {ApplicationCommandOptionType} type The type of the application command option + * @property {string} value The value of the option + * @property {boolean} focused Whether this option is currently in focus for autocomplete + */ + + /** + * Gets the focused option. + * @param {boolean} [getFull=false] Whether to get the full option object + * @returns {string|AutocompleteFocusedOption} + * The value of the option, or the whole option if getFull is true + */ + getFocused(getFull = false) { + const focusedOption = this._hoistedOptions.find(option => option.focused); + if (!focusedOption) throw new DiscordjsTypeError(ErrorCodes.AutocompleteInteractionOptionNoFocusedOption); + return getFull ? focusedOption : focusedOption.value; + } +} + +module.exports = CommandInteractionOptionResolver; diff --git a/node_modules/discord.js/src/structures/Component.js b/node_modules/discord.js/src/structures/Component.js new file mode 100644 index 0000000..10ba27d --- /dev/null +++ b/node_modules/discord.js/src/structures/Component.js @@ -0,0 +1,47 @@ +'use strict'; + +const isEqual = require('fast-deep-equal'); + +/** + * Represents a component + */ +class Component { + constructor(data) { + /** + * The API data associated with this component + * @type {APIMessageComponent} + */ + this.data = data; + } + + /** + * The type of the component + * @type {ComponentType} + * @readonly + */ + get type() { + return this.data.type; + } + + /** + * Whether or not the given components are equal + * @param {Component|APIMessageComponent} other The component to compare against + * @returns {boolean} + */ + equals(other) { + if (other instanceof Component) { + return isEqual(other.data, this.data); + } + return isEqual(other, this.data); + } + + /** + * Returns the API-compatible JSON for this component + * @returns {APIMessageComponent} + */ + toJSON() { + return { ...this.data }; + } +} + +module.exports = Component; diff --git a/node_modules/discord.js/src/structures/ContextMenuCommandInteraction.js b/node_modules/discord.js/src/structures/ContextMenuCommandInteraction.js new file mode 100644 index 0000000..fc49ca5 --- /dev/null +++ b/node_modules/discord.js/src/structures/ContextMenuCommandInteraction.js @@ -0,0 +1,64 @@ +'use strict'; + +const { lazy } = require('@discordjs/util'); +const { ApplicationCommandOptionType } = require('discord-api-types/v10'); +const CommandInteraction = require('./CommandInteraction'); +const CommandInteractionOptionResolver = require('./CommandInteractionOptionResolver'); + +const getMessage = lazy(() => require('./Message').Message); + +/** + * Represents a context menu interaction. + * @extends {CommandInteraction} + */ +class ContextMenuCommandInteraction extends CommandInteraction { + constructor(client, data) { + super(client, data); + /** + * The target of the interaction, parsed into options + * @type {CommandInteractionOptionResolver} + */ + this.options = new CommandInteractionOptionResolver( + this.client, + this.resolveContextMenuOptions(data.data), + this.transformResolved(data.data.resolved), + ); + + /** + * The id of the target of this interaction + * @type {Snowflake} + */ + this.targetId = data.data.target_id; + } + + /** + * Resolves and transforms options received from the API for a context menu interaction. + * @param {APIApplicationCommandInteractionData} data The interaction data + * @returns {CommandInteractionOption[]} + * @private + */ + resolveContextMenuOptions({ target_id, resolved }) { + const result = []; + + if (resolved.users?.[target_id]) { + result.push( + this.transformOption({ name: 'user', type: ApplicationCommandOptionType.User, value: target_id }, resolved), + ); + } + + if (resolved.messages?.[target_id]) { + result.push({ + name: 'message', + type: '_MESSAGE', + value: target_id, + message: + this.channel?.messages._add(resolved.messages[target_id]) ?? + new (getMessage())(this.client, resolved.messages[target_id]), + }); + } + + return result; + } +} + +module.exports = ContextMenuCommandInteraction; diff --git a/node_modules/discord.js/src/structures/DMChannel.js b/node_modules/discord.js/src/structures/DMChannel.js new file mode 100644 index 0000000..2c917c4 --- /dev/null +++ b/node_modules/discord.js/src/structures/DMChannel.js @@ -0,0 +1,129 @@ +'use strict'; + +const { userMention } = require('@discordjs/builders'); +const { ChannelType } = require('discord-api-types/v10'); +const { BaseChannel } = require('./BaseChannel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const DMMessageManager = require('../managers/DMMessageManager'); +const Partials = require('../util/Partials'); + +/** + * Represents a direct message channel between two users. + * @extends {BaseChannel} + * @implements {TextBasedChannel} + */ +class DMChannel extends BaseChannel { + constructor(client, data) { + super(client, data); + + // Override the channel type so partials have a known type + this.type = ChannelType.DM; + + /** + * A manager of the messages belonging to this channel + * @type {DMMessageManager} + */ + this.messages = new DMMessageManager(this); + } + + _patch(data) { + super._patch(data); + + if (data.recipients) { + const recipient = data.recipients[0]; + + /** + * The recipient's id + * @type {Snowflake} + */ + this.recipientId = recipient.id; + + if ('username' in recipient || this.client.options.partials.includes(Partials.User)) { + this.client.users._add(recipient); + } + } + + if ('last_message_id' in data) { + /** + * The channel's last message id, if one was sent + * @type {?Snowflake} + */ + this.lastMessageId = data.last_message_id; + } + + if ('last_pin_timestamp' in data) { + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = Date.parse(data.last_pin_timestamp); + } else { + this.lastPinTimestamp ??= null; + } + } + + /** + * Whether this DMChannel is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return this.lastMessageId === undefined; + } + + /** + * The recipient on the other end of the DM + * @type {?User} + * @readonly + */ + get recipient() { + return this.client.users.resolve(this.recipientId); + } + + /** + * Fetch this DMChannel. + * @param {boolean} [force=true] Whether to skip the cache check and request the API + * @returns {Promise<DMChannel>} + */ + fetch(force = true) { + return this.client.users.createDM(this.recipientId, { force }); + } + + /** + * When concatenated with a string, this automatically returns the recipient's mention instead of the + * DMChannel object. + * @returns {string} + * @example + * // Logs: Hello from <@123456789012345678>! + * console.log(`Hello from ${channel}!`); + */ + toString() { + return userMention(this.recipientId); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + get lastMessage() {} + get lastPinAt() {} + send() {} + sendTyping() {} + createMessageCollector() {} + awaitMessages() {} + createMessageComponentCollector() {} + awaitMessageComponent() {} + // Doesn't work on DM channels; bulkDelete() {} + // Doesn't work on DM channels; fetchWebhooks() {} + // Doesn't work on DM channels; createWebhook() {} + // Doesn't work on DM channels; setRateLimitPerUser() {} + // Doesn't work on DM channels; setNSFW() {} +} + +TextBasedChannel.applyToClass(DMChannel, true, [ + 'bulkDelete', + 'fetchWebhooks', + 'createWebhook', + 'setRateLimitPerUser', + 'setNSFW', +]); + +module.exports = DMChannel; diff --git a/node_modules/discord.js/src/structures/DirectoryChannel.js b/node_modules/discord.js/src/structures/DirectoryChannel.js new file mode 100644 index 0000000..870eff9 --- /dev/null +++ b/node_modules/discord.js/src/structures/DirectoryChannel.js @@ -0,0 +1,36 @@ +'use strict'; + +const { BaseChannel } = require('./BaseChannel'); + +/** + * Represents a channel that displays a directory of guilds. + * @extends {BaseChannel} + */ +class DirectoryChannel extends BaseChannel { + constructor(guild, data, client) { + super(client, data); + + /** + * The guild the channel is in + * @type {InviteGuild} + */ + this.guild = guild; + + /** + * The id of the guild the channel is in + * @type {Snowflake} + */ + this.guildId = guild.id; + } + + _patch(data) { + super._patch(data); + /** + * The channel's name + * @type {string} + */ + this.name = data.name; + } +} + +module.exports = DirectoryChannel; diff --git a/node_modules/discord.js/src/structures/Embed.js b/node_modules/discord.js/src/structures/Embed.js new file mode 100644 index 0000000..dd68120 --- /dev/null +++ b/node_modules/discord.js/src/structures/Embed.js @@ -0,0 +1,220 @@ +'use strict'; + +const { embedLength } = require('@discordjs/builders'); +const isEqual = require('fast-deep-equal'); + +/** + * Represents an embed. + */ +class Embed { + constructor(data) { + /** + * The API embed data. + * @type {APIEmbed} + * @readonly + */ + this.data = { ...data }; + } + + /** + * An array of fields of this embed. + * @type {Array<APIEmbedField>} + * @readonly + */ + get fields() { + return this.data.fields ?? []; + } + + /** + * The title of this embed. + * @type {?string} + * @readonly + */ + get title() { + return this.data.title ?? null; + } + + /** + * The description of this embed. + * @type {?string} + * @readonly + */ + get description() { + return this.data.description ?? null; + } + + /** + * The URL of this embed. + * @type {?string} + * @readonly + */ + get url() { + return this.data.url ?? null; + } + + /** + * The color of this embed. + * @type {?number} + * @readonly + */ + get color() { + return this.data.color ?? null; + } + + /** + * The timestamp of this embed. This is in an ISO 8601 format. + * @type {?string} + * @readonly + */ + get timestamp() { + return this.data.timestamp ?? null; + } + + /** + * @typedef {Object} EmbedAssetData + * @property {?string} url The URL of the image + * @property {?string} proxyURL The proxy URL of the image + * @property {?number} height The height of the image + * @property {?number} width The width of the image + */ + + /** + * The thumbnail of this embed. + * @type {?EmbedAssetData} + * @readonly + */ + get thumbnail() { + if (!this.data.thumbnail) return null; + return { + url: this.data.thumbnail.url, + proxyURL: this.data.thumbnail.proxy_url, + height: this.data.thumbnail.height, + width: this.data.thumbnail.width, + }; + } + + /** + * The image of this embed. + * @type {?EmbedAssetData} + * @readonly + */ + get image() { + if (!this.data.image) return null; + return { + url: this.data.image.url, + proxyURL: this.data.image.proxy_url, + height: this.data.image.height, + width: this.data.image.width, + }; + } + + /** + * The video of this embed. + * @type {?EmbedAssetData} + * @readonly + */ + get video() { + if (!this.data.video) return null; + return { + url: this.data.video.url, + proxyURL: this.data.video.proxy_url, + height: this.data.video.height, + width: this.data.video.width, + }; + } + + /** + * @typedef {Object} EmbedAuthorData + * @property {string} name The name of the author + * @property {?string} url The URL of the author + * @property {?string} iconURL The icon URL of the author + * @property {?string} proxyIconURL The proxy icon URL of the author + */ + + /** + * The author of this embed. + * @type {?EmbedAuthorData} + * @readonly + */ + get author() { + if (!this.data.author) return null; + return { + name: this.data.author.name, + url: this.data.author.url, + iconURL: this.data.author.icon_url, + proxyIconURL: this.data.author.proxy_icon_url, + }; + } + + /** + * The provider of this embed. + * @type {?APIEmbedProvider} + * @readonly + */ + get provider() { + return this.data.provider ?? null; + } + + /** + * @typedef {Object} EmbedFooterData + * @property {string} text The text of the footer + * @property {?string} iconURL The URL of the icon + * @property {?string} proxyIconURL The proxy URL of the icon + */ + + /** + * The footer of this embed. + * @type {?EmbedFooterData} + * @readonly + */ + get footer() { + if (!this.data.footer) return null; + return { + text: this.data.footer.text, + iconURL: this.data.footer.icon_url, + proxyIconURL: this.data.footer.proxy_icon_url, + }; + } + + /** + * The accumulated length for the embed title, description, fields, footer text, and author name. + * @type {number} + * @readonly + */ + get length() { + return embedLength(this.data); + } + + /** + * The hex color of this embed. + * @type {?string} + * @readonly + */ + get hexColor() { + return typeof this.data.color === 'number' + ? `#${this.data.color.toString(16).padStart(6, '0')}` + : this.data.color ?? null; + } + + /** + * Returns the API-compatible JSON for this embed. + * @returns {APIEmbed} + */ + toJSON() { + return { ...this.data }; + } + + /** + * Whether the given embeds are equal. + * @param {Embed|APIEmbed} other The embed to compare against + * @returns {boolean} + */ + equals(other) { + if (other instanceof Embed) { + return isEqual(other.data, this.data); + } + return isEqual(other, this.data); + } +} + +module.exports = Embed; diff --git a/node_modules/discord.js/src/structures/EmbedBuilder.js b/node_modules/discord.js/src/structures/EmbedBuilder.js new file mode 100644 index 0000000..10e445c --- /dev/null +++ b/node_modules/discord.js/src/structures/EmbedBuilder.js @@ -0,0 +1,50 @@ +'use strict'; + +const { EmbedBuilder: BuildersEmbed, embedLength } = require('@discordjs/builders'); +const { isJSONEncodable } = require('@discordjs/util'); +const { toSnakeCase } = require('../util/Transformers'); +const { resolveColor } = require('../util/Util'); + +/** + * Represents an embed builder. + * @extends {BuildersEmbed} + */ +class EmbedBuilder extends BuildersEmbed { + constructor(data) { + super(toSnakeCase(data)); + } + + /** + * Sets the color of this embed + * @param {?ColorResolvable} color The color of the embed + * @returns {EmbedBuilder} + */ + setColor(color) { + return super.setColor(color && resolveColor(color)); + } + + /** + * Creates a new embed builder from JSON data + * @param {EmbedBuilder|Embed|APIEmbed} other The other data + * @returns {EmbedBuilder} + */ + static from(other) { + return new this(isJSONEncodable(other) ? other.toJSON() : other); + } + + /** + * The accumulated length for the embed title, description, fields, footer text, and author name. + * @type {number} + * @readonly + */ + get length() { + return embedLength(this.data); + } +} + +module.exports = EmbedBuilder; + +/** + * @external BuildersEmbed + * @see {@link https://discord.js.org/docs/packages/builders/stable/EmbedBuilder:Class} + */ diff --git a/node_modules/discord.js/src/structures/Emoji.js b/node_modules/discord.js/src/structures/Emoji.js new file mode 100644 index 0000000..409d292 --- /dev/null +++ b/node_modules/discord.js/src/structures/Emoji.js @@ -0,0 +1,108 @@ +'use strict'; + +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const Base = require('./Base'); + +/** + * Represents raw emoji data from the API + * @typedef {APIEmoji} RawEmoji + * @property {?Snowflake} id The emoji's id + * @property {?string} name The emoji's name + * @property {?boolean} animated Whether the emoji is animated + */ + +/** + * Represents an emoji, see {@link GuildEmoji} and {@link ReactionEmoji}. + * @extends {Base} + */ +class Emoji extends Base { + constructor(client, emoji) { + super(client); + /** + * Whether or not the emoji is animated + * @type {?boolean} + */ + this.animated = emoji.animated ?? null; + + /** + * The emoji's name + * @type {?string} + */ + this.name = emoji.name ?? null; + + /** + * The emoji's id + * @type {?Snowflake} + */ + this.id = emoji.id; + } + + /** + * The identifier of this emoji, used for message reactions + * @type {string} + * @readonly + */ + get identifier() { + if (this.id) return `${this.animated ? 'a:' : ''}${this.name}:${this.id}`; + return encodeURIComponent(this.name); + } + + /** + * The URL to the emoji file if it's a custom emoji + * @type {?string} + * @readonly + */ + get url() { + return this.id && this.client.rest.cdn.emoji(this.id, this.animated ? 'gif' : 'png'); + } + + /** + * The timestamp the emoji was created at, or null if unicode + * @type {?number} + * @readonly + */ + get createdTimestamp() { + return this.id && DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the emoji was created at, or null if unicode + * @type {?Date} + * @readonly + */ + get createdAt() { + return this.id && new Date(this.createdTimestamp); + } + + /** + * When concatenated with a string, this automatically returns the text required to form a graphical emoji on Discord + * instead of the Emoji object. + * @returns {string} + * @example + * // Send a custom emoji from a guild: + * const emoji = guild.emojis.cache.first(); + * msg.channel.send(`Hello! ${emoji}`); + * @example + * // Send the emoji used in a reaction to the channel the reaction is part of + * reaction.message.channel.send(`The emoji used was: ${reaction.emoji}`); + */ + toString() { + return this.id ? `<${this.animated ? 'a' : ''}:${this.name}:${this.id}>` : this.name; + } + + toJSON() { + return super.toJSON({ + guild: 'guildId', + createdTimestamp: true, + url: true, + identifier: true, + }); + } +} + +exports.Emoji = Emoji; + +/** + * @external APIEmoji + * @see {@link https://discord.com/developers/docs/resources/emoji#emoji-object} + */ diff --git a/node_modules/discord.js/src/structures/ForumChannel.js b/node_modules/discord.js/src/structures/ForumChannel.js new file mode 100644 index 0000000..87e6478 --- /dev/null +++ b/node_modules/discord.js/src/structures/ForumChannel.js @@ -0,0 +1,264 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const GuildForumThreadManager = require('../managers/GuildForumThreadManager'); +const { transformAPIGuildForumTag, transformAPIGuildDefaultReaction } = require('../util/Channels'); + +/** + * @typedef {Object} GuildForumTagEmoji + * @property {?Snowflake} id The id of a guild's custom emoji + * @property {?string} name The unicode character of the emoji + */ + +/** + * @typedef {Object} GuildForumTag + * @property {Snowflake} id The id of the tag + * @property {string} name The name of the tag + * @property {boolean} moderated Whether this tag can only be added to or removed from threads + * by a member with the `ManageThreads` permission + * @property {?GuildForumTagEmoji} emoji The emoji of this tag + */ + +/** + * @typedef {Object} GuildForumTagData + * @property {Snowflake} [id] The id of the tag + * @property {string} name The name of the tag + * @property {boolean} [moderated] Whether this tag can only be added to or removed from threads + * by a member with the `ManageThreads` permission + * @property {?GuildForumTagEmoji} [emoji] The emoji of this tag + */ + +/** + * @typedef {Object} DefaultReactionEmoji + * @property {?Snowflake} id The id of a guild's custom emoji + * @property {?string} name The unicode character of the emoji + */ + +/** + * Represents a channel that only contains threads + * @extends {GuildChannel} + * @implements {TextBasedChannel} + */ +class ForumChannel extends GuildChannel { + constructor(guild, data, client) { + super(guild, data, client, false); + + /** + * A manager of the threads belonging to this channel + * @type {GuildForumThreadManager} + */ + this.threads = new GuildForumThreadManager(this); + + this._patch(data); + } + + _patch(data) { + super._patch(data); + if ('available_tags' in data) { + /** + * The set of tags that can be used in this channel. + * @type {GuildForumTag[]} + */ + this.availableTags = data.available_tags.map(tag => transformAPIGuildForumTag(tag)); + } else { + this.availableTags ??= []; + } + + if ('default_reaction_emoji' in data) { + /** + * The emoji to show in the add reaction button on a thread in a guild forum channel + * @type {?DefaultReactionEmoji} + */ + this.defaultReactionEmoji = data.default_reaction_emoji + ? transformAPIGuildDefaultReaction(data.default_reaction_emoji) + : null; + } else { + this.defaultReactionEmoji ??= null; + } + + if ('default_thread_rate_limit_per_user' in data) { + /** + * The initial rate limit per user (slowmode) to set on newly created threads in a channel. + * @type {?number} + */ + this.defaultThreadRateLimitPerUser = data.default_thread_rate_limit_per_user; + } else { + this.defaultThreadRateLimitPerUser ??= null; + } + + if ('rate_limit_per_user' in data) { + /** + * The rate limit per user (slowmode) for this channel. + * @type {?number} + */ + this.rateLimitPerUser = data.rate_limit_per_user; + } else { + this.rateLimitPerUser ??= null; + } + + if ('default_auto_archive_duration' in data) { + /** + * The default auto archive duration for newly created threads in this channel. + * @type {?ThreadAutoArchiveDuration} + */ + this.defaultAutoArchiveDuration = data.default_auto_archive_duration; + } else { + this.defaultAutoArchiveDuration ??= null; + } + + if ('nsfw' in data) { + /** + * If this channel is considered NSFW. + * @type {boolean} + */ + this.nsfw = data.nsfw; + } else { + this.nsfw ??= false; + } + + if ('topic' in data) { + /** + * The topic of this channel. + * @type {?string} + */ + this.topic = data.topic; + } + + if ('default_sort_order' in data) { + /** + * The default sort order mode used to order posts + * @type {?SortOrderType} + */ + this.defaultSortOrder = data.default_sort_order; + } else { + this.defaultSortOrder ??= null; + } + + /** + * The default layout type used to display posts + * @type {ForumLayoutType} + */ + this.defaultForumLayout = data.default_forum_layout; + } + + /** + * Sets the available tags for this forum channel + * @param {GuildForumTagData[]} availableTags The tags to set as available in this channel + * @param {string} [reason] Reason for changing the available tags + * @returns {Promise<ForumChannel>} + */ + setAvailableTags(availableTags, reason) { + return this.edit({ availableTags, reason }); + } + + /** + * Sets the default reaction emoji for this channel + * @param {?DefaultReactionEmoji} defaultReactionEmoji The emoji to set as the default reaction emoji + * @param {string} [reason] Reason for changing the default reaction emoji + * @returns {Promise<ForumChannel>} + */ + setDefaultReactionEmoji(defaultReactionEmoji, reason) { + return this.edit({ defaultReactionEmoji, reason }); + } + + /** + * Sets the default rate limit per user (slowmode) for new threads in this channel + * @param {number} defaultThreadRateLimitPerUser The rate limit to set on newly created threads in this channel + * @param {string} [reason] Reason for changing the default rate limit + * @returns {Promise<ForumChannel>} + */ + setDefaultThreadRateLimitPerUser(defaultThreadRateLimitPerUser, reason) { + return this.edit({ defaultThreadRateLimitPerUser, reason }); + } + + /** + * Creates an invite to this guild channel. + * @param {InviteCreateOptions} [options={}] The options for creating the invite + * @returns {Promise<Invite>} + * @example + * // Create an invite to a channel + * channel.createInvite() + * .then(invite => console.log(`Created an invite with a code of ${invite.code}`)) + * .catch(console.error); + */ + createInvite(options) { + return this.guild.invites.create(this.id, options); + } + + /** + * Fetches a collection of invites to this guild channel. + * Resolves with a collection mapping invites by their codes. + * @param {boolean} [cache=true] Whether to cache the fetched invites + * @returns {Promise<Collection<string, Invite>>} + */ + fetchInvites(cache) { + return this.guild.invites.fetch({ channelId: this.id, cache }); + } + + /** + * Sets the default auto archive duration for all newly created threads in this channel. + * @param {ThreadAutoArchiveDuration} defaultAutoArchiveDuration The new default auto archive duration + * @param {string} [reason] Reason for changing the channel's default auto archive duration + * @returns {Promise<ForumChannel>} + */ + setDefaultAutoArchiveDuration(defaultAutoArchiveDuration, reason) { + return this.edit({ defaultAutoArchiveDuration, reason }); + } + + /** + * Sets a new topic for the guild channel. + * @param {?string} topic The new topic for the guild channel + * @param {string} [reason] Reason for changing the guild channel's topic + * @returns {Promise<ForumChannel>} + * @example + * // Set a new channel topic + * channel.setTopic('needs more rate limiting') + * .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`)) + * .catch(console.error); + */ + setTopic(topic, reason) { + return this.edit({ topic, reason }); + } + + /** + * Sets the default sort order mode used to order posts + * @param {?SortOrderType} defaultSortOrder The default sort order mode to set on this channel + * @param {string} [reason] Reason for changing the default sort order + * @returns {Promise<ForumChannel>} + */ + setDefaultSortOrder(defaultSortOrder, reason) { + return this.edit({ defaultSortOrder, reason }); + } + + /** + * Sets the default forum layout type used to display posts + * @param {ForumLayoutType} defaultForumLayout The default forum layout type to set on this channel + * @param {string} [reason] Reason for changing the default forum layout + * @returns {Promise<ForumChannel>} + */ + setDefaultForumLayout(defaultForumLayout, reason) { + return this.edit({ defaultForumLayout, reason }); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + createWebhook() {} + fetchWebhooks() {} + setNSFW() {} + setRateLimitPerUser() {} +} + +TextBasedChannel.applyToClass(ForumChannel, true, [ + 'send', + 'lastMessage', + 'lastPinAt', + 'bulkDelete', + 'sendTyping', + 'createMessageCollector', + 'awaitMessages', + 'createMessageComponentCollector', + 'awaitMessageComponent', +]); + +module.exports = ForumChannel; diff --git a/node_modules/discord.js/src/structures/Guild.js b/node_modules/discord.js/src/structures/Guild.js new file mode 100644 index 0000000..f07e9b4 --- /dev/null +++ b/node_modules/discord.js/src/structures/Guild.js @@ -0,0 +1,1367 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { makeURLSearchParams } = require('@discordjs/rest'); +const { ChannelType, GuildPremiumTier, Routes, GuildFeature } = require('discord-api-types/v10'); +const AnonymousGuild = require('./AnonymousGuild'); +const GuildAuditLogs = require('./GuildAuditLogs'); +const { GuildOnboarding } = require('./GuildOnboarding'); +const GuildPreview = require('./GuildPreview'); +const GuildTemplate = require('./GuildTemplate'); +const Integration = require('./Integration'); +const Webhook = require('./Webhook'); +const WelcomeScreen = require('./WelcomeScreen'); +const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors'); +const AutoModerationRuleManager = require('../managers/AutoModerationRuleManager'); +const GuildApplicationCommandManager = require('../managers/GuildApplicationCommandManager'); +const GuildBanManager = require('../managers/GuildBanManager'); +const GuildChannelManager = require('../managers/GuildChannelManager'); +const GuildEmojiManager = require('../managers/GuildEmojiManager'); +const GuildInviteManager = require('../managers/GuildInviteManager'); +const GuildMemberManager = require('../managers/GuildMemberManager'); +const GuildScheduledEventManager = require('../managers/GuildScheduledEventManager'); +const GuildStickerManager = require('../managers/GuildStickerManager'); +const PresenceManager = require('../managers/PresenceManager'); +const RoleManager = require('../managers/RoleManager'); +const StageInstanceManager = require('../managers/StageInstanceManager'); +const VoiceStateManager = require('../managers/VoiceStateManager'); +const DataResolver = require('../util/DataResolver'); +const Status = require('../util/Status'); +const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField'); +const { discordSort, getSortableGroupTypes } = require('../util/Util'); + +/** + * Represents a guild (or a server) on Discord. + * <info>It's recommended to see if a guild is available before performing operations or reading data from it. You can + * check this with {@link Guild#available}.</info> + * @extends {AnonymousGuild} + */ +class Guild extends AnonymousGuild { + constructor(client, data) { + super(client, data, false); + + /** + * A manager of the application commands belonging to this guild + * @type {GuildApplicationCommandManager} + */ + this.commands = new GuildApplicationCommandManager(this); + + /** + * A manager of the members belonging to this guild + * @type {GuildMemberManager} + */ + this.members = new GuildMemberManager(this); + + /** + * A manager of the channels belonging to this guild + * @type {GuildChannelManager} + */ + this.channels = new GuildChannelManager(this); + + /** + * A manager of the bans belonging to this guild + * @type {GuildBanManager} + */ + this.bans = new GuildBanManager(this); + + /** + * A manager of the roles belonging to this guild + * @type {RoleManager} + */ + this.roles = new RoleManager(this); + + /** + * A manager of the presences belonging to this guild + * @type {PresenceManager} + */ + this.presences = new PresenceManager(this.client); + + /** + * A manager of the voice states of this guild + * @type {VoiceStateManager} + */ + this.voiceStates = new VoiceStateManager(this); + + /** + * A manager of the stage instances of this guild + * @type {StageInstanceManager} + */ + this.stageInstances = new StageInstanceManager(this); + + /** + * A manager of the invites of this guild + * @type {GuildInviteManager} + */ + this.invites = new GuildInviteManager(this); + + /** + * A manager of the scheduled events of this guild + * @type {GuildScheduledEventManager} + */ + this.scheduledEvents = new GuildScheduledEventManager(this); + + /** + * A manager of the auto moderation rules of this guild. + * @type {AutoModerationRuleManager} + */ + this.autoModerationRules = new AutoModerationRuleManager(this); + + if (!data) return; + if (data.unavailable) { + /** + * Whether the guild is available to access. If it is not available, it indicates a server outage + * @type {boolean} + */ + this.available = false; + } else { + this._patch(data); + if (!data.channels) this.available = false; + } + + /** + * The id of the shard this Guild belongs to. + * @type {number} + */ + this.shardId = data.shardId; + } + + /** + * The Shard this Guild belongs to. + * @type {WebSocketShard} + * @readonly + */ + get shard() { + return this.client.ws.shards.get(this.shardId); + } + + _patch(data) { + super._patch(data); + this.id = data.id; + if ('name' in data) this.name = data.name; + if ('icon' in data) this.icon = data.icon; + if ('unavailable' in data) { + this.available = !data.unavailable; + } else { + this.available ??= true; + } + + if ('discovery_splash' in data) { + /** + * The hash of the guild discovery splash image + * @type {?string} + */ + this.discoverySplash = data.discovery_splash; + } + + if ('member_count' in data) { + /** + * The full amount of members in this guild + * @type {number} + */ + this.memberCount = data.member_count; + } + + if ('large' in data) { + /** + * Whether the guild is "large" (has more than {@link WebsocketOptions large_threshold} members, 50 by default) + * @type {boolean} + */ + this.large = Boolean(data.large); + } + + if ('premium_progress_bar_enabled' in data) { + /** + * Whether this guild has its premium (boost) progress bar enabled + * @type {boolean} + */ + this.premiumProgressBarEnabled = data.premium_progress_bar_enabled; + } + + if ('application_id' in data) { + /** + * The id of the application that created this guild (if applicable) + * @type {?Snowflake} + */ + this.applicationId = data.application_id; + } + + if ('afk_timeout' in data) { + /** + * The time in seconds before a user is counted as "away from keyboard" + * @type {?number} + */ + this.afkTimeout = data.afk_timeout; + } + + if ('afk_channel_id' in data) { + /** + * The id of the voice channel where AFK members are moved + * @type {?Snowflake} + */ + this.afkChannelId = data.afk_channel_id; + } + + if ('system_channel_id' in data) { + /** + * The system channel's id + * @type {?Snowflake} + */ + this.systemChannelId = data.system_channel_id; + } + + if ('premium_tier' in data) { + /** + * The premium tier of this guild + * @type {GuildPremiumTier} + */ + this.premiumTier = data.premium_tier; + } + + if ('widget_enabled' in data) { + /** + * Whether widget images are enabled on this guild + * @type {?boolean} + */ + this.widgetEnabled = data.widget_enabled; + } else { + this.widgetEnabled ??= null; + } + + if ('widget_channel_id' in data) { + /** + * The widget channel's id, if enabled + * @type {?string} + */ + this.widgetChannelId = data.widget_channel_id; + } else { + this.widgetChannelId ??= null; + } + + if ('explicit_content_filter' in data) { + /** + * The explicit content filter level of the guild + * @type {GuildExplicitContentFilter} + */ + this.explicitContentFilter = data.explicit_content_filter; + } + + if ('mfa_level' in data) { + /** + * The required MFA level for this guild + * @type {GuildMFALevel} + */ + this.mfaLevel = data.mfa_level; + } + + if ('joined_at' in data) { + /** + * The timestamp the client user joined the guild at + * @type {number} + */ + this.joinedTimestamp = Date.parse(data.joined_at); + } + + if ('default_message_notifications' in data) { + /** + * The default message notification level of the guild + * @type {GuildDefaultMessageNotifications} + */ + this.defaultMessageNotifications = data.default_message_notifications; + } + + if ('system_channel_flags' in data) { + /** + * The value set for the guild's system channel flags + * @type {Readonly<SystemChannelFlagsBitField>} + */ + this.systemChannelFlags = new SystemChannelFlagsBitField(data.system_channel_flags).freeze(); + } + + if ('max_members' in data) { + /** + * The maximum amount of members the guild can have + * @type {?number} + */ + this.maximumMembers = data.max_members; + } else { + this.maximumMembers ??= null; + } + + if ('max_presences' in data) { + /** + * The maximum amount of presences the guild can have (this is `null` for all but the largest of guilds) + * <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info> + * @type {?number} + */ + this.maximumPresences = data.max_presences; + } else { + this.maximumPresences ??= null; + } + + if ('max_video_channel_users' in data) { + /** + * The maximum amount of users allowed in a video channel. + * @type {?number} + */ + this.maxVideoChannelUsers = data.max_video_channel_users; + } else { + this.maxVideoChannelUsers ??= null; + } + + if ('max_stage_video_channel_users' in data) { + /** + * The maximum amount of users allowed in a stage video channel. + * @type {?number} + */ + this.maxStageVideoChannelUsers = data.max_stage_video_channel_users; + } else { + this.maxStageVideoChannelUsers ??= null; + } + + if ('approximate_member_count' in data) { + /** + * The approximate amount of members the guild has + * <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info> + * @type {?number} + */ + this.approximateMemberCount = data.approximate_member_count; + } else { + this.approximateMemberCount ??= null; + } + + if ('approximate_presence_count' in data) { + /** + * The approximate amount of presences the guild has + * <info>You will need to fetch the guild using {@link Guild#fetch} if you want to receive this parameter</info> + * @type {?number} + */ + this.approximatePresenceCount = data.approximate_presence_count; + } else { + this.approximatePresenceCount ??= null; + } + + /** + * The use count of the vanity URL code of the guild, if any + * <info>You will need to fetch this parameter using {@link Guild#fetchVanityData} if you want to receive it</info> + * @type {?number} + */ + this.vanityURLUses ??= null; + + if ('rules_channel_id' in data) { + /** + * The rules channel's id for the guild + * @type {?Snowflake} + */ + this.rulesChannelId = data.rules_channel_id; + } + + if ('public_updates_channel_id' in data) { + /** + * The community updates channel's id for the guild + * @type {?Snowflake} + */ + this.publicUpdatesChannelId = data.public_updates_channel_id; + } + + if ('preferred_locale' in data) { + /** + * The preferred locale of the guild, defaults to `en-US` + * @type {Locale} + */ + this.preferredLocale = data.preferred_locale; + } + + if ('safety_alerts_channel_id' in data) { + /** + * The safety alerts channel's id for the guild + * @type {?Snowflake} + */ + this.safetyAlertsChannelId = data.safety_alerts_channel_id; + } else { + this.safetyAlertsChannelId ??= null; + } + + if (data.channels) { + this.channels.cache.clear(); + for (const rawChannel of data.channels) { + this.client.channels._add(rawChannel, this); + } + } + + if (data.threads) { + for (const rawThread of data.threads) { + this.client.channels._add(rawThread, this); + } + } + + if (data.roles) { + this.roles.cache.clear(); + for (const role of data.roles) this.roles._add(role); + } + + if (data.members) { + this.members.cache.clear(); + for (const guildUser of data.members) this.members._add(guildUser); + } + + if ('owner_id' in data) { + /** + * The user id of this guild's owner + * @type {Snowflake} + */ + this.ownerId = data.owner_id; + } + + if (data.presences) { + for (const presence of data.presences) { + this.presences._add(Object.assign(presence, { guild: this })); + } + } + + if (data.stage_instances) { + this.stageInstances.cache.clear(); + for (const stageInstance of data.stage_instances) { + this.stageInstances._add(stageInstance); + } + } + + if (data.guild_scheduled_events) { + this.scheduledEvents.cache.clear(); + for (const scheduledEvent of data.guild_scheduled_events) { + this.scheduledEvents._add(scheduledEvent); + } + } + + if (data.voice_states) { + this.voiceStates.cache.clear(); + for (const voiceState of data.voice_states) { + this.voiceStates._add(voiceState); + } + } + + if (!this.emojis) { + /** + * A manager of the emojis belonging to this guild + * @type {GuildEmojiManager} + */ + this.emojis = new GuildEmojiManager(this); + if (data.emojis) for (const emoji of data.emojis) this.emojis._add(emoji); + } else if (data.emojis) { + this.client.actions.GuildEmojisUpdate.handle({ + guild_id: this.id, + emojis: data.emojis, + }); + } + + if (!this.stickers) { + /** + * A manager of the stickers belonging to this guild + * @type {GuildStickerManager} + */ + this.stickers = new GuildStickerManager(this); + if (data.stickers) for (const sticker of data.stickers) this.stickers._add(sticker); + } else if (data.stickers) { + this.client.actions.GuildStickersUpdate.handle({ + guild_id: this.id, + stickers: data.stickers, + }); + } + } + + /** + * The time the client user joined the guild + * @type {Date} + * @readonly + */ + get joinedAt() { + return new Date(this.joinedTimestamp); + } + + /** + * The URL to this guild's discovery splash image. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + discoverySplashURL(options = {}) { + return this.discoverySplash && this.client.rest.cdn.discoverySplash(this.id, this.discoverySplash, options); + } + + /** + * Fetches the owner of the guild. + * If the member object isn't needed, use {@link Guild#ownerId} instead. + * @param {BaseFetchOptions} [options] The options for fetching the member + * @returns {Promise<GuildMember>} + */ + async fetchOwner(options) { + if (!this.ownerId) { + throw new DiscordjsError(ErrorCodes.FetchOwnerId); + } + const member = await this.members.fetch({ ...options, user: this.ownerId }); + return member; + } + + /** + * AFK voice channel for this guild + * @type {?VoiceChannel} + * @readonly + */ + get afkChannel() { + return this.client.channels.resolve(this.afkChannelId); + } + + /** + * System channel for this guild + * @type {?TextChannel} + * @readonly + */ + get systemChannel() { + return this.client.channels.resolve(this.systemChannelId); + } + + /** + * Widget channel for this guild + * @type {?(TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel)} + * @readonly + */ + get widgetChannel() { + return this.client.channels.resolve(this.widgetChannelId); + } + + /** + * Rules channel for this guild + * @type {?TextChannel} + * @readonly + */ + get rulesChannel() { + return this.client.channels.resolve(this.rulesChannelId); + } + + /** + * Public updates channel for this guild + * @type {?TextChannel} + * @readonly + */ + get publicUpdatesChannel() { + return this.client.channels.resolve(this.publicUpdatesChannelId); + } + + /** + * Safety alerts channel for this guild + * @type {?TextChannel} + * @readonly + */ + get safetyAlertsChannel() { + return this.client.channels.resolve(this.safetyAlertsChannelId); + } + + /** + * The maximum bitrate available for this guild + * @type {number} + * @readonly + */ + get maximumBitrate() { + if (this.features.includes(GuildFeature.VIPRegions)) { + return 384_000; + } + + switch (this.premiumTier) { + case GuildPremiumTier.Tier1: + return 128_000; + case GuildPremiumTier.Tier2: + return 256_000; + case GuildPremiumTier.Tier3: + return 384_000; + default: + return 96_000; + } + } + + /** + * Fetches a collection of integrations to this guild. + * Resolves with a collection mapping integrations by their ids. + * @returns {Promise<Collection<Snowflake|string, Integration>>} + * @example + * // Fetch integrations + * guild.fetchIntegrations() + * .then(integrations => console.log(`Fetched ${integrations.size} integrations`)) + * .catch(console.error); + */ + async fetchIntegrations() { + const data = await this.client.rest.get(Routes.guildIntegrations(this.id)); + return data.reduce( + (collection, integration) => collection.set(integration.id, new Integration(this.client, integration, this)), + new Collection(), + ); + } + + /** + * Fetches a collection of templates from this guild. + * Resolves with a collection mapping templates by their codes. + * @returns {Promise<Collection<string, GuildTemplate>>} + */ + async fetchTemplates() { + const templates = await this.client.rest.get(Routes.guildTemplates(this.id)); + return templates.reduce((col, data) => col.set(data.code, new GuildTemplate(this.client, data)), new Collection()); + } + + /** + * Fetches the welcome screen for this guild. + * @returns {Promise<WelcomeScreen>} + */ + async fetchWelcomeScreen() { + const data = await this.client.rest.get(Routes.guildWelcomeScreen(this.id)); + return new WelcomeScreen(this, data); + } + + /** + * Creates a template for the guild. + * @param {string} name The name for the template + * @param {string} [description] The description for the template + * @returns {Promise<GuildTemplate>} + */ + async createTemplate(name, description) { + const data = await this.client.rest.post(Routes.guildTemplates(this.id), { body: { name, description } }); + return new GuildTemplate(this.client, data); + } + + /** + * Obtains a guild preview for this guild from Discord. + * @returns {Promise<GuildPreview>} + */ + async fetchPreview() { + const data = await this.client.rest.get(Routes.guildPreview(this.id)); + return new GuildPreview(this.client, data); + } + + /** + * An object containing information about a guild's vanity invite. + * @typedef {Object} Vanity + * @property {?string} code Vanity invite code + * @property {number} uses How many times this invite has been used + */ + + /** + * Fetches the vanity URL invite object to this guild. + * Resolves with an object containing the vanity URL invite code and the use count + * @returns {Promise<Vanity>} + * @example + * // Fetch invite data + * guild.fetchVanityData() + * .then(res => { + * console.log(`Vanity URL: https://discord.gg/${res.code} with ${res.uses} uses`); + * }) + * .catch(console.error); + */ + async fetchVanityData() { + const data = await this.client.rest.get(Routes.guildVanityUrl(this.id)); + this.vanityURLCode = data.code; + this.vanityURLUses = data.uses; + + return data; + } + + /** + * Fetches all webhooks for the guild. + * @returns {Promise<Collection<Snowflake, Webhook>>} + * @example + * // Fetch webhooks + * guild.fetchWebhooks() + * .then(webhooks => console.log(`Fetched ${webhooks.size} webhooks`)) + * .catch(console.error); + */ + async fetchWebhooks() { + const apiHooks = await this.client.rest.get(Routes.guildWebhooks(this.id)); + const hooks = new Collection(); + for (const hook of apiHooks) hooks.set(hook.id, new Webhook(this.client, hook)); + return hooks; + } + + /** + * Fetches the guild widget data, requires the widget to be enabled. + * @returns {Promise<Widget>} + * @example + * // Fetches the guild widget data + * guild.fetchWidget() + * .then(widget => console.log(`The widget shows ${widget.channels.size} channels`)) + * .catch(console.error); + */ + fetchWidget() { + return this.client.fetchGuildWidget(this.id); + } + + /** + * Data for the Guild Widget Settings object + * @typedef {Object} GuildWidgetSettings + * @property {boolean} enabled Whether the widget is enabled + * @property {?(TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel)} channel The widget invite channel + */ + + /** + * The Guild Widget Settings object + * @typedef {Object} GuildWidgetSettingsData + * @property {boolean} enabled Whether the widget is enabled + * @property {?(TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel|Snowflake)} channel + * The widget invite channel + */ + + /** + * Fetches the guild widget settings. + * @returns {Promise<GuildWidgetSettings>} + * @example + * // Fetches the guild widget settings + * guild.fetchWidgetSettings() + * .then(widget => console.log(`The widget is ${widget.enabled ? 'enabled' : 'disabled'}`)) + * .catch(console.error); + */ + async fetchWidgetSettings() { + const data = await this.client.rest.get(Routes.guildWidgetSettings(this.id)); + this.widgetEnabled = data.enabled; + this.widgetChannelId = data.channel_id; + return { + enabled: data.enabled, + channel: data.channel_id ? this.channels.cache.get(data.channel_id) : null, + }; + } + + /** + * Options used to fetch audit logs. + * @typedef {Object} GuildAuditLogsFetchOptions + * @property {Snowflake|GuildAuditLogsEntry} [before] Consider only entries before this entry + * @property {Snowflake|GuildAuditLogsEntry} [after] Consider only entries after this entry + * @property {number} [limit] The number of entries to return + * @property {UserResolvable} [user] Only return entries for actions made by this user + * @property {?AuditLogEvent} [type] Only return entries for this action type + */ + + /** + * Fetches audit logs for this guild. + * @param {GuildAuditLogsFetchOptions} [options={}] Options for fetching audit logs + * @returns {Promise<GuildAuditLogs>} + * @example + * // Output audit log entries + * guild.fetchAuditLogs() + * .then(audit => console.log(audit.entries.first())) + * .catch(console.error); + */ + async fetchAuditLogs({ before, after, limit, user, type } = {}) { + const query = makeURLSearchParams({ + before: before?.id ?? before, + after: after?.id ?? after, + limit, + action_type: type, + }); + + if (user) { + const userId = this.client.users.resolveId(user); + if (!userId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'user', 'UserResolvable'); + query.set('user_id', userId); + } + + const data = await this.client.rest.get(Routes.guildAuditLog(this.id), { query }); + return new GuildAuditLogs(this, data); + } + + /** + * Fetches the guild onboarding data for this guild. + * @returns {Promise<GuildOnboarding>} + */ + async fetchOnboarding() { + const data = await this.client.rest.get(Routes.guildOnboarding(this.id)); + return new GuildOnboarding(this.client, data); + } + + /** + * The data for editing a guild. + * @typedef {Object} GuildEditOptions + * @property {string} [name] The name of the guild + * @property {?GuildVerificationLevel} [verificationLevel] The verification level of the guild + * @property {?GuildDefaultMessageNotifications} [defaultMessageNotifications] The default message + * notification level of the guild + * @property {?GuildExplicitContentFilter} [explicitContentFilter] The level of the explicit content filter + * @property {?VoiceChannelResolvable} [afkChannel] The AFK channel of the guild + * @property {number} [afkTimeout] The AFK timeout of the guild + * @property {?(BufferResolvable|Base64Resolvable)} [icon] The icon of the guild + * @property {GuildMemberResolvable} [owner] The owner of the guild + * @property {?(BufferResolvable|Base64Resolvable)} [splash] The invite splash image of the guild + * @property {?(BufferResolvable|Base64Resolvable)} [discoverySplash] The discovery splash image of the guild + * @property {?(BufferResolvable|Base64Resolvable)} [banner] The banner of the guild + * @property {?TextChannelResolvable} [systemChannel] The system channel of the guild + * @property {SystemChannelFlagsResolvable} [systemChannelFlags] The system channel flags of the guild + * @property {?TextChannelResolvable} [rulesChannel] The rules channel of the guild + * @property {?TextChannelResolvable} [publicUpdatesChannel] The community updates channel of the guild + * @property {?TextChannelResolvable} [safetyAlertsChannel] The safety alerts channel of the guild + * @property {?string} [preferredLocale] The preferred locale of the guild + * @property {GuildFeature[]} [features] The features of the guild + * @property {?string} [description] The discovery description of the guild + * @property {boolean} [premiumProgressBarEnabled] Whether the guild's premium progress bar is enabled + * @property {string} [reason] Reason for editing this guild + */ + + /** + * Data that can be resolved to a Text Channel object. This can be: + * * A TextChannel + * * A Snowflake + * @typedef {TextChannel|Snowflake} TextChannelResolvable + */ + + /** + * Data that can be resolved to a Voice Channel object. This can be: + * * A VoiceChannel + * * A Snowflake + * @typedef {VoiceChannel|Snowflake} VoiceChannelResolvable + */ + + /** + * Updates the guild with new information - e.g. a new name. + * @param {GuildEditOptions} options The options to provide + * @returns {Promise<Guild>} + * @example + * // Set the guild name + * guild.edit({ + * name: 'Discord Guild', + * }) + * .then(updated => console.log(`New guild name ${updated}`)) + * .catch(console.error); + */ + async edit({ + verificationLevel, + defaultMessageNotifications, + explicitContentFilter, + afkChannel, + afkTimeout, + icon, + owner, + splash, + discoverySplash, + banner, + systemChannel, + systemChannelFlags, + rulesChannel, + publicUpdatesChannel, + preferredLocale, + premiumProgressBarEnabled, + safetyAlertsChannel, + ...options + }) { + const data = await this.client.rest.patch(Routes.guild(this.id), { + body: { + ...options, + verification_level: verificationLevel, + default_message_notifications: defaultMessageNotifications, + explicit_content_filter: explicitContentFilter, + afk_channel_id: afkChannel && this.client.channels.resolveId(afkChannel), + afk_timeout: afkTimeout, + icon: icon && (await DataResolver.resolveImage(icon)), + owner_id: owner && this.client.users.resolveId(owner), + splash: splash && (await DataResolver.resolveImage(splash)), + discovery_splash: discoverySplash && (await DataResolver.resolveImage(discoverySplash)), + banner: banner && (await DataResolver.resolveImage(banner)), + system_channel_id: systemChannel && this.client.channels.resolveId(systemChannel), + system_channel_flags: + systemChannelFlags === undefined ? undefined : SystemChannelFlagsBitField.resolve(systemChannelFlags), + rules_channel_id: rulesChannel && this.client.channels.resolveId(rulesChannel), + public_updates_channel_id: publicUpdatesChannel && this.client.channels.resolveId(publicUpdatesChannel), + preferred_locale: preferredLocale, + premium_progress_bar_enabled: premiumProgressBarEnabled, + safety_alerts_channel_id: safetyAlertsChannel && this.client.channels.resolveId(safetyAlertsChannel), + }, + reason: options.reason, + }); + + return this.client.actions.GuildUpdate.handle(data).updated; + } + + /** + * Welcome channel data + * @typedef {Object} WelcomeChannelData + * @property {string} description The description to show for this welcome channel + * @property {TextChannel|NewsChannel|ForumChannel|Snowflake} channel The channel to link for this welcome channel + * @property {EmojiIdentifierResolvable} [emoji] The emoji to display for this welcome channel + */ + + /** + * Welcome screen edit data + * @typedef {Object} WelcomeScreenEditOptions + * @property {boolean} [enabled] Whether the welcome screen is enabled + * @property {string} [description] The description for the welcome screen + * @property {WelcomeChannelData[]} [welcomeChannels] The welcome channel data for the welcome screen + */ + + /** + * Data that can be resolved to a GuildTextChannel object. This can be: + * * A TextChannel + * * A NewsChannel + * * A Snowflake + * @typedef {TextChannel|NewsChannel|Snowflake} GuildTextChannelResolvable + */ + + /** + * Data that can be resolved to a GuildVoiceChannel object. This can be: + * * A VoiceChannel + * * A StageChannel + * * A Snowflake + * @typedef {VoiceChannel|StageChannel|Snowflake} GuildVoiceChannelResolvable + */ + + /** + * Updates the guild's welcome screen + * @param {WelcomeScreenEditOptions} options The options to provide + * @returns {Promise<WelcomeScreen>} + * @example + * guild.editWelcomeScreen({ + * description: 'Hello World', + * enabled: true, + * welcomeChannels: [ + * { + * description: 'foobar', + * channel: '222197033908436994', + * } + * ], + * }) + */ + async editWelcomeScreen(options) { + const { enabled, description, welcomeChannels } = options; + const welcome_channels = welcomeChannels?.map(welcomeChannelData => { + const emoji = this.emojis.resolve(welcomeChannelData.emoji); + return { + emoji_id: emoji?.id, + emoji_name: emoji?.name ?? welcomeChannelData.emoji, + channel_id: this.channels.resolveId(welcomeChannelData.channel), + description: welcomeChannelData.description, + }; + }); + + const patchData = await this.client.rest.patch(Routes.guildWelcomeScreen(this.id), { + body: { + welcome_channels, + description, + enabled, + }, + }); + return new WelcomeScreen(this, patchData); + } + + /** + * Edits the level of the explicit content filter. + * @param {?GuildExplicitContentFilter} explicitContentFilter The new level of the explicit content filter + * @param {string} [reason] Reason for changing the level of the guild's explicit content filter + * @returns {Promise<Guild>} + */ + setExplicitContentFilter(explicitContentFilter, reason) { + return this.edit({ explicitContentFilter, reason }); + } + + /** + * Edits the setting of the default message notifications of the guild. + * @param {?GuildDefaultMessageNotifications} defaultMessageNotifications + * The new default message notification level of the guild + * @param {string} [reason] Reason for changing the setting of the default message notifications + * @returns {Promise<Guild>} + */ + setDefaultMessageNotifications(defaultMessageNotifications, reason) { + return this.edit({ defaultMessageNotifications, reason }); + } + + /** + * Edits the flags of the default message notifications of the guild. + * @param {SystemChannelFlagsResolvable} systemChannelFlags The new flags for the default message notifications + * @param {string} [reason] Reason for changing the flags of the default message notifications + * @returns {Promise<Guild>} + */ + setSystemChannelFlags(systemChannelFlags, reason) { + return this.edit({ systemChannelFlags, reason }); + } + + /** + * Edits the name of the guild. + * @param {string} name The new name of the guild + * @param {string} [reason] Reason for changing the guild's name + * @returns {Promise<Guild>} + * @example + * // Edit the guild name + * guild.setName('Discord Guild') + * .then(updated => console.log(`Updated guild name to ${updated.name}`)) + * .catch(console.error); + */ + setName(name, reason) { + return this.edit({ name, reason }); + } + + /** + * Edits the verification level of the guild. + * @param {?GuildVerificationLevel} verificationLevel The new verification level of the guild + * @param {string} [reason] Reason for changing the guild's verification level + * @returns {Promise<Guild>} + * @example + * // Edit the guild verification level + * guild.setVerificationLevel(1) + * .then(updated => console.log(`Updated guild verification level to ${guild.verificationLevel}`)) + * .catch(console.error); + */ + setVerificationLevel(verificationLevel, reason) { + return this.edit({ verificationLevel, reason }); + } + + /** + * Edits the AFK channel of the guild. + * @param {?VoiceChannelResolvable} afkChannel The new AFK channel + * @param {string} [reason] Reason for changing the guild's AFK channel + * @returns {Promise<Guild>} + * @example + * // Edit the guild AFK channel + * guild.setAFKChannel(channel) + * .then(updated => console.log(`Updated guild AFK channel to ${guild.afkChannel.name}`)) + * .catch(console.error); + */ + setAFKChannel(afkChannel, reason) { + return this.edit({ afkChannel, reason }); + } + + /** + * Edits the system channel of the guild. + * @param {?TextChannelResolvable} systemChannel The new system channel + * @param {string} [reason] Reason for changing the guild's system channel + * @returns {Promise<Guild>} + * @example + * // Edit the guild system channel + * guild.setSystemChannel(channel) + * .then(updated => console.log(`Updated guild system channel to ${guild.systemChannel.name}`)) + * .catch(console.error); + */ + setSystemChannel(systemChannel, reason) { + return this.edit({ systemChannel, reason }); + } + + /** + * Edits the AFK timeout of the guild. + * @param {number} afkTimeout The time in seconds that a user must be idle to be considered AFK + * @param {string} [reason] Reason for changing the guild's AFK timeout + * @returns {Promise<Guild>} + * @example + * // Edit the guild AFK channel + * guild.setAFKTimeout(60) + * .then(updated => console.log(`Updated guild AFK timeout to ${guild.afkTimeout}`)) + * .catch(console.error); + */ + setAFKTimeout(afkTimeout, reason) { + return this.edit({ afkTimeout, reason }); + } + + /** + * Sets a new guild icon. + * @param {?(Base64Resolvable|BufferResolvable)} icon The new icon of the guild + * @param {string} [reason] Reason for changing the guild's icon + * @returns {Promise<Guild>} + * @example + * // Edit the guild icon + * guild.setIcon('./icon.png') + * .then(updated => console.log('Updated the guild icon')) + * .catch(console.error); + */ + setIcon(icon, reason) { + return this.edit({ icon, reason }); + } + + /** + * Sets a new owner of the guild. + * @param {GuildMemberResolvable} owner The new owner of the guild + * @param {string} [reason] Reason for setting the new owner + * @returns {Promise<Guild>} + * @example + * // Edit the guild owner + * guild.setOwner(guild.members.cache.first()) + * .then(guild => guild.fetchOwner()) + * .then(owner => console.log(`Updated the guild owner to ${owner.displayName}`)) + * .catch(console.error); + */ + setOwner(owner, reason) { + return this.edit({ owner, reason }); + } + + /** + * Sets a new guild invite splash image. + * @param {?(Base64Resolvable|BufferResolvable)} splash The new invite splash image of the guild + * @param {string} [reason] Reason for changing the guild's invite splash image + * @returns {Promise<Guild>} + * @example + * // Edit the guild splash + * guild.setSplash('./splash.png') + * .then(updated => console.log('Updated the guild splash')) + * .catch(console.error); + */ + setSplash(splash, reason) { + return this.edit({ splash, reason }); + } + + /** + * Sets a new guild discovery splash image. + * @param {?(Base64Resolvable|BufferResolvable)} discoverySplash The new discovery splash image of the guild + * @param {string} [reason] Reason for changing the guild's discovery splash image + * @returns {Promise<Guild>} + * @example + * // Edit the guild discovery splash + * guild.setDiscoverySplash('./discoverysplash.png') + * .then(updated => console.log('Updated the guild discovery splash')) + * .catch(console.error); + */ + setDiscoverySplash(discoverySplash, reason) { + return this.edit({ discoverySplash, reason }); + } + + /** + * Sets a new guild banner. + * @param {?(Base64Resolvable|BufferResolvable)} banner The new banner of the guild + * @param {string} [reason] Reason for changing the guild's banner + * @returns {Promise<Guild>} + * @example + * guild.setBanner('./banner.png') + * .then(updated => console.log('Updated the guild banner')) + * .catch(console.error); + */ + setBanner(banner, reason) { + return this.edit({ banner, reason }); + } + + /** + * Edits the rules channel of the guild. + * @param {?TextChannelResolvable} rulesChannel The new rules channel + * @param {string} [reason] Reason for changing the guild's rules channel + * @returns {Promise<Guild>} + * @example + * // Edit the guild rules channel + * guild.setRulesChannel(channel) + * .then(updated => console.log(`Updated guild rules channel to ${guild.rulesChannel.name}`)) + * .catch(console.error); + */ + setRulesChannel(rulesChannel, reason) { + return this.edit({ rulesChannel, reason }); + } + + /** + * Edits the community updates channel of the guild. + * @param {?TextChannelResolvable} publicUpdatesChannel The new community updates channel + * @param {string} [reason] Reason for changing the guild's community updates channel + * @returns {Promise<Guild>} + * @example + * // Edit the guild community updates channel + * guild.setPublicUpdatesChannel(channel) + * .then(updated => console.log(`Updated guild community updates channel to ${guild.publicUpdatesChannel.name}`)) + * .catch(console.error); + */ + setPublicUpdatesChannel(publicUpdatesChannel, reason) { + return this.edit({ publicUpdatesChannel, reason }); + } + + /** + * Edits the preferred locale of the guild. + * @param {?Locale} preferredLocale The new preferred locale of the guild + * @param {string} [reason] Reason for changing the guild's preferred locale + * @returns {Promise<Guild>} + * @example + * // Edit the guild preferred locale + * guild.setPreferredLocale('en-US') + * .then(updated => console.log(`Updated guild preferred locale to ${guild.preferredLocale}`)) + * .catch(console.error); + */ + setPreferredLocale(preferredLocale, reason) { + return this.edit({ preferredLocale, reason }); + } + + /** + * Edits the enabled state of the guild's premium progress bar + * @param {boolean} [enabled=true] The new enabled state of the guild's premium progress bar + * @param {string} [reason] Reason for changing the state of the guild's premium progress bar + * @returns {Promise<Guild>} + */ + setPremiumProgressBarEnabled(enabled = true, reason) { + return this.edit({ premiumProgressBarEnabled: enabled, reason }); + } + + /** + * Edits the safety alerts channel of the guild. + * @param {?TextChannelResolvable} safetyAlertsChannel The new safety alerts channel + * @param {string} [reason] Reason for changing the guild's safety alerts channel + * @returns {Promise<Guild>} + * @example + * // Edit the guild safety alerts channel + * guild.setSafetyAlertsChannel(channel) + * .then(updated => console.log(`Updated guild safety alerts channel to ${updated.safetyAlertsChannel.name}`)) + * .catch(console.error); + */ + setSafetyAlertsChannel(safetyAlertsChannel, reason) { + return this.edit({ safetyAlertsChannel, reason }); + } + + /** + * Edits the guild's widget settings. + * @param {GuildWidgetSettingsData} settings The widget settings for the guild + * @param {string} [reason] Reason for changing the guild's widget settings + * @returns {Promise<Guild>} + */ + async setWidgetSettings(settings, reason) { + await this.client.rest.patch(Routes.guildWidgetSettings(this.id), { + body: { + enabled: settings.enabled, + channel_id: this.channels.resolveId(settings.channel), + }, + reason, + }); + return this; + } + + /** + * Sets the guild's MFA level + * <info>An elevated MFA level requires guild moderators to have 2FA enabled.</info> + * @param {GuildMFALevel} level The MFA level + * @param {string} [reason] Reason for changing the guild's MFA level + * @returns {Promise<Guild>} + * @example + * // Set the MFA level of the guild to Elevated + * guild.setMFALevel(GuildMFALevel.Elevated) + * .then(guild => console.log("Set guild's MFA level to Elevated")) + * .catch(console.error); + */ + async setMFALevel(level, reason) { + await this.client.rest.post(Routes.guildMFA(this.id), { + body: { + level, + }, + reason, + }); + return this; + } + + /** + * Leaves the guild. + * @returns {Promise<Guild>} + * @example + * // Leave a guild + * guild.leave() + * .then(guild => console.log(`Left the guild: ${guild.name}`)) + * .catch(console.error); + */ + async leave() { + if (this.ownerId === this.client.user.id) throw new DiscordjsError(ErrorCodes.GuildOwned); + await this.client.rest.delete(Routes.userGuild(this.id)); + return this; + } + + /** + * Deletes the guild. + * @returns {Promise<Guild>} + * @example + * // Delete a guild + * guild.delete() + * .then(g => console.log(`Deleted the guild ${g}`)) + * .catch(console.error); + */ + async delete() { + await this.client.rest.delete(Routes.guild(this.id)); + return this; + } + + /** + * Sets whether this guild's invites are disabled. + * @param {boolean} [disabled=true] Whether the invites are disabled + * @returns {Promise<Guild>} + */ + async disableInvites(disabled = true) { + const features = this.features.filter(feature => feature !== GuildFeature.InvitesDisabled); + if (disabled) features.push(GuildFeature.InvitesDisabled); + return this.edit({ features }); + } + + /** + * Whether this guild equals another guild. It compares all properties, so for most operations + * it is advisable to just compare `guild.id === guild2.id` as it is much faster and is often + * what most users need. + * @param {Guild} guild The guild to compare with + * @returns {boolean} + */ + equals(guild) { + return ( + guild && + guild instanceof this.constructor && + this.id === guild.id && + this.available === guild.available && + this.splash === guild.splash && + this.discoverySplash === guild.discoverySplash && + this.name === guild.name && + this.memberCount === guild.memberCount && + this.large === guild.large && + this.icon === guild.icon && + this.ownerId === guild.ownerId && + this.verificationLevel === guild.verificationLevel && + (this.features === guild.features || + (this.features.length === guild.features.length && + this.features.every((feat, i) => feat === guild.features[i]))) + ); + } + + toJSON() { + const json = super.toJSON({ + available: false, + createdTimestamp: true, + nameAcronym: true, + presences: false, + voiceStates: false, + }); + json.iconURL = this.iconURL(); + json.splashURL = this.splashURL(); + json.discoverySplashURL = this.discoverySplashURL(); + json.bannerURL = this.bannerURL(); + return json; + } + + /** + * The voice state adapter for this guild that can be used with @discordjs/voice to play audio in voice + * and stage channels. + * @type {Function} + * @readonly + */ + get voiceAdapterCreator() { + return methods => { + this.client.voice.adapters.set(this.id, methods); + return { + sendPayload: data => { + if (this.shard.status !== Status.Ready) return false; + this.shard.send(data); + return true; + }, + destroy: () => { + this.client.voice.adapters.delete(this.id); + }, + }; + }; + } + + /** + * Creates a collection of this guild's roles, sorted by their position and ids. + * @returns {Collection<Snowflake, Role>} + * @private + */ + _sortedRoles() { + return discordSort(this.roles.cache); + } + + /** + * Creates a collection of this guild's or a specific category's channels, sorted by their position and ids. + * @param {GuildChannel} [channel] Category to get the channels of + * @returns {Collection<Snowflake, GuildChannel>} + * @private + */ + _sortedChannels(channel) { + const channelIsCategory = channel.type === ChannelType.GuildCategory; + const types = getSortableGroupTypes(channel.type); + return discordSort( + this.channels.cache.filter(c => types.includes(c.type) && (channelIsCategory || c.parentId === channel.parentId)), + ); + } +} + +exports.Guild = Guild; + +/** + * @external APIGuild + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object} + */ diff --git a/node_modules/discord.js/src/structures/GuildAuditLogs.js b/node_modules/discord.js/src/structures/GuildAuditLogs.js new file mode 100644 index 0000000..2ce13a8 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildAuditLogs.js @@ -0,0 +1,91 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const ApplicationCommand = require('./ApplicationCommand'); +const GuildAuditLogsEntry = require('./GuildAuditLogsEntry'); +const Integration = require('./Integration'); +const Webhook = require('./Webhook'); +const { flatten } = require('../util/Util'); + +/** + * Audit logs entries are held in this class. + */ +class GuildAuditLogs { + constructor(guild, data) { + if (data.users) for (const user of data.users) guild.client.users._add(user); + if (data.threads) for (const thread of data.threads) guild.client.channels._add(thread, guild); + /** + * Cached webhooks + * @type {Collection<Snowflake, Webhook>} + * @private + */ + this.webhooks = new Collection(); + if (data.webhooks) { + for (const hook of data.webhooks) { + this.webhooks.set(hook.id, new Webhook(guild.client, hook)); + } + } + + /** + * Cached integrations + * @type {Collection<Snowflake|string, Integration>} + * @private + */ + this.integrations = new Collection(); + if (data.integrations) { + for (const integration of data.integrations) { + this.integrations.set(integration.id, new Integration(guild.client, integration, guild)); + } + } + + /** + * Cached {@link GuildScheduledEvent}s. + * @type {Collection<Snowflake, GuildScheduledEvent>} + * @private + */ + this.guildScheduledEvents = data.guild_scheduled_events.reduce( + (guildScheduledEvents, guildScheduledEvent) => + guildScheduledEvents.set(guildScheduledEvent.id, guild.scheduledEvents._add(guildScheduledEvent)), + new Collection(), + ); + + /** + * Cached application commands, includes application commands from other applications + * @type {Collection<Snowflake, ApplicationCommand>} + * @private + */ + this.applicationCommands = new Collection(); + if (data.application_commands) { + for (const command of data.application_commands) { + this.applicationCommands.set(command.id, new ApplicationCommand(guild.client, command, guild)); + } + } + + /** + * Cached auto moderation rules. + * @type {Collection<Snowflake, AutoModerationRule>} + * @private + */ + this.autoModerationRules = data.auto_moderation_rules.reduce( + (autoModerationRules, autoModerationRule) => + autoModerationRules.set(autoModerationRule.id, guild.autoModerationRules._add(autoModerationRule)), + new Collection(), + ); + + /** + * The entries for this guild's audit logs + * @type {Collection<Snowflake, GuildAuditLogsEntry>} + */ + this.entries = new Collection(); + for (const item of data.audit_log_entries) { + const entry = new GuildAuditLogsEntry(guild, item, this); + this.entries.set(entry.id, entry); + } + } + + toJSON() { + return flatten(this); + } +} + +module.exports = GuildAuditLogs; diff --git a/node_modules/discord.js/src/structures/GuildAuditLogsEntry.js b/node_modules/discord.js/src/structures/GuildAuditLogsEntry.js new file mode 100644 index 0000000..febbd12 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildAuditLogsEntry.js @@ -0,0 +1,528 @@ +'use strict'; + +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { AuditLogOptionsType, AuditLogEvent } = require('discord-api-types/v10'); +const AutoModerationRule = require('./AutoModerationRule'); +const { GuildScheduledEvent } = require('./GuildScheduledEvent'); +const Integration = require('./Integration'); +const Invite = require('./Invite'); +const { StageInstance } = require('./StageInstance'); +const { Sticker } = require('./Sticker'); +const Webhook = require('./Webhook'); +const Partials = require('../util/Partials'); +const { flatten } = require('../util/Util'); + +const Targets = { + All: 'All', + Guild: 'Guild', + GuildScheduledEvent: 'GuildScheduledEvent', + Channel: 'Channel', + User: 'User', + Role: 'Role', + Invite: 'Invite', + Webhook: 'Webhook', + Emoji: 'Emoji', + Message: 'Message', + Integration: 'Integration', + StageInstance: 'StageInstance', + Sticker: 'Sticker', + Thread: 'Thread', + ApplicationCommand: 'ApplicationCommand', + AutoModeration: 'AutoModeration', + Unknown: 'Unknown', +}; + +/** + * The target of a guild audit log entry. It can be one of: + * * A guild + * * A channel + * * A user + * * A role + * * An invite + * * A webhook + * * An emoji + * * A message + * * An integration + * * A stage instance + * * A sticker + * * A guild scheduled event + * * A thread + * * An application command + * * An auto moderation rule + * * An object with an id key if target was deleted or fake entity + * * An object where the keys represent either the new value or the old value + * @typedef {?(Object|Guild|BaseChannel|User|Role|Invite|Webhook|GuildEmoji|Message|Integration|StageInstance|Sticker| + * GuildScheduledEvent|ApplicationCommand|AutoModerationRule)} AuditLogEntryTarget + */ + +/** + * The action type of an entry, e.g. `Create`. Here are the available types: + * * Create + * * Delete + * * Update + * * All + * @typedef {string} AuditLogActionType + */ + +/** + * The target type of an entry. Here are the available types: + * * Guild + * * Channel + * * User + * * Role + * * Invite + * * Webhook + * * Emoji + * * Message + * * Integration + * * StageInstance + * * Sticker + * * Thread + * * GuildScheduledEvent + * * ApplicationCommandPermission + * @typedef {string} AuditLogTargetType + */ + +/** + * Audit logs entry. + */ +class GuildAuditLogsEntry { + /** + * Key mirror of all available audit log targets. + * @type {Object<string, string>} + * @memberof GuildAuditLogsEntry + */ + static Targets = Targets; + + constructor(guild, data, logs) { + /** + * The target type of this entry + * @type {AuditLogTargetType} + */ + this.targetType = GuildAuditLogsEntry.targetType(data.action_type); + const targetType = this.targetType; + + /** + * The action type of this entry + * @type {AuditLogActionType} + */ + this.actionType = GuildAuditLogsEntry.actionType(data.action_type); + + /** + * The type of action that occurred. + * @type {AuditLogEvent} + */ + this.action = data.action_type; + + /** + * The reason of this entry + * @type {?string} + */ + this.reason = data.reason ?? null; + + /** + * The id of the user that executed this entry + * @type {?Snowflake} + */ + this.executorId = data.user_id; + + /** + * The user that executed this entry + * @type {?User} + */ + this.executor = data.user_id + ? guild.client.options.partials.includes(Partials.User) + ? guild.client.users._add({ id: data.user_id }) + : guild.client.users.cache.get(data.user_id) ?? null + : null; + + /** + * An entry in the audit log representing a specific change. + * @typedef {Object} AuditLogChange + * @property {string} key The property that was changed, e.g. `nick` for nickname changes + * <warn>For application command permissions updates the key is the id of the user, channel, + * role, or a permission constant that was updated instead of an actual property name</warn> + * @property {*} [old] The old value of the change, e.g. for nicknames, the old nickname + * @property {*} [new] The new value of the change, e.g. for nicknames, the new nickname + */ + + /** + * Specific property changes + * @type {AuditLogChange[]} + */ + this.changes = data.changes?.map(c => ({ key: c.key, old: c.old_value, new: c.new_value })) ?? []; + + /** + * The entry's id + * @type {Snowflake} + */ + this.id = data.id; + + /** + * Any extra data from the entry + * @type {?(Object|Role|GuildMember)} + */ + this.extra = null; + switch (data.action_type) { + case AuditLogEvent.MemberPrune: + this.extra = { + removed: Number(data.options.members_removed), + days: Number(data.options.delete_member_days), + }; + break; + + case AuditLogEvent.MemberMove: + case AuditLogEvent.MessageDelete: + case AuditLogEvent.MessageBulkDelete: + this.extra = { + channel: guild.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id }, + count: Number(data.options.count), + }; + break; + + case AuditLogEvent.MessagePin: + case AuditLogEvent.MessageUnpin: + this.extra = { + channel: guild.client.channels.cache.get(data.options.channel_id) ?? { id: data.options.channel_id }, + messageId: data.options.message_id, + }; + break; + + case AuditLogEvent.MemberDisconnect: + this.extra = { + count: Number(data.options.count), + }; + break; + + case AuditLogEvent.ChannelOverwriteCreate: + case AuditLogEvent.ChannelOverwriteUpdate: + case AuditLogEvent.ChannelOverwriteDelete: + switch (data.options.type) { + case AuditLogOptionsType.Role: + this.extra = guild.roles.cache.get(data.options.id) ?? { + id: data.options.id, + name: data.options.role_name, + type: AuditLogOptionsType.Role, + }; + break; + + case AuditLogOptionsType.Member: + this.extra = guild.members.cache.get(data.options.id) ?? { + id: data.options.id, + type: AuditLogOptionsType.Member, + }; + break; + + default: + break; + } + break; + + case AuditLogEvent.StageInstanceCreate: + case AuditLogEvent.StageInstanceDelete: + case AuditLogEvent.StageInstanceUpdate: + this.extra = { + channel: guild.client.channels.cache.get(data.options?.channel_id) ?? { id: data.options?.channel_id }, + }; + break; + + case AuditLogEvent.ApplicationCommandPermissionUpdate: + this.extra = { + applicationId: data.options.application_id, + }; + break; + + case AuditLogEvent.AutoModerationBlockMessage: + case AuditLogEvent.AutoModerationFlagToChannel: + case AuditLogEvent.AutoModerationUserCommunicationDisabled: + this.extra = { + autoModerationRuleName: data.options.auto_moderation_rule_name, + autoModerationRuleTriggerType: data.options.auto_moderation_rule_trigger_type, + channel: guild.client.channels.cache.get(data.options?.channel_id) ?? { id: data.options?.channel_id }, + }; + break; + + default: + break; + } + + /** + * The id of the target of this entry + * @type {?Snowflake} + */ + this.targetId = data.target_id; + + /** + * The target of this entry + * @type {?AuditLogEntryTarget} + */ + this.target = null; + if (targetType === Targets.Unknown) { + this.target = this.changes.reduce((o, c) => { + o[c.key] = c.new ?? c.old; + return o; + }, {}); + this.target.id = data.target_id; + // MemberDisconnect and similar types do not provide a target_id. + } else if (targetType === Targets.User && data.target_id) { + this.target = guild.client.options.partials.includes(Partials.User) + ? guild.client.users._add({ id: data.target_id }) + : guild.client.users.cache.get(data.target_id) ?? null; + } else if (targetType === Targets.Guild) { + this.target = guild.client.guilds.cache.get(data.target_id); + } else if (targetType === Targets.Webhook) { + this.target = + logs?.webhooks.get(data.target_id) ?? + new Webhook( + guild.client, + this.changes.reduce( + (o, c) => { + o[c.key] = c.new ?? c.old; + return o; + }, + { + id: data.target_id, + guild_id: guild.id, + }, + ), + ); + } else if (targetType === Targets.Invite) { + let change = this.changes.find(c => c.key === 'code'); + change = change.new ?? change.old; + + this.target = + guild.invites.cache.get(change) ?? + new Invite( + guild.client, + this.changes.reduce( + (o, c) => { + o[c.key] = c.new ?? c.old; + return o; + }, + { guild }, + ), + ); + } else if (targetType === Targets.Message) { + // Discord sends a channel id for the MessageBulkDelete action type. + this.target = + data.action_type === AuditLogEvent.MessageBulkDelete + ? guild.channels.cache.get(data.target_id) ?? { id: data.target_id } + : guild.client.users.cache.get(data.target_id) ?? null; + } else if (targetType === Targets.Integration) { + this.target = + logs?.integrations.get(data.target_id) ?? + new Integration( + guild.client, + this.changes.reduce( + (o, c) => { + o[c.key] = c.new ?? c.old; + return o; + }, + { id: data.target_id }, + ), + guild, + ); + } else if (targetType === Targets.Channel || targetType === Targets.Thread) { + this.target = + guild.channels.cache.get(data.target_id) ?? + this.changes.reduce( + (o, c) => { + o[c.key] = c.new ?? c.old; + return o; + }, + { id: data.target_id }, + ); + } else if (targetType === Targets.StageInstance) { + this.target = + guild.stageInstances.cache.get(data.target_id) ?? + new StageInstance( + guild.client, + this.changes.reduce( + (o, c) => { + o[c.key] = c.new ?? c.old; + return o; + }, + { + id: data.target_id, + channel_id: data.options?.channel_id, + guild_id: guild.id, + }, + ), + ); + } else if (targetType === Targets.Sticker) { + this.target = + guild.stickers.cache.get(data.target_id) ?? + new Sticker( + guild.client, + this.changes.reduce( + (o, c) => { + o[c.key] = c.new ?? c.old; + return o; + }, + { id: data.target_id }, + ), + ); + } else if (targetType === Targets.GuildScheduledEvent) { + this.target = + guild.scheduledEvents.cache.get(data.target_id) ?? + new GuildScheduledEvent( + guild.client, + this.changes.reduce( + (o, c) => { + o[c.key] = c.new ?? c.old; + return o; + }, + { id: data.target_id, guild_id: guild.id }, + ), + ); + } else if (targetType === Targets.ApplicationCommand) { + this.target = logs?.applicationCommands.get(data.target_id) ?? { id: data.target_id }; + } else if (targetType === Targets.AutoModeration) { + this.target = + guild.autoModerationRules.cache.get(data.target_id) ?? + new AutoModerationRule( + guild.client, + this.changes.reduce( + (o, c) => { + o[c.key] = c.new ?? c.old; + return o; + }, + { id: data.target_id, guild_id: guild.id }, + ), + guild, + ); + } else if (data.target_id) { + this.target = guild[`${targetType.toLowerCase()}s`]?.cache.get(data.target_id) ?? { id: data.target_id }; + } + } + + /** + * Finds the target type of a guild audit log entry. + * @param {AuditLogEvent} target The action target + * @returns {AuditLogTargetType} + */ + static targetType(target) { + if (target < 10) return Targets.Guild; + if (target < 20) return Targets.Channel; + if (target < 30) return Targets.User; + if (target < 40) return Targets.Role; + if (target < 50) return Targets.Invite; + if (target < 60) return Targets.Webhook; + if (target < 70) return Targets.Emoji; + if (target < 80) return Targets.Message; + if (target < 83) return Targets.Integration; + if (target < 86) return Targets.StageInstance; + if (target < 100) return Targets.Sticker; + if (target < 110) return Targets.GuildScheduledEvent; + if (target < 120) return Targets.Thread; + if (target < 130) return Targets.ApplicationCommand; + if (target >= 140 && target < 150) return Targets.AutoModeration; + return Targets.Unknown; + } + + /** + * Finds the action type from the guild audit log entry action. + * @param {AuditLogEvent} action The action target + * @returns {AuditLogActionType} + */ + static actionType(action) { + if ( + [ + AuditLogEvent.ChannelCreate, + AuditLogEvent.ChannelOverwriteCreate, + AuditLogEvent.MemberBanRemove, + AuditLogEvent.BotAdd, + AuditLogEvent.RoleCreate, + AuditLogEvent.InviteCreate, + AuditLogEvent.WebhookCreate, + AuditLogEvent.EmojiCreate, + AuditLogEvent.MessagePin, + AuditLogEvent.IntegrationCreate, + AuditLogEvent.StageInstanceCreate, + AuditLogEvent.StickerCreate, + AuditLogEvent.GuildScheduledEventCreate, + AuditLogEvent.ThreadCreate, + AuditLogEvent.AutoModerationRuleCreate, + AuditLogEvent.AutoModerationBlockMessage, + ].includes(action) + ) { + return 'Create'; + } + + if ( + [ + AuditLogEvent.ChannelDelete, + AuditLogEvent.ChannelOverwriteDelete, + AuditLogEvent.MemberKick, + AuditLogEvent.MemberPrune, + AuditLogEvent.MemberBanAdd, + AuditLogEvent.MemberDisconnect, + AuditLogEvent.RoleDelete, + AuditLogEvent.InviteDelete, + AuditLogEvent.WebhookDelete, + AuditLogEvent.EmojiDelete, + AuditLogEvent.MessageDelete, + AuditLogEvent.MessageBulkDelete, + AuditLogEvent.MessageUnpin, + AuditLogEvent.IntegrationDelete, + AuditLogEvent.StageInstanceDelete, + AuditLogEvent.StickerDelete, + AuditLogEvent.GuildScheduledEventDelete, + AuditLogEvent.ThreadDelete, + AuditLogEvent.AutoModerationRuleDelete, + ].includes(action) + ) { + return 'Delete'; + } + + if ( + [ + AuditLogEvent.GuildUpdate, + AuditLogEvent.ChannelUpdate, + AuditLogEvent.ChannelOverwriteUpdate, + AuditLogEvent.MemberUpdate, + AuditLogEvent.MemberRoleUpdate, + AuditLogEvent.MemberMove, + AuditLogEvent.RoleUpdate, + AuditLogEvent.InviteUpdate, + AuditLogEvent.WebhookUpdate, + AuditLogEvent.EmojiUpdate, + AuditLogEvent.IntegrationUpdate, + AuditLogEvent.StageInstanceUpdate, + AuditLogEvent.StickerUpdate, + AuditLogEvent.GuildScheduledEventUpdate, + AuditLogEvent.ThreadUpdate, + AuditLogEvent.ApplicationCommandPermissionUpdate, + AuditLogEvent.AutoModerationRuleUpdate, + ].includes(action) + ) { + return 'Update'; + } + + return 'All'; + } + + /** + * The timestamp this entry was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time this entry was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + toJSON() { + return flatten(this, { createdTimestamp: true }); + } +} + +module.exports = GuildAuditLogsEntry; diff --git a/node_modules/discord.js/src/structures/GuildBan.js b/node_modules/discord.js/src/structures/GuildBan.js new file mode 100644 index 0000000..9c5a10e --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildBan.js @@ -0,0 +1,59 @@ +'use strict'; + +const Base = require('./Base'); + +/** + * Represents a ban in a guild on Discord. + * @extends {Base} + */ +class GuildBan extends Base { + constructor(client, data, guild) { + super(client); + + /** + * The guild in which the ban is + * @type {Guild} + */ + this.guild = guild; + + this._patch(data); + } + + _patch(data) { + if ('user' in data) { + /** + * The user this ban applies to + * @type {User} + */ + this.user = this.client.users._add(data.user, true); + } + + if ('reason' in data) { + /** + * The reason for the ban + * @type {?string} + */ + this.reason = data.reason; + } + } + + /** + * Whether this GuildBan is partial. If the reason is not provided the value is null + * @type {boolean} + * @readonly + */ + get partial() { + return !('reason' in this); + } + + /** + * Fetches this GuildBan. + * @param {boolean} [force=true] Whether to skip the cache check and request the API + * @returns {Promise<GuildBan>} + */ + fetch(force = true) { + return this.guild.bans.fetch({ user: this.user, cache: true, force }); + } +} + +module.exports = GuildBan; diff --git a/node_modules/discord.js/src/structures/GuildChannel.js b/node_modules/discord.js/src/structures/GuildChannel.js new file mode 100644 index 0000000..c066c71 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildChannel.js @@ -0,0 +1,472 @@ +'use strict'; + +const { Snowflake } = require('@sapphire/snowflake'); +const { PermissionFlagsBits, ChannelType } = require('discord-api-types/v10'); +const { BaseChannel } = require('./BaseChannel'); +const { DiscordjsError, ErrorCodes } = require('../errors'); +const PermissionOverwriteManager = require('../managers/PermissionOverwriteManager'); +const { VoiceBasedChannelTypes } = require('../util/Constants'); +const PermissionsBitField = require('../util/PermissionsBitField'); +const { getSortableGroupTypes } = require('../util/Util'); + +/** + * Represents a guild channel from any of the following: + * - {@link TextChannel} + * - {@link VoiceChannel} + * - {@link CategoryChannel} + * - {@link NewsChannel} + * - {@link StageChannel} + * - {@link ForumChannel} + * @extends {BaseChannel} + * @abstract + */ +class GuildChannel extends BaseChannel { + constructor(guild, data, client, immediatePatch = true) { + super(client, data, false); + + /** + * The guild the channel is in + * @type {Guild} + */ + this.guild = guild; + + /** + * The id of the guild the channel is in + * @type {Snowflake} + */ + this.guildId = guild?.id ?? data.guild_id; + /** + * A manager of permission overwrites that belong to this channel + * @type {PermissionOverwriteManager} + */ + this.permissionOverwrites = new PermissionOverwriteManager(this); + + if (data && immediatePatch) this._patch(data); + } + + _patch(data) { + super._patch(data); + + if ('name' in data) { + /** + * The name of the guild channel + * @type {string} + */ + this.name = data.name; + } + + if ('position' in data) { + /** + * The raw position of the channel from Discord + * @type {number} + */ + this.rawPosition = data.position; + } + + if ('guild_id' in data) { + this.guildId = data.guild_id; + } + + if ('parent_id' in data) { + /** + * The id of the category parent of this channel + * @type {?Snowflake} + */ + this.parentId = data.parent_id; + } else { + this.parentId ??= null; + } + + if ('permission_overwrites' in data) { + this.permissionOverwrites.cache.clear(); + for (const overwrite of data.permission_overwrites) { + this.permissionOverwrites._add(overwrite); + } + } + } + + _clone() { + const clone = super._clone(); + clone.permissionOverwrites = new PermissionOverwriteManager(clone, this.permissionOverwrites.cache.values()); + return clone; + } + + /** + * The category parent of this channel + * @type {?CategoryChannel} + * @readonly + */ + get parent() { + return this.guild.channels.resolve(this.parentId); + } + + /** + * If the permissionOverwrites match the parent channel, null if no parent + * @type {?boolean} + * @readonly + */ + get permissionsLocked() { + if (!this.parent) return null; + + // Get all overwrites + const overwriteIds = new Set([ + ...this.permissionOverwrites.cache.keys(), + ...this.parent.permissionOverwrites.cache.keys(), + ]); + + // Compare all overwrites + return [...overwriteIds].every(key => { + const channelVal = this.permissionOverwrites.cache.get(key); + const parentVal = this.parent.permissionOverwrites.cache.get(key); + + // Handle empty overwrite + if ( + (!channelVal && + parentVal.deny.bitfield === PermissionsBitField.DefaultBit && + parentVal.allow.bitfield === PermissionsBitField.DefaultBit) || + (!parentVal && + channelVal.deny.bitfield === PermissionsBitField.DefaultBit && + channelVal.allow.bitfield === PermissionsBitField.DefaultBit) + ) { + return true; + } + + // Compare overwrites + return ( + channelVal !== undefined && + parentVal !== undefined && + channelVal.deny.bitfield === parentVal.deny.bitfield && + channelVal.allow.bitfield === parentVal.allow.bitfield + ); + }); + } + + /** + * The position of the channel + * @type {number} + * @readonly + */ + get position() { + const selfIsCategory = this.type === ChannelType.GuildCategory; + const types = getSortableGroupTypes(this.type); + + let count = 0; + for (const channel of this.guild.channels.cache.values()) { + if (!types.includes(channel.type)) continue; + if (!selfIsCategory && channel.parentId !== this.parentId) continue; + if (this.rawPosition === channel.rawPosition) { + if (Snowflake.compare(channel.id, this.id) === -1) count++; + } else if (this.rawPosition > channel.rawPosition) { + count++; + } + } + + return count; + } + + /** + * Gets the overall set of permissions for a member or role in this channel, taking into account channel overwrites. + * @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for + * @param {boolean} [checkAdmin=true] Whether having the {@link PermissionFlagsBits.Administrator} permission + * will return all permissions + * @returns {?Readonly<PermissionsBitField>} + */ + permissionsFor(memberOrRole, checkAdmin = true) { + const member = this.guild.members.resolve(memberOrRole); + if (member) return this.memberPermissions(member, checkAdmin); + const role = this.guild.roles.resolve(memberOrRole); + return role && this.rolePermissions(role, checkAdmin); + } + + overwritesFor(member, verified = false, roles = null) { + if (!verified) member = this.guild.members.resolve(member); + if (!member) return []; + + roles ??= member.roles.cache; + const roleOverwrites = []; + let memberOverwrites; + let everyoneOverwrites; + + for (const overwrite of this.permissionOverwrites.cache.values()) { + if (overwrite.id === this.guild.id) { + everyoneOverwrites = overwrite; + } else if (roles.has(overwrite.id)) { + roleOverwrites.push(overwrite); + } else if (overwrite.id === member.id) { + memberOverwrites = overwrite; + } + } + + return { + everyone: everyoneOverwrites, + roles: roleOverwrites, + member: memberOverwrites, + }; + } + + /** + * Gets the overall set of permissions for a member in this channel, taking into account channel overwrites. + * @param {GuildMember} member The member to obtain the overall permissions for + * @param {boolean} checkAdmin Whether having the {@link PermissionFlagsBits.Administrator} permission + * will return all permissions + * @returns {Readonly<PermissionsBitField>} + * @private + */ + memberPermissions(member, checkAdmin) { + if (checkAdmin && member.id === this.guild.ownerId) { + return new PermissionsBitField(PermissionsBitField.All).freeze(); + } + + const roles = member.roles.cache; + const permissions = new PermissionsBitField(roles.map(role => role.permissions)); + + if (checkAdmin && permissions.has(PermissionFlagsBits.Administrator)) { + return new PermissionsBitField(PermissionsBitField.All).freeze(); + } + + const overwrites = this.overwritesFor(member, true, roles); + + return permissions + .remove(overwrites.everyone?.deny ?? PermissionsBitField.DefaultBit) + .add(overwrites.everyone?.allow ?? PermissionsBitField.DefaultBit) + .remove(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.deny) : PermissionsBitField.DefaultBit) + .add(overwrites.roles.length > 0 ? overwrites.roles.map(role => role.allow) : PermissionsBitField.DefaultBit) + .remove(overwrites.member?.deny ?? PermissionsBitField.DefaultBit) + .add(overwrites.member?.allow ?? PermissionsBitField.DefaultBit) + .freeze(); + } + + /** + * Gets the overall set of permissions for a role in this channel, taking into account channel overwrites. + * @param {Role} role The role to obtain the overall permissions for + * @param {boolean} checkAdmin Whether having the {@link PermissionFlagsBits.Administrator} permission + * will return all permissions + * @returns {Readonly<PermissionsBitField>} + * @private + */ + rolePermissions(role, checkAdmin) { + if (checkAdmin && role.permissions.has(PermissionFlagsBits.Administrator)) { + return new PermissionsBitField(PermissionsBitField.All).freeze(); + } + + const everyoneOverwrites = this.permissionOverwrites.cache.get(this.guild.id); + const roleOverwrites = this.permissionOverwrites.cache.get(role.id); + + return role.permissions + .remove(everyoneOverwrites?.deny ?? PermissionsBitField.DefaultBit) + .add(everyoneOverwrites?.allow ?? PermissionsBitField.DefaultBit) + .remove(roleOverwrites?.deny ?? PermissionsBitField.DefaultBit) + .add(roleOverwrites?.allow ?? PermissionsBitField.DefaultBit) + .freeze(); + } + + /** + * Locks in the permission overwrites from the parent channel. + * @returns {Promise<GuildChannel>} + */ + lockPermissions() { + if (!this.parent) return Promise.reject(new DiscordjsError(ErrorCodes.GuildChannelOrphan)); + const permissionOverwrites = this.parent.permissionOverwrites.cache.map(overwrite => overwrite.toJSON()); + return this.edit({ permissionOverwrites }); + } + + /** + * A collection of cached members of this channel, mapped by their ids. + * Members that can view this channel, if the channel is text-based. + * Members in the channel, if the channel is voice-based. + * @type {Collection<Snowflake, GuildMember>} + * @readonly + */ + get members() { + return this.guild.members.cache.filter(m => this.permissionsFor(m).has(PermissionFlagsBits.ViewChannel, false)); + } + + /** + * Edits the channel. + * @param {GuildChannelEditOptions} options The options to provide + * @returns {Promise<GuildChannel>} + * @example + * // Edit a channel + * channel.edit({ name: 'new-channel' }) + * .then(console.log) + * .catch(console.error); + */ + edit(options) { + return this.guild.channels.edit(this, options); + } + + /** + * Sets a new name for the guild channel. + * @param {string} name The new name for the guild channel + * @param {string} [reason] Reason for changing the guild channel's name + * @returns {Promise<GuildChannel>} + * @example + * // Set a new channel name + * channel.setName('not_general') + * .then(newChannel => console.log(`Channel's new name is ${newChannel.name}`)) + * .catch(console.error); + */ + setName(name, reason) { + return this.edit({ name, reason }); + } + + /** + * Options used to set the parent of a channel. + * @typedef {Object} SetParentOptions + * @property {boolean} [lockPermissions=true] Whether to lock the permissions to what the parent's permissions are + * @property {string} [reason] The reason for modifying the parent of the channel + */ + + /** + * Sets the parent of this channel. + * @param {?CategoryChannelResolvable} channel The category channel to set as the parent + * @param {SetParentOptions} [options={}] The options for setting the parent + * @returns {Promise<GuildChannel>} + * @example + * // Add a parent to a channel + * message.channel.setParent('355908108431917066', { lockPermissions: false }) + * .then(channel => console.log(`New parent of ${message.channel.name}: ${channel.name}`)) + * .catch(console.error); + */ + setParent(channel, { lockPermissions = true, reason } = {}) { + return this.edit({ + parent: channel ?? null, + lockPermissions, + reason, + }); + } + + /** + * Options used to set the position of a channel. + * @typedef {Object} SetChannelPositionOptions + * @property {boolean} [relative=false] Whether or not to change the position relative to its current value + * @property {string} [reason] The reason for changing the position + */ + + /** + * Sets a new position for the guild channel. + * @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 + * channel.setPosition(2) + * .then(newChannel => console.log(`Channel's new position is ${newChannel.position}`)) + * .catch(console.error); + */ + setPosition(position, options = {}) { + return this.guild.channels.setPosition(this, position, options); + } + + /** + * Options used to clone a guild channel. + * @typedef {GuildChannelCreateOptions} GuildChannelCloneOptions + * @property {string} [name=this.name] Name of the new channel + */ + + /** + * Clones this channel. + * @param {GuildChannelCloneOptions} [options] The options for cloning this channel + * @returns {Promise<GuildChannel>} + */ + clone(options = {}) { + return this.guild.channels.create({ + name: options.name ?? this.name, + permissionOverwrites: this.permissionOverwrites.cache, + topic: this.topic, + type: this.type, + nsfw: this.nsfw, + parent: this.parent, + bitrate: this.bitrate, + userLimit: this.userLimit, + rateLimitPerUser: this.rateLimitPerUser, + position: this.rawPosition, + reason: null, + ...options, + }); + } + + /** + * Checks if this channel has the same type, topic, position, name, overwrites, and id as another channel. + * In most cases, a simple `channel.id === channel2.id` will do, and is much faster too. + * @param {GuildChannel} channel Channel to compare with + * @returns {boolean} + */ + equals(channel) { + let equal = + channel && + this.id === channel.id && + this.type === channel.type && + this.topic === channel.topic && + this.position === channel.position && + this.name === channel.name; + + if (equal) { + if (this.permissionOverwrites && channel.permissionOverwrites) { + equal = this.permissionOverwrites.cache.equals(channel.permissionOverwrites.cache); + } else { + equal = !this.permissionOverwrites && !channel.permissionOverwrites; + } + } + + return equal; + } + + /** + * Whether the channel is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + return this.manageable && this.guild.rulesChannelId !== this.id && this.guild.publicUpdatesChannelId !== this.id; + } + + /** + * Whether the channel is manageable by the client user + * @type {boolean} + * @readonly + */ + get manageable() { + if (this.client.user.id === this.guild.ownerId) return true; + const permissions = this.permissionsFor(this.client.user); + if (!permissions) return false; + + // This flag allows managing even if timed out + if (permissions.has(PermissionFlagsBits.Administrator, false)) return true; + if (this.guild.members.me.communicationDisabledUntilTimestamp > Date.now()) return false; + + const bitfield = VoiceBasedChannelTypes.includes(this.type) + ? PermissionFlagsBits.ManageChannels | PermissionFlagsBits.Connect + : PermissionFlagsBits.ViewChannel | PermissionFlagsBits.ManageChannels; + return permissions.has(bitfield, false); + } + + /** + * Whether the channel is viewable by the client user + * @type {boolean} + * @readonly + */ + get viewable() { + if (this.client.user.id === this.guild.ownerId) return true; + const permissions = this.permissionsFor(this.client.user); + if (!permissions) return false; + return permissions.has(PermissionFlagsBits.ViewChannel, false); + } + + /** + * Deletes this channel. + * @param {string} [reason] Reason for deleting this channel + * @returns {Promise<GuildChannel>} + * @example + * // Delete the channel + * channel.delete('making room for new channels') + * .then(console.log) + * .catch(console.error); + */ + async delete(reason) { + await this.guild.channels.delete(this.id, reason); + return this; + } +} + +module.exports = GuildChannel; diff --git a/node_modules/discord.js/src/structures/GuildEmoji.js b/node_modules/discord.js/src/structures/GuildEmoji.js new file mode 100644 index 0000000..0035a36 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildEmoji.js @@ -0,0 +1,148 @@ +'use strict'; + +const { PermissionFlagsBits } = require('discord-api-types/v10'); +const BaseGuildEmoji = require('./BaseGuildEmoji'); +const { DiscordjsError, ErrorCodes } = require('../errors'); +const GuildEmojiRoleManager = require('../managers/GuildEmojiRoleManager'); + +/** + * Represents a custom emoji. + * @extends {BaseGuildEmoji} + */ +class GuildEmoji extends BaseGuildEmoji { + constructor(client, data, guild) { + super(client, data, guild); + + /** + * The user who created this emoji + * @type {?User} + */ + this.author = null; + + /** + * Array of role ids this emoji is active for + * @name GuildEmoji#_roles + * @type {Snowflake[]} + * @private + */ + Object.defineProperty(this, '_roles', { value: [], writable: true }); + + this._patch(data); + } + + /** + * The guild this emoji is part of + * @type {Guild} + * @name GuildEmoji#guild + */ + + _clone() { + const clone = super._clone(); + clone._roles = this._roles.slice(); + return clone; + } + + _patch(data) { + super._patch(data); + + if (data.user) this.author = this.client.users._add(data.user); + if (data.roles) this._roles = data.roles; + } + + /** + * Whether the emoji is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + if (!this.guild.members.me) throw new DiscordjsError(ErrorCodes.GuildUncachedMe); + return !this.managed && this.guild.members.me.permissions.has(PermissionFlagsBits.ManageGuildExpressions); + } + + /** + * A manager for roles this emoji is active for. + * @type {GuildEmojiRoleManager} + * @readonly + */ + get roles() { + return new GuildEmojiRoleManager(this); + } + + /** + * Fetches the author for this emoji + * @returns {Promise<User>} + */ + fetchAuthor() { + return this.guild.emojis.fetchAuthor(this); + } + + /** + * Data for editing an emoji. + * @typedef {Object} GuildEmojiEditOptions + * @property {string} [name] The name of the emoji + * @property {Collection<Snowflake, Role>|RoleResolvable[]} [roles] Roles to restrict emoji to + * @property {string} [reason] Reason for editing this emoji + */ + + /** + * Edits the emoji. + * @param {GuildEmojiEditOptions} options The options to provide + * @returns {Promise<GuildEmoji>} + * @example + * // Edit an emoji + * emoji.edit({ name: 'newemoji' }) + * .then(e => console.log(`Edited emoji ${e}`)) + * .catch(console.error); + */ + edit(options) { + return this.guild.emojis.edit(this.id, options); + } + + /** + * Sets the name of the emoji. + * @param {string} name The new name for the emoji + * @param {string} [reason] Reason for changing the emoji's name + * @returns {Promise<GuildEmoji>} + */ + setName(name, reason) { + return this.edit({ name, reason }); + } + + /** + * Deletes the emoji. + * @param {string} [reason] Reason for deleting the emoji + * @returns {Promise<GuildEmoji>} + */ + async delete(reason) { + await this.guild.emojis.delete(this.id, reason); + return this; + } + + /** + * Whether this emoji is the same as another one. + * @param {GuildEmoji|APIEmoji} other The emoji to compare it to + * @returns {boolean} + */ + equals(other) { + if (other instanceof GuildEmoji) { + return ( + other.id === this.id && + other.name === this.name && + other.managed === this.managed && + other.available === this.available && + other.requiresColons === this.requiresColons && + other.roles.cache.size === this.roles.cache.size && + other.roles.cache.every(role => this.roles.cache.has(role.id)) + ); + } else { + return ( + other.id === this.id && + other.name === this.name && + other.roles.length === this.roles.cache.size && + other.roles.every(role => this.roles.cache.has(role)) + ); + } + } +} + +module.exports = GuildEmoji; diff --git a/node_modules/discord.js/src/structures/GuildMember.js b/node_modules/discord.js/src/structures/GuildMember.js new file mode 100644 index 0000000..8806b50 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildMember.js @@ -0,0 +1,520 @@ +'use strict'; + +const { PermissionFlagsBits } = require('discord-api-types/v10'); +const Base = require('./Base'); +const VoiceState = require('./VoiceState'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const { DiscordjsError, ErrorCodes } = require('../errors'); +const GuildMemberRoleManager = require('../managers/GuildMemberRoleManager'); +const { GuildMemberFlagsBitField } = require('../util/GuildMemberFlagsBitField'); +const PermissionsBitField = require('../util/PermissionsBitField'); + +/** + * Represents a member of a guild on Discord. + * @implements {TextBasedChannel} + * @extends {Base} + */ +class GuildMember extends Base { + constructor(client, data, guild) { + super(client); + + /** + * The guild that this member is part of + * @type {Guild} + */ + this.guild = guild; + + /** + * The timestamp the member joined the guild at + * @type {?number} + */ + this.joinedTimestamp = null; + + /** + * The last timestamp this member started boosting the guild + * @type {?number} + */ + this.premiumSinceTimestamp = null; + + /** + * The nickname of this member, if they have one + * @type {?string} + */ + this.nickname = null; + + /** + * Whether this member has yet to pass the guild's membership gate + * @type {?boolean} + */ + this.pending = null; + + /** + * The timestamp this member's timeout will be removed + * @type {?number} + */ + this.communicationDisabledUntilTimestamp = null; + + /** + * The role ids of the member + * @name GuildMember#_roles + * @type {Snowflake[]} + * @private + */ + Object.defineProperty(this, '_roles', { value: [], writable: true }); + + if (data) this._patch(data); + } + + _patch(data) { + if ('user' in data) { + /** + * The user that this guild member instance represents + * @type {?User} + */ + this.user = this.client.users._add(data.user, true); + } + + if ('nick' in data) this.nickname = data.nick; + if ('avatar' in data) { + /** + * The guild member's avatar hash + * @type {?string} + */ + this.avatar = data.avatar; + } else if (typeof this.avatar !== 'string') { + this.avatar = null; + } + if ('joined_at' in data) this.joinedTimestamp = Date.parse(data.joined_at); + if ('premium_since' in data) { + this.premiumSinceTimestamp = data.premium_since ? Date.parse(data.premium_since) : null; + } + if ('roles' in data) this._roles = data.roles; + + if ('pending' in data) { + this.pending = data.pending; + } else if (!this.partial) { + // See https://github.com/discordjs/discord.js/issues/6546 for more info. + this.pending ??= false; + } + + if ('communication_disabled_until' in data) { + this.communicationDisabledUntilTimestamp = + data.communication_disabled_until && Date.parse(data.communication_disabled_until); + } + + if ('flags' in data) { + /** + * The flags of this member + * @type {Readonly<GuildMemberFlagsBitField>} + */ + this.flags = new GuildMemberFlagsBitField(data.flags).freeze(); + } else { + this.flags ??= new GuildMemberFlagsBitField().freeze(); + } + } + + _clone() { + const clone = super._clone(); + clone._roles = this._roles.slice(); + return clone; + } + + /** + * Whether this GuildMember is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return this.joinedTimestamp === null; + } + + /** + * A manager for the roles belonging to this member + * @type {GuildMemberRoleManager} + * @readonly + */ + get roles() { + return new GuildMemberRoleManager(this); + } + + /** + * The voice state of this member + * @type {VoiceState} + * @readonly + */ + get voice() { + return this.guild.voiceStates.cache.get(this.id) ?? new VoiceState(this.guild, { user_id: this.id }); + } + + /** + * A link to the member's guild avatar. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + avatarURL(options = {}) { + return this.avatar && this.client.rest.cdn.guildMemberAvatar(this.guild.id, this.id, this.avatar, options); + } + + /** + * A link to the member's guild avatar if they have one. + * Otherwise, a link to their {@link User#displayAvatarURL} will be returned. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {string} + */ + displayAvatarURL(options) { + return this.avatarURL(options) ?? this.user.displayAvatarURL(options); + } + + /** + * The time this member joined the guild + * @type {?Date} + * @readonly + */ + get joinedAt() { + return this.joinedTimestamp && new Date(this.joinedTimestamp); + } + + /** + * The time this member's timeout will be removed + * @type {?Date} + * @readonly + */ + get communicationDisabledUntil() { + return this.communicationDisabledUntilTimestamp && new Date(this.communicationDisabledUntilTimestamp); + } + + /** + * The last time this member started boosting the guild + * @type {?Date} + * @readonly + */ + get premiumSince() { + return this.premiumSinceTimestamp && new Date(this.premiumSinceTimestamp); + } + + /** + * The presence of this guild member + * @type {?Presence} + * @readonly + */ + get presence() { + return this.guild.presences.resolve(this.id); + } + + /** + * The displayed color of this member in base 10 + * @type {number} + * @readonly + */ + get displayColor() { + return this.roles.color?.color ?? 0; + } + + /** + * The displayed color of this member in hexadecimal + * @type {string} + * @readonly + */ + get displayHexColor() { + return this.roles.color?.hexColor ?? '#000000'; + } + + /** + * The member's id + * @type {Snowflake} + * @readonly + */ + get id() { + return this.user.id; + } + + /** + * The DM between the client's user and this member + * @type {?DMChannel} + * @readonly + */ + get dmChannel() { + return this.client.users.dmChannel(this.id); + } + + /** + * The nickname of this member, or their user display name if they don't have one + * @type {?string} + * @readonly + */ + get displayName() { + return this.nickname ?? this.user.displayName; + } + + /** + * The overall set of permissions for this member, taking only roles and owner status into account + * @type {Readonly<PermissionsBitField>} + * @readonly + */ + get permissions() { + if (this.user.id === this.guild.ownerId) return new PermissionsBitField(PermissionsBitField.All).freeze(); + return new PermissionsBitField(this.roles.cache.map(role => role.permissions)).freeze(); + } + + /** + * Whether the client user is above this user in the hierarchy, according to role position and guild ownership. + * This is a prerequisite for many moderative actions. + * @type {boolean} + * @readonly + */ + get manageable() { + if (this.user.id === this.guild.ownerId) return false; + if (this.user.id === this.client.user.id) return false; + if (this.client.user.id === this.guild.ownerId) return true; + if (!this.guild.members.me) throw new DiscordjsError(ErrorCodes.GuildUncachedMe); + return this.guild.members.me.roles.highest.comparePositionTo(this.roles.highest) > 0; + } + + /** + * Whether this member is kickable by the client user + * @type {boolean} + * @readonly + */ + get kickable() { + if (!this.guild.members.me) throw new DiscordjsError(ErrorCodes.GuildUncachedMe); + return this.manageable && this.guild.members.me.permissions.has(PermissionFlagsBits.KickMembers); + } + + /** + * Whether this member is bannable by the client user + * @type {boolean} + * @readonly + */ + get bannable() { + if (!this.guild.members.me) throw new DiscordjsError(ErrorCodes.GuildUncachedMe); + return this.manageable && this.guild.members.me.permissions.has(PermissionFlagsBits.BanMembers); + } + + /** + * Whether this member is moderatable by the client user + * @type {boolean} + * @readonly + */ + get moderatable() { + return ( + !this.permissions.has(PermissionFlagsBits.Administrator) && + this.manageable && + (this.guild.members.me?.permissions.has(PermissionFlagsBits.ModerateMembers) ?? false) + ); + } + + /** + * Whether this member is currently timed out + * @returns {boolean} + */ + isCommunicationDisabled() { + return this.communicationDisabledUntilTimestamp > Date.now(); + } + + /** + * Returns `channel.permissionsFor(guildMember)`. Returns permissions for a member in a guild channel, + * taking into account roles and permission overwrites. + * @param {GuildChannelResolvable} channel The guild channel to use as context + * @returns {Readonly<PermissionsBitField>} + */ + permissionsIn(channel) { + channel = this.guild.channels.resolve(channel); + if (!channel) throw new DiscordjsError(ErrorCodes.GuildChannelResolve); + return channel.permissionsFor(this); + } + + /** + * Edits this member. + * @param {GuildMemberEditOptions} options The options to provide + * @returns {Promise<GuildMember>} + */ + edit(options) { + return this.guild.members.edit(this, options); + } + + /** + * Sets the flags for this member. + * @param {GuildMemberFlagsResolvable} flags The flags to set + * @param {string} [reason] Reason for setting the flags + * @returns {Promise<GuildMember>} + */ + setFlags(flags, reason) { + return this.edit({ flags, reason }); + } + + /** + * Sets the nickname for this member. + * @param {?string} nick The nickname for the guild member, or `null` if you want to reset their nickname + * @param {string} [reason] Reason for setting the nickname + * @returns {Promise<GuildMember>} + * @example + * // Set a nickname for a guild member + * guildMember.setNickname('cool nickname', 'Needed a new nickname') + * .then(member => console.log(`Set nickname of ${member.user.username}`)) + * .catch(console.error); + * @example + * // Remove a nickname for a guild member + * guildMember.setNickname(null, 'No nicknames allowed!') + * .then(member => console.log(`Removed nickname for ${member.user.username}`)) + * .catch(console.error); + */ + setNickname(nick, reason) { + return this.edit({ nick, reason }); + } + + /** + * Creates a DM channel between the client and this member. + * @param {boolean} [force=false] Whether to skip the cache check and request the API + * @returns {Promise<DMChannel>} + */ + createDM(force = false) { + return this.user.createDM(force); + } + + /** + * Deletes any DMs with this member. + * @returns {Promise<DMChannel>} + */ + deleteDM() { + return this.user.deleteDM(); + } + + /** + * Kicks this member from the guild. + * @param {string} [reason] Reason for kicking user + * @returns {Promise<GuildMember>} + */ + kick(reason) { + return this.guild.members.kick(this, reason); + } + + /** + * Bans this guild member. + * @param {BanOptions} [options] Options for the ban + * @returns {Promise<GuildMember>} + * @example + * // Ban a guild member, deleting a week's worth of messages + * guildMember.ban({ deleteMessageSeconds: 60 * 60 * 24 * 7, reason: 'They deserved it' }) + * .then(console.log) + * .catch(console.error); + */ + ban(options) { + return this.guild.bans.create(this, options); + } + + /** + * Times this guild member out. + * @param {DateResolvable|null} communicationDisabledUntil The date or timestamp + * for the member's communication to be disabled until. Provide `null` to remove the timeout. + * @param {string} [reason] The reason for this timeout. + * @returns {Promise<GuildMember>} + * @example + * // Time a guild member out for 5 minutes + * guildMember.disableCommunicationUntil(Date.now() + (5 * 60 * 1000), 'They deserved it') + * .then(console.log) + * .catch(console.error); + * @example + * // Remove the timeout of a guild member + * guildMember.disableCommunicationUntil(null) + * .then(member => console.log(`Removed timeout for ${member.displayName}`)) + * .catch(console.error); + */ + disableCommunicationUntil(communicationDisabledUntil, reason) { + return this.edit({ communicationDisabledUntil, reason }); + } + + /** + * Times this guild member out. + * @param {number|null} timeout The time in milliseconds + * for the member's communication to be disabled until. Provide `null` to remove the timeout. + * @param {string} [reason] The reason for this timeout. + * @returns {Promise<GuildMember>} + * @example + * // Time a guild member out for 5 minutes + * guildMember.timeout(5 * 60 * 1000, 'They deserved it') + * .then(console.log) + * .catch(console.error); + */ + timeout(timeout, reason) { + return this.disableCommunicationUntil(timeout && Date.now() + timeout, reason); + } + + /** + * Fetches this GuildMember. + * @param {boolean} [force=true] Whether to skip the cache check and request the API + * @returns {Promise<GuildMember>} + */ + fetch(force = true) { + return this.guild.members.fetch({ user: this.id, cache: true, force }); + } + + /** + * Whether this guild member equals another guild member. It compares all properties, so for most + * comparison it is advisable to just compare `member.id === member2.id` as it is significantly faster + * and is often what most users need. + * @param {GuildMember} member The member to compare with + * @returns {boolean} + */ + equals(member) { + return ( + member instanceof this.constructor && + this.id === member.id && + this.partial === member.partial && + this.guild.id === member.guild.id && + this.joinedTimestamp === member.joinedTimestamp && + this.nickname === member.nickname && + this.avatar === member.avatar && + this.pending === member.pending && + this.communicationDisabledUntilTimestamp === member.communicationDisabledUntilTimestamp && + this.flags.bitfield === member.flags.bitfield && + (this._roles === member._roles || + (this._roles.length === member._roles.length && this._roles.every((role, i) => role === member._roles[i]))) + ); + } + + /** + * When concatenated with a string, this automatically returns the user's mention instead of the GuildMember object. + * @returns {string} + * @example + * // Logs: Hello from <@123456789012345678>! + * console.log(`Hello from ${member}!`); + */ + toString() { + return this.user.toString(); + } + + toJSON() { + const json = super.toJSON({ + guild: 'guildId', + user: 'userId', + displayName: true, + roles: true, + }); + json.avatarURL = this.avatarURL(); + json.displayAvatarURL = this.displayAvatarURL(); + return json; + } +} + +/** + * Sends a message to this user. + * @method send + * @memberof GuildMember + * @instance + * @param {string|MessagePayload|MessageCreateOptions} options The options to provide + * @returns {Promise<Message>} + * @example + * // Send a direct message + * guildMember.send('Hello!') + * .then(message => console.log(`Sent message: ${message.content} to ${guildMember.displayName}`)) + * .catch(console.error); + */ + +TextBasedChannel.applyToClass(GuildMember); + +exports.GuildMember = GuildMember; + +/** + * @external APIGuildMember + * @see {@link https://discord.com/developers/docs/resources/guild#guild-member-object} + */ diff --git a/node_modules/discord.js/src/structures/GuildOnboarding.js b/node_modules/discord.js/src/structures/GuildOnboarding.js new file mode 100644 index 0000000..119f905 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildOnboarding.js @@ -0,0 +1,58 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const Base = require('./Base'); +const { GuildOnboardingPrompt } = require('./GuildOnboardingPrompt'); + +/** + * Represents the onboarding data of a guild. + * @extends {Base} + */ +class GuildOnboarding extends Base { + constructor(client, data) { + super(client); + + /** + * The id of the guild this onboarding data is for + * @type {Snowflake} + */ + this.guildId = data.guild_id; + + const guild = this.guild; + + /** + * The prompts shown during onboarding and in customize community + * @type {Collection<Snowflake, GuildOnboardingPrompt>} + */ + this.prompts = data.prompts.reduce( + (prompts, prompt) => prompts.set(prompt.id, new GuildOnboardingPrompt(client, prompt, this.guildId)), + new Collection(), + ); + + /** + * The ids of the channels that new members get opted into automatically + * @type {Collection<Snowflake, GuildChannel>} + */ + this.defaultChannels = data.default_channel_ids.reduce( + (channels, channelId) => channels.set(channelId, guild.channels.cache.get(channelId)), + new Collection(), + ); + + /** + * Whether onboarding is enabled + * @type {boolean} + */ + this.enabled = data.enabled; + } + + /** + * The guild this onboarding is from + * @type {Guild} + * @readonly + */ + get guild() { + return this.client.guilds.cache.get(this.guildId); + } +} + +exports.GuildOnboarding = GuildOnboarding; diff --git a/node_modules/discord.js/src/structures/GuildOnboardingPrompt.js b/node_modules/discord.js/src/structures/GuildOnboardingPrompt.js new file mode 100644 index 0000000..4de3f5d --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildOnboardingPrompt.js @@ -0,0 +1,78 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const Base = require('./Base'); +const { GuildOnboardingPromptOption } = require('./GuildOnboardingPromptOption'); + +/** + * Represents the data of a prompt of a guilds onboarding. + * @extends {Base} + */ +class GuildOnboardingPrompt extends Base { + constructor(client, data, guildId) { + super(client); + + /** + * The id of the guild this onboarding prompt is from + * @type {Snowflake} + */ + this.guildId = guildId; + + /** + * The id of the prompt + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The options available within the prompt + * @type {Collection<Snowflake, GuildOnboardingPromptOption>} + */ + this.options = data.options.reduce( + (options, option) => options.set(option.id, new GuildOnboardingPromptOption(client, option, guildId)), + new Collection(), + ); + + /** + * The title of the prompt + * @type {string} + */ + this.title = data.title; + + /** + * Whether users are limited to selecting one option for the prompt + * @type {boolean} + */ + this.singleSelect = data.single_select; + + /** + * Whether the prompt is required before a user completes the onboarding flow + * @type {boolean} + */ + this.required = data.required; + + /** + * Whether the prompt is present in the onboarding flow. + * If `false`, the prompt will only appear in the Channels & Roles tab + * @type {boolean} + */ + this.inOnboarding = data.in_onboarding; + + /** + * The type of the prompt + * @type {GuildOnboardingPromptType} + */ + this.type = data.type; + } + + /** + * The guild this onboarding prompt is from + * @type {Guild} + * @readonly + */ + get guild() { + return this.client.guilds.cache.get(this.guildId); + } +} + +exports.GuildOnboardingPrompt = GuildOnboardingPrompt; diff --git a/node_modules/discord.js/src/structures/GuildOnboardingPromptOption.js b/node_modules/discord.js/src/structures/GuildOnboardingPromptOption.js new file mode 100644 index 0000000..3002144 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildOnboardingPromptOption.js @@ -0,0 +1,84 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const Base = require('./Base'); +const { resolvePartialEmoji } = require('../util/Util'); + +/** + * Represents the data of an option from a prompt of a guilds onboarding. + * @extends {Base} + */ +class GuildOnboardingPromptOption extends Base { + constructor(client, data, guildId) { + super(client); + + /** + * The id of the guild this onboarding prompt option is from + * @type {Snowflake} + */ + this.guildId = guildId; + + const guild = this.guild; + + /** + * The id of the option + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The channels a member is added to when the option is selected + * @type {Collection<Snowflake, GuildChannel>} + */ + this.channels = data.channel_ids.reduce( + (channels, channelId) => channels.set(channelId, guild.channels.cache.get(channelId)), + new Collection(), + ); + + /** + * The roles assigned to a member when the option is selected + * @type {Collection<Snowflake, Role>} + */ + this.roles = data.role_ids.reduce( + (roles, roleId) => roles.set(roleId, guild.roles.cache.get(roleId)), + new Collection(), + ); + + /** + * The data for an emoji of a guilds onboarding prompt option + * @typedef {Object} GuildOnboardingPromptOptionEmoji + * @property {?Snowflake} id The id of the emoji + * @property {string} name The name of the emoji + * @property {boolean} animated Whether the emoji is animated + */ + + /** + * The emoji of the option + * @type {?GuildOnboardingPromptOptionEmoji} + */ + this.emoji = resolvePartialEmoji(data.emoji); + + /** + * The title of the option + * @type {string} + */ + this.title = data.title; + + /** + * The description of the option + * @type {?string} + */ + this.description = data.description; + } + + /** + * The guild this onboarding prompt option is from + * @type {Guild} + * @readonly + */ + get guild() { + return this.client.guilds.cache.get(this.guildId); + } +} + +exports.GuildOnboardingPromptOption = GuildOnboardingPromptOption; diff --git a/node_modules/discord.js/src/structures/GuildPreview.js b/node_modules/discord.js/src/structures/GuildPreview.js new file mode 100644 index 0000000..6ff2026 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildPreview.js @@ -0,0 +1,193 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { Routes } = require('discord-api-types/v10'); +const Base = require('./Base'); +const GuildPreviewEmoji = require('./GuildPreviewEmoji'); +const { Sticker } = require('./Sticker'); + +/** + * Represents the data about the guild any bot can preview, connected to the specified guild. + * @extends {Base} + */ +class GuildPreview extends Base { + constructor(client, data) { + super(client); + + if (!data) return; + + this._patch(data); + } + + _patch(data) { + /** + * The id of this guild + * @type {string} + */ + this.id = data.id; + + if ('name' in data) { + /** + * The name of this guild + * @type {string} + */ + this.name = data.name; + } + + if ('icon' in data) { + /** + * The icon of this guild + * @type {?string} + */ + this.icon = data.icon; + } + + if ('splash' in data) { + /** + * The splash icon of this guild + * @type {?string} + */ + this.splash = data.splash; + } + + if ('discovery_splash' in data) { + /** + * The discovery splash icon of this guild + * @type {?string} + */ + this.discoverySplash = data.discovery_splash; + } + + if ('features' in data) { + /** + * An array of enabled guild features + * @type {GuildFeature[]} + */ + this.features = data.features; + } + + if ('approximate_member_count' in data) { + /** + * The approximate count of members in this guild + * @type {number} + */ + this.approximateMemberCount = data.approximate_member_count; + } + + if ('approximate_presence_count' in data) { + /** + * The approximate count of online members in this guild + * @type {number} + */ + this.approximatePresenceCount = data.approximate_presence_count; + } + + if ('description' in data) { + /** + * The description for this guild + * @type {?string} + */ + this.description = data.description; + } else { + this.description ??= null; + } + + if (!this.emojis) { + /** + * Collection of emojis belonging to this guild + * @type {Collection<Snowflake, GuildPreviewEmoji>} + */ + this.emojis = new Collection(); + } else { + this.emojis.clear(); + } + for (const emoji of data.emojis) { + this.emojis.set(emoji.id, new GuildPreviewEmoji(this.client, emoji, this)); + } + + /** + * Collection of stickers belonging to this guild + * @type {Collection<Snowflake, Sticker>} + */ + this.stickers = data.stickers.reduce( + (stickers, sticker) => stickers.set(sticker.id, new Sticker(this.client, sticker)), + new Collection(), + ); + } + + /** + * The timestamp this guild was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time this guild was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The URL to this guild's splash. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + splashURL(options = {}) { + return this.splash && this.client.rest.cdn.splash(this.id, this.splash, options); + } + + /** + * The URL to this guild's discovery splash. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + discoverySplashURL(options = {}) { + return this.discoverySplash && this.client.rest.cdn.discoverySplash(this.id, this.discoverySplash, options); + } + + /** + * The URL to this guild's icon. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + iconURL(options = {}) { + return this.icon && this.client.rest.cdn.icon(this.id, this.icon, options); + } + + /** + * Fetches this guild. + * @returns {Promise<GuildPreview>} + */ + async fetch() { + const data = await this.client.rest.get(Routes.guildPreview(this.id)); + this._patch(data); + return this; + } + + /** + * When concatenated with a string, this automatically returns the guild's name instead of the Guild object. + * @returns {string} + * @example + * // Logs: Hello from My Guild! + * console.log(`Hello from ${previewGuild}!`); + */ + toString() { + return this.name; + } + + toJSON() { + const json = super.toJSON(); + json.iconURL = this.iconURL(); + json.splashURL = this.splashURL(); + return json; + } +} + +module.exports = GuildPreview; diff --git a/node_modules/discord.js/src/structures/GuildPreviewEmoji.js b/node_modules/discord.js/src/structures/GuildPreviewEmoji.js new file mode 100644 index 0000000..144b41d --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildPreviewEmoji.js @@ -0,0 +1,27 @@ +'use strict'; + +const BaseGuildEmoji = require('./BaseGuildEmoji'); + +/** + * Represents an instance of an emoji belonging to a public guild obtained through Discord's preview endpoint. + * @extends {BaseGuildEmoji} + */ +class GuildPreviewEmoji extends BaseGuildEmoji { + /** + * The public guild this emoji is part of + * @type {GuildPreview} + * @name GuildPreviewEmoji#guild + */ + + constructor(client, data, guild) { + super(client, data, guild); + + /** + * The roles this emoji is active for + * @type {Snowflake[]} + */ + this.roles = data.roles; + } +} + +module.exports = GuildPreviewEmoji; diff --git a/node_modules/discord.js/src/structures/GuildScheduledEvent.js b/node_modules/discord.js/src/structures/GuildScheduledEvent.js new file mode 100644 index 0000000..e9a37b2 --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildScheduledEvent.js @@ -0,0 +1,439 @@ +'use strict'; + +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { GuildScheduledEventStatus, GuildScheduledEventEntityType, RouteBases } = require('discord-api-types/v10'); +const Base = require('./Base'); +const { DiscordjsError, ErrorCodes } = require('../errors'); + +/** + * Represents a scheduled event in a {@link Guild}. + * @extends {Base} + */ +class GuildScheduledEvent extends Base { + constructor(client, data) { + super(client); + + /** + * The id of the guild scheduled event + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The id of the guild this guild scheduled event belongs to + * @type {Snowflake} + */ + this.guildId = data.guild_id; + + this._patch(data); + } + + _patch(data) { + if ('channel_id' in data) { + /** + * The channel id in which the scheduled event will be hosted, + * or `null` if entity type is {@link GuildScheduledEventEntityType.External} + * @type {?Snowflake} + */ + this.channelId = data.channel_id; + } else { + this.channelId ??= null; + } + + if ('creator_id' in data) { + /** + * The id of the user that created this guild scheduled event + * @type {?Snowflake} + */ + this.creatorId = data.creator_id; + } else { + this.creatorId ??= null; + } + + /** + * The name of the guild scheduled event + * @type {string} + */ + this.name = data.name; + + if ('description' in data) { + /** + * The description of the guild scheduled event + * @type {?string} + */ + this.description = data.description; + } else { + this.description ??= null; + } + + /** + * The timestamp the guild scheduled event will start at + * <info>This can be potentially `null` only when it's an {@link AuditLogEntryTarget}</info> + * @type {?number} + */ + this.scheduledStartTimestamp = data.scheduled_start_time ? Date.parse(data.scheduled_start_time) : null; + + /** + * The timestamp the guild scheduled event will end at, + * or `null` if the event does not have a scheduled time to end + * @type {?number} + */ + this.scheduledEndTimestamp = data.scheduled_end_time ? Date.parse(data.scheduled_end_time) : null; + + /** + * The privacy level of the guild scheduled event + * @type {GuildScheduledEventPrivacyLevel} + */ + this.privacyLevel = data.privacy_level; + + /** + * The status of the guild scheduled event + * @type {GuildScheduledEventStatus} + */ + this.status = data.status; + + /** + * The type of hosting entity associated with the scheduled event + * @type {GuildScheduledEventEntityType} + */ + this.entityType = data.entity_type; + + if ('entity_id' in data) { + /** + * The id of the hosting entity associated with the scheduled event + * @type {?Snowflake} + */ + this.entityId = data.entity_id; + } else { + this.entityId ??= null; + } + + if ('user_count' in data) { + /** + * The number of users who are subscribed to this guild scheduled event + * @type {?number} + */ + this.userCount = data.user_count; + } else { + this.userCount ??= null; + } + + if ('creator' in data) { + /** + * The user that created this guild scheduled event + * @type {?User} + */ + this.creator = this.client.users._add(data.creator); + } else { + this.creator ??= this.client.users.resolve(this.creatorId); + } + + /* eslint-disable max-len */ + /** + * Represents the additional metadata for a {@link GuildScheduledEvent} + * @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-guild-scheduled-event-entity-metadata} + * @typedef {Object} GuildScheduledEventEntityMetadata + * @property {?string} location The location of the guild scheduled event + */ + /* eslint-enable max-len */ + + if ('entity_metadata' in data) { + if (data.entity_metadata) { + /** + * Additional metadata + * @type {?GuildScheduledEventEntityMetadata} + */ + this.entityMetadata = { + location: data.entity_metadata.location ?? this.entityMetadata?.location ?? null, + }; + } else { + this.entityMetadata = null; + } + } else { + this.entityMetadata ??= null; + } + + if ('image' in data) { + /** + * The cover image hash for this scheduled event + * @type {?string} + */ + this.image = data.image; + } else { + this.image ??= null; + } + } + + /** + * The URL of this scheduled event's cover image + * @param {BaseImageURLOptions} [options={}] Options for image URL + * @returns {?string} + */ + coverImageURL(options = {}) { + return this.image && this.client.rest.cdn.guildScheduledEventCover(this.id, this.image, options); + } + + /** + * The timestamp the guild scheduled event was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the guild scheduled event was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The time the guild scheduled event will start at + * <info>This can be potentially `null` only when it's an {@link AuditLogEntryTarget}</info> + * @type {?Date} + * @readonly + */ + get scheduledStartAt() { + return this.scheduledStartTimestamp && new Date(this.scheduledStartTimestamp); + } + + /** + * The time the guild scheduled event will end at, + * or `null` if the event does not have a scheduled time to end + * @type {?Date} + * @readonly + */ + get scheduledEndAt() { + return this.scheduledEndTimestamp && new Date(this.scheduledEndTimestamp); + } + + /** + * The channel associated with this scheduled event + * @type {?(VoiceChannel|StageChannel)} + * @readonly + */ + get channel() { + return this.client.channels.resolve(this.channelId); + } + + /** + * The guild this scheduled event belongs to + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.resolve(this.guildId); + } + + /** + * The URL to the guild scheduled event + * @type {string} + * @readonly + */ + get url() { + return `${RouteBases.scheduledEvent}/${this.guildId}/${this.id}`; + } + + /** + * Options used to create an invite URL to a {@link GuildScheduledEvent} + * @typedef {InviteCreateOptions} GuildScheduledEventInviteURLCreateOptions + * @property {GuildInvitableChannelResolvable} [channel] The channel to create the invite in. + * <warn>This is required when the `entityType` of `GuildScheduledEvent` is + * {@link GuildScheduledEventEntityType.External}, gets ignored otherwise</warn> + */ + + /** + * Creates an invite URL to this guild scheduled event. + * @param {GuildScheduledEventInviteURLCreateOptions} [options] The options to create the invite + * @returns {Promise<string>} + */ + async createInviteURL(options) { + let channelId = this.channelId; + if (this.entityType === GuildScheduledEventEntityType.External) { + if (!options?.channel) throw new DiscordjsError(ErrorCodes.InviteOptionsMissingChannel); + channelId = this.guild.channels.resolveId(options.channel); + if (!channelId) throw new DiscordjsError(ErrorCodes.GuildChannelResolve); + } + const invite = await this.guild.invites.create(channelId, options); + return `${RouteBases.invite}/${invite.code}?event=${this.id}`; + } + + /** + * Edits this guild scheduled event. + * @param {GuildScheduledEventEditOptions} options The options to edit the guild scheduled event + * @returns {Promise<GuildScheduledEvent>} + * @example + * // Edit a guild scheduled event + * guildScheduledEvent.edit({ name: 'Party' }) + * .then(guildScheduledEvent => console.log(guildScheduledEvent)) + * .catch(console.error); + */ + edit(options) { + return this.guild.scheduledEvents.edit(this.id, options); + } + + /** + * Deletes this guild scheduled event. + * @returns {Promise<GuildScheduledEvent>} + * @example + * // Delete a guild scheduled event + * guildScheduledEvent.delete() + * .then(guildScheduledEvent => console.log(guildScheduledEvent)) + * .catch(console.error); + */ + async delete() { + await this.guild.scheduledEvents.delete(this.id); + return this; + } + + /** + * Sets a new name for the guild scheduled event. + * @param {string} name The new name of the guild scheduled event + * @param {string} [reason] The reason for changing the name + * @returns {Promise<GuildScheduledEvent>} + * @example + * // Set name of a guild scheduled event + * guildScheduledEvent.setName('Birthday Party') + * .then(guildScheduledEvent => console.log(`Set the name to: ${guildScheduledEvent.name}`)) + * .catch(console.error); + */ + setName(name, reason) { + return this.edit({ name, reason }); + } + + /** + * Sets a new time to schedule the event at. + * @param {DateResolvable} scheduledStartTime The time to schedule the event at + * @param {string} [reason] The reason for changing the scheduled start time + * @returns {Promise<GuildScheduledEvent>} + * @example + * // Set start time of a guild scheduled event + * guildScheduledEvent.setScheduledStartTime('2022-09-24T00:00:00+05:30') + * .then(guildScheduledEvent => console.log(`Set the start time to: ${guildScheduledEvent.scheduledStartTime}`)) + * .catch(console.error); + */ + setScheduledStartTime(scheduledStartTime, reason) { + return this.edit({ scheduledStartTime, reason }); + } + + // TODO: scheduledEndTime gets reset on passing null but it hasn't been documented + /** + * Sets a new time to end the event at. + * @param {DateResolvable} scheduledEndTime The time to end the event at + * @param {string} [reason] The reason for changing the scheduled end time + * @returns {Promise<GuildScheduledEvent>} + * @example + * // Set end time of a guild scheduled event + * guildScheduledEvent.setScheduledEndTime('2022-09-25T00:00:00+05:30') + * .then(guildScheduledEvent => console.log(`Set the end time to: ${guildScheduledEvent.scheduledEndTime}`)) + * .catch(console.error); + */ + setScheduledEndTime(scheduledEndTime, reason) { + return this.edit({ scheduledEndTime, reason }); + } + + /** + * Sets the new description of the guild scheduled event. + * @param {string} description The description of the guild scheduled event + * @param {string} [reason] The reason for changing the description + * @returns {Promise<GuildScheduledEvent>} + * @example + * // Set description of a guild scheduled event + * guildScheduledEvent.setDescription('A virtual birthday party') + * .then(guildScheduledEvent => console.log(`Set the description to: ${guildScheduledEvent.description}`)) + * .catch(console.error); + */ + setDescription(description, reason) { + return this.edit({ description, reason }); + } + + /** + * Sets the new status of the guild scheduled event. + * <info>If you're working with TypeScript, use this method in conjunction with status type-guards + * like {@link GuildScheduledEvent#isScheduled} to get only valid status as suggestion</info> + * @param {GuildScheduledEventStatus} status The status of the guild scheduled event + * @param {string} [reason] The reason for changing the status + * @returns {Promise<GuildScheduledEvent>} + * @example + * // Set status of a guild scheduled event + * guildScheduledEvent.setStatus(GuildScheduledEventStatus.Active) + * .then(guildScheduledEvent => console.log(`Set the status to: ${guildScheduledEvent.status}`)) + * .catch(console.error); + */ + setStatus(status, reason) { + return this.edit({ status, reason }); + } + + /** + * Sets the new location of the guild scheduled event. + * @param {string} location The location of the guild scheduled event + * @param {string} [reason] The reason for changing the location + * @returns {Promise<GuildScheduledEvent>} + * @example + * // Set location of a guild scheduled event + * guildScheduledEvent.setLocation('Earth') + * .then(guildScheduledEvent => console.log(`Set the location to: ${guildScheduledEvent.entityMetadata.location}`)) + * .catch(console.error); + */ + setLocation(location, reason) { + return this.edit({ entityMetadata: { location }, reason }); + } + + /** + * Fetches subscribers of this guild scheduled event. + * @param {FetchGuildScheduledEventSubscribersOptions} [options] Options for fetching the subscribers + * @returns {Promise<Collection<Snowflake, GuildScheduledEventUser>>} + */ + fetchSubscribers(options) { + return this.guild.scheduledEvents.fetchSubscribers(this.id, options); + } + + /** + * When concatenated with a string, this automatically concatenates the event's URL instead of the object. + * @returns {string} + * @example + * // Logs: Event: https://discord.com/events/412345678901234567/499876543211234567 + * console.log(`Event: ${guildScheduledEvent}`); + */ + toString() { + return this.url; + } + + /** + * Indicates whether this guild scheduled event has an {@link GuildScheduledEventStatus.Active} status. + * @returns {boolean} + */ + isActive() { + return this.status === GuildScheduledEventStatus.Active; + } + + /** + * Indicates whether this guild scheduled event has a {@link GuildScheduledEventStatus.Canceled} status. + * @returns {boolean} + */ + isCanceled() { + return this.status === GuildScheduledEventStatus.Canceled; + } + + /** + * Indicates whether this guild scheduled event has a {@link GuildScheduledEventStatus.Completed} status. + * @returns {boolean} + */ + isCompleted() { + return this.status === GuildScheduledEventStatus.Completed; + } + + /** + * Indicates whether this guild scheduled event has a {@link GuildScheduledEventStatus.Scheduled} status. + * @returns {boolean} + */ + isScheduled() { + return this.status === GuildScheduledEventStatus.Scheduled; + } +} + +exports.GuildScheduledEvent = GuildScheduledEvent; diff --git a/node_modules/discord.js/src/structures/GuildTemplate.js b/node_modules/discord.js/src/structures/GuildTemplate.js new file mode 100644 index 0000000..c1e219b --- /dev/null +++ b/node_modules/discord.js/src/structures/GuildTemplate.js @@ -0,0 +1,241 @@ +'use strict'; + +const { setTimeout, clearTimeout } = require('node:timers'); +const { RouteBases, Routes } = require('discord-api-types/v10'); +const Base = require('./Base'); +const DataResolver = require('../util/DataResolver'); +const Events = require('../util/Events'); + +/** + * Represents the template for a guild. + * @extends {Base} + */ +class GuildTemplate extends Base { + /** + * A regular expression that matches guild template links. + * The `code` group property is present on the `exec()` result of this expression. + * @type {RegExp} + * @memberof GuildTemplate + */ + static GuildTemplatesPattern = /discord(?:app)?\.(?:com\/template|new)\/(?<code>[\w-]{2,255})/i; + + constructor(client, data) { + super(client); + this._patch(data); + } + + _patch(data) { + if ('code' in data) { + /** + * The unique code of this template + * @type {string} + */ + this.code = data.code; + } + + if ('name' in data) { + /** + * The name of this template + * @type {string} + */ + this.name = data.name; + } + + if ('description' in data) { + /** + * The description of this template + * @type {?string} + */ + this.description = data.description; + } + + if ('usage_count' in data) { + /** + * The amount of times this template has been used + * @type {number} + */ + this.usageCount = data.usage_count; + } + + if ('creator_id' in data) { + /** + * The id of the user that created this template + * @type {Snowflake} + */ + this.creatorId = data.creator_id; + } + + if ('creator' in data) { + /** + * The user that created this template + * @type {User} + */ + this.creator = this.client.users._add(data.creator); + } + + if ('created_at' in data) { + /** + * The timestamp of when this template was created at + * @type {number} + */ + this.createdTimestamp = Date.parse(data.created_at); + } + + if ('updated_at' in data) { + /** + * The timestamp of when this template was last synced to the guild + * @type {number} + */ + this.updatedTimestamp = Date.parse(data.updated_at); + } + + if ('source_guild_id' in data) { + /** + * The id of the guild that this template belongs to + * @type {Snowflake} + */ + this.guildId = data.source_guild_id; + } + + if ('serialized_source_guild' in data) { + /** + * The data of the guild that this template would create + * @type {APIGuild} + */ + this.serializedGuild = data.serialized_source_guild; + } + + /** + * Whether this template has unsynced changes + * @type {?boolean} + */ + this.unSynced = 'is_dirty' in data ? Boolean(data.is_dirty) : null; + + return this; + } + + /** + * Creates a guild based on this template. + * <warn>This is only available to bots in fewer than 10 guilds.</warn> + * @param {string} name The name of the guild + * @param {BufferResolvable|Base64Resolvable} [icon] The icon for the guild + * @returns {Promise<Guild>} + */ + async createGuild(name, icon) { + const { client } = this; + const data = await client.rest.post(Routes.template(this.code), { + body: { + name, + icon: await DataResolver.resolveImage(icon), + }, + }); + + if (client.guilds.cache.has(data.id)) return client.guilds.cache.get(data.id); + + return new Promise(resolve => { + const resolveGuild = guild => { + client.off(Events.GuildCreate, handleGuild); + client.decrementMaxListeners(); + resolve(guild); + }; + + const handleGuild = guild => { + if (guild.id === data.id) { + clearTimeout(timeout); + resolveGuild(guild); + } + }; + + client.incrementMaxListeners(); + client.on(Events.GuildCreate, handleGuild); + + const timeout = setTimeout(() => resolveGuild(client.guilds._add(data)), 10_000).unref(); + }); + } + + /** + * Options used to edit a guild template. + * @typedef {Object} GuildTemplateEditOptions + * @property {string} [name] The name of this template + * @property {string} [description] The description of this template + */ + + /** + * Updates the metadata of this template. + * @param {GuildTemplateEditOptions} [options] Options for editing the template + * @returns {Promise<GuildTemplate>} + */ + async edit({ name, description } = {}) { + const data = await this.client.rest.patch(Routes.guildTemplate(this.guildId, this.code), { + body: { name, description }, + }); + return this._patch(data); + } + + /** + * Deletes this template. + * @returns {Promise<GuildTemplate>} + */ + async delete() { + await this.client.rest.delete(Routes.guildTemplate(this.guildId, this.code)); + return this; + } + + /** + * Syncs this template to the current state of the guild. + * @returns {Promise<GuildTemplate>} + */ + async sync() { + const data = await this.client.rest.put(Routes.guildTemplate(this.guildId, this.code)); + return this._patch(data); + } + + /** + * The time when this template was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The time when this template was last synced to the guild + * @type {Date} + * @readonly + */ + get updatedAt() { + return new Date(this.updatedTimestamp); + } + + /** + * The guild that this template belongs to + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.resolve(this.guildId); + } + + /** + * The URL of this template + * @type {string} + * @readonly + */ + get url() { + return `${RouteBases.template}/${this.code}`; + } + + /** + * When concatenated with a string, this automatically returns the template's code instead of the template object. + * @returns {string} + * @example + * // Logs: Template: FKvmczH2HyUf + * console.log(`Template: ${guildTemplate}!`); + */ + toString() { + return this.code; + } +} + +module.exports = GuildTemplate; diff --git a/node_modules/discord.js/src/structures/Integration.js b/node_modules/discord.js/src/structures/Integration.js new file mode 100644 index 0000000..fa9777b --- /dev/null +++ b/node_modules/discord.js/src/structures/Integration.js @@ -0,0 +1,220 @@ +'use strict'; + +const { Routes } = require('discord-api-types/v10'); +const Base = require('./Base'); +const IntegrationApplication = require('./IntegrationApplication'); + +/** + * The information account for an integration + * @typedef {Object} IntegrationAccount + * @property {Snowflake|string} id The id of the account + * @property {string} name The name of the account + */ + +/** + * The type of an {@link Integration}. This can be: + * * `twitch` + * * `youtube` + * * `discord` + * * `guild_subscription` + * @typedef {string} IntegrationType + */ + +/** + * Represents a guild integration. + * @extends {Base} + */ +class Integration extends Base { + constructor(client, data, guild) { + super(client); + + /** + * The guild this integration belongs to + * @type {Guild} + */ + this.guild = guild; + + /** + * The integration id + * @type {Snowflake|string} + */ + this.id = data.id; + + /** + * The integration name + * @type {string} + */ + this.name = data.name; + + /** + * The integration type + * @type {IntegrationType} + */ + this.type = data.type; + + /** + * Whether this integration is enabled + * @type {?boolean} + */ + this.enabled = data.enabled ?? null; + + if ('syncing' in data) { + /** + * Whether this integration is syncing + * @type {?boolean} + */ + this.syncing = data.syncing; + } else { + this.syncing ??= null; + } + + /** + * The role that this integration uses for subscribers + * @type {?Role} + */ + this.role = this.guild.roles.resolve(data.role_id); + + if ('enable_emoticons' in data) { + /** + * Whether emoticons should be synced for this integration (twitch only currently) + * @type {?boolean} + */ + this.enableEmoticons = data.enable_emoticons; + } else { + this.enableEmoticons ??= null; + } + + if (data.user) { + /** + * The user for this integration + * @type {?User} + */ + this.user = this.client.users._add(data.user); + } else { + this.user ??= null; + } + + /** + * The account integration information + * @type {IntegrationAccount} + */ + this.account = data.account; + + if ('synced_at' in data) { + /** + * The timestamp at which this integration was last synced at + * @type {?number} + */ + this.syncedTimestamp = Date.parse(data.synced_at); + } else { + this.syncedTimestamp ??= null; + } + + if ('subscriber_count' in data) { + /** + * How many subscribers this integration has + * @type {?number} + */ + this.subscriberCount = data.subscriber_count; + } else { + this.subscriberCount ??= null; + } + + if ('revoked' in data) { + /** + * Whether this integration has been revoked + * @type {?boolean} + */ + this.revoked = data.revoked; + } else { + this.revoked ??= null; + } + + this._patch(data); + } + + /** + * The date at which this integration was last synced at + * @type {?Date} + * @readonly + */ + get syncedAt() { + return this.syncedTimestamp && new Date(this.syncedTimestamp); + } + + /** + * All roles that are managed by this integration + * @type {Collection<Snowflake, Role>} + * @readonly + */ + get roles() { + const roles = this.guild.roles.cache; + return roles.filter(role => role.tags?.integrationId === this.id); + } + + _patch(data) { + if ('expire_behavior' in data) { + /** + * The behavior of expiring subscribers + * @type {?IntegrationExpireBehavior} + */ + this.expireBehavior = data.expire_behavior; + } else { + this.expireBehavior ??= null; + } + + if ('expire_grace_period' in data) { + /** + * The grace period (in days) before expiring subscribers + * @type {?number} + */ + this.expireGracePeriod = data.expire_grace_period; + } else { + this.expireGracePeriod ??= null; + } + + if ('application' in data) { + if (this.application) { + this.application._patch(data.application); + } else { + /** + * The application for this integration + * @type {?IntegrationApplication} + */ + this.application = new IntegrationApplication(this.client, data.application); + } + } else { + this.application ??= null; + } + + if ('scopes' in data) { + /** + * The scopes this application has been authorized for + * @type {OAuth2Scopes[]} + */ + this.scopes = data.scopes; + } else { + this.scopes ??= []; + } + } + + /** + * Deletes this integration. + * @returns {Promise<Integration>} + * @param {string} [reason] Reason for deleting this integration + */ + async delete(reason) { + await this.client.rest.delete(Routes.guildIntegration(this.guild.id, this.id), { reason }); + return this; + } + + toJSON() { + return super.toJSON({ + role: 'roleId', + guild: 'guildId', + user: 'userId', + }); + } +} + +module.exports = Integration; diff --git a/node_modules/discord.js/src/structures/IntegrationApplication.js b/node_modules/discord.js/src/structures/IntegrationApplication.js new file mode 100644 index 0000000..4985008 --- /dev/null +++ b/node_modules/discord.js/src/structures/IntegrationApplication.js @@ -0,0 +1,85 @@ +'use strict'; + +const Application = require('./interfaces/Application'); + +/** + * Represents an Integration's OAuth2 Application. + * @extends {Application} + */ +class IntegrationApplication extends Application { + _patch(data) { + super._patch(data); + + if ('bot' in data) { + /** + * The bot user for this application + * @type {?User} + */ + this.bot = this.client.users._add(data.bot); + } else { + this.bot ??= null; + } + + if ('terms_of_service_url' in data) { + /** + * The URL of the application's terms of service + * @type {?string} + */ + this.termsOfServiceURL = data.terms_of_service_url; + } else { + this.termsOfServiceURL ??= null; + } + + if ('privacy_policy_url' in data) { + /** + * The URL of the application's privacy policy + * @type {?string} + */ + this.privacyPolicyURL = data.privacy_policy_url; + } else { + this.privacyPolicyURL ??= null; + } + + if ('rpc_origins' in data) { + /** + * The Array of RPC origin URLs + * @type {string[]} + */ + this.rpcOrigins = data.rpc_origins; + } else { + this.rpcOrigins ??= []; + } + + if ('hook' in data) { + /** + * Whether the application can be default hooked by the client + * @type {?boolean} + */ + this.hook = data.hook; + } else { + this.hook ??= null; + } + + if ('cover_image' in data) { + /** + * The hash of the application's cover image + * @type {?string} + */ + this.cover = data.cover_image; + } else { + this.cover ??= null; + } + + if ('verify_key' in data) { + /** + * The hex-encoded key for verification in interactions and the GameSDK's GetTicket + * @type {?string} + */ + this.verifyKey = data.verify_key; + } else { + this.verifyKey ??= null; + } + } +} + +module.exports = IntegrationApplication; diff --git a/node_modules/discord.js/src/structures/InteractionCollector.js b/node_modules/discord.js/src/structures/InteractionCollector.js new file mode 100644 index 0000000..bb8e6c7 --- /dev/null +++ b/node_modules/discord.js/src/structures/InteractionCollector.js @@ -0,0 +1,269 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const Collector = require('./interfaces/Collector'); +const Events = require('../util/Events'); + +/** + * @typedef {CollectorOptions} InteractionCollectorOptions + * @property {TextBasedChannelsResolvable} [channel] The channel to listen to interactions from + * @property {ComponentType} [componentType] The type of component to listen for + * @property {GuildResolvable} [guild] The guild to listen to interactions from + * @property {InteractionType} [interactionType] The type of interaction to listen for + * @property {number} [max] The maximum total amount of interactions to collect + * @property {number} [maxComponents] The maximum number of components to collect + * @property {number} [maxUsers] The maximum number of users to interact + * @property {Message|APIMessage} [message] The message to listen to interactions from + * @property {InteractionResponse} [interactionResponse] The interaction response to listen + * to message component interactions from + */ + +/** + * Collects interactions. + * Will automatically stop if the message ({@link Client#event:messageDelete messageDelete} or + * {@link Client#event:messageDeleteBulk messageDeleteBulk}), + * channel ({@link Client#event:channelDelete channelDelete}), or + * guild ({@link Client#event:guildDelete guildDelete}) is deleted. + * <info>Interaction collectors that do not specify `time` or `idle` may be prone to always running. + * Ensure your interaction collectors end via either of these options or manual cancellation.</info> + * @extends {Collector} + */ +class InteractionCollector extends Collector { + /** + * @param {Client} client The client on which to collect interactions + * @param {InteractionCollectorOptions} [options={}] The options to apply to this collector + */ + constructor(client, options = {}) { + super(client, options); + + /** + * The message from which to collect interactions, if provided + * @type {?Snowflake} + */ + this.messageId = options.message?.id ?? options.interactionResponse?.interaction.message?.id ?? null; + + /** + * The message interaction id from which to collect interactions, if provided + * @type {?Snowflake} + */ + this.messageInteractionId = options.interactionResponse?.id ?? null; + + /** + * The channel from which to collect interactions, if provided + * @type {?Snowflake} + */ + this.channelId = + options.interactionResponse?.interaction.channelId ?? + options.message?.channelId ?? + options.message?.channel_id ?? + this.client.channels.resolveId(options.channel); + + /** + * The guild from which to collect interactions, if provided + * @type {?Snowflake} + */ + this.guildId = + options.interactionResponse?.interaction.guildId ?? + options.message?.guildId ?? + options.message?.guild_id ?? + this.client.guilds.resolveId(options.channel?.guild) ?? + this.client.guilds.resolveId(options.guild); + + /** + * The type of interaction to collect + * @type {?InteractionType} + */ + this.interactionType = options.interactionType ?? null; + + /** + * The type of component to collect + * @type {?ComponentType} + */ + this.componentType = options.componentType ?? null; + + /** + * The users that have interacted with this collector + * @type {Collection<Snowflake, User>} + */ + this.users = new Collection(); + + /** + * The total number of interactions collected + * @type {number} + */ + this.total = 0; + + this.client.incrementMaxListeners(); + + const bulkDeleteListener = messages => { + if (messages.has(this.messageId)) this.stop('messageDelete'); + }; + + if (this.messageId || this.messageInteractionId) { + this._handleMessageDeletion = this._handleMessageDeletion.bind(this); + this.client.on(Events.MessageDelete, this._handleMessageDeletion); + this.client.on(Events.MessageBulkDelete, bulkDeleteListener); + } + + if (this.channelId) { + this._handleChannelDeletion = this._handleChannelDeletion.bind(this); + this._handleThreadDeletion = this._handleThreadDeletion.bind(this); + this.client.on(Events.ChannelDelete, this._handleChannelDeletion); + this.client.on(Events.ThreadDelete, this._handleThreadDeletion); + } + + if (this.guildId) { + this._handleGuildDeletion = this._handleGuildDeletion.bind(this); + this.client.on(Events.GuildDelete, this._handleGuildDeletion); + } + + this.client.on(Events.InteractionCreate, this.handleCollect); + + this.once('end', () => { + this.client.removeListener(Events.InteractionCreate, this.handleCollect); + this.client.removeListener(Events.MessageDelete, this._handleMessageDeletion); + this.client.removeListener(Events.MessageBulkDelete, bulkDeleteListener); + this.client.removeListener(Events.ChannelDelete, this._handleChannelDeletion); + this.client.removeListener(Events.ThreadDelete, this._handleThreadDeletion); + this.client.removeListener(Events.GuildDelete, this._handleGuildDeletion); + this.client.decrementMaxListeners(); + }); + + this.on('collect', interaction => { + this.total++; + this.users.set(interaction.user.id, interaction.user); + }); + } + + /** + * Handles an incoming interaction for possible collection. + * @param {BaseInteraction} interaction The interaction to possibly collect + * @returns {?Snowflake} + * @private + */ + collect(interaction) { + /** + * Emitted whenever an interaction is collected. + * @event InteractionCollector#collect + * @param {BaseInteraction} interaction The interaction that was collected + */ + + if (this.interactionType && interaction.type !== this.interactionType) return null; + if (this.componentType && interaction.componentType !== this.componentType) return null; + if (this.messageId && interaction.message?.id !== this.messageId) return null; + if ( + this.messageInteractionId && + interaction.message?.interaction?.id && + interaction.message.interaction.id !== this.messageInteractionId + ) { + return null; + } + if (this.channelId && interaction.channelId !== this.channelId) return null; + if (this.guildId && interaction.guildId !== this.guildId) return null; + + return interaction.id; + } + + /** + * Handles an interaction for possible disposal. + * @param {BaseInteraction} interaction The interaction that could be disposed of + * @returns {?Snowflake} + */ + dispose(interaction) { + /** + * Emitted whenever an interaction is disposed of. + * @event InteractionCollector#dispose + * @param {BaseInteraction} interaction The interaction that was disposed of + */ + if (this.type && interaction.type !== this.type) return null; + if (this.componentType && interaction.componentType !== this.componentType) return null; + if (this.messageId && interaction.message?.id !== this.messageId) return null; + if ( + this.messageInteractionId && + interaction.message?.interaction?.id && + interaction.message.interaction.id !== this.messageInteractionId + ) { + return null; + } + if (this.channelId && interaction.channelId !== this.channelId) return null; + if (this.guildId && interaction.guildId !== this.guildId) return null; + + return interaction.id; + } + + /** + * Empties this interaction collector. + */ + empty() { + this.total = 0; + this.collected.clear(); + this.users.clear(); + this.checkEnd(); + } + + /** + * The reason this collector has ended with, or null if it hasn't ended yet + * @type {?string} + * @readonly + */ + get endReason() { + if (this.options.max && this.total >= this.options.max) return 'limit'; + if (this.options.maxComponents && this.collected.size >= this.options.maxComponents) return 'componentLimit'; + if (this.options.maxUsers && this.users.size >= this.options.maxUsers) return 'userLimit'; + return super.endReason; + } + + /** + * Handles checking if the message has been deleted, and if so, stops the collector with the reason 'messageDelete'. + * @private + * @param {Message} message The message that was deleted + * @returns {void} + */ + _handleMessageDeletion(message) { + if (message.id === this.messageId) { + this.stop('messageDelete'); + } + + if (message.interaction?.id === this.messageInteractionId) { + this.stop('messageDelete'); + } + } + + /** + * Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'. + * @private + * @param {GuildChannel} channel The channel that was deleted + * @returns {void} + */ + _handleChannelDeletion(channel) { + if (channel.id === this.channelId || channel.threads?.cache.has(this.channelId)) { + this.stop('channelDelete'); + } + } + + /** + * Handles checking if the thread has been deleted, and if so, stops the collector with the reason 'threadDelete'. + * @private + * @param {ThreadChannel} thread The thread that was deleted + * @returns {void} + */ + _handleThreadDeletion(thread) { + if (thread.id === this.channelId) { + this.stop('threadDelete'); + } + } + + /** + * Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'. + * @private + * @param {Guild} guild The guild that was deleted + * @returns {void} + */ + _handleGuildDeletion(guild) { + if (guild.id === this.guildId) { + this.stop('guildDelete'); + } + } +} + +module.exports = InteractionCollector; diff --git a/node_modules/discord.js/src/structures/InteractionResponse.js b/node_modules/discord.js/src/structures/InteractionResponse.js new file mode 100644 index 0000000..9b372e3 --- /dev/null +++ b/node_modules/discord.js/src/structures/InteractionResponse.js @@ -0,0 +1,102 @@ +'use strict'; + +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { InteractionType } = require('discord-api-types/v10'); +const { DiscordjsError, ErrorCodes } = require('../errors'); + +/** + * Represents an interaction's response + */ +class InteractionResponse { + constructor(interaction, id) { + /** + * The interaction associated with the interaction response + * @type {BaseInteraction} + */ + this.interaction = interaction; + /** + * The id of the original interaction response + * @type {Snowflake} + */ + this.id = id ?? interaction.id; + this.client = interaction.client; + } + + /** + * The timestamp the interaction response was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the interaction response was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Collects a single component interaction that passes the filter. + * The Promise will reject if the time expires. + * @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector + * @returns {Promise<MessageComponentInteraction>} + */ + awaitMessageComponent(options = {}) { + const _options = { ...options, max: 1 }; + return new Promise((resolve, reject) => { + const collector = this.createMessageComponentCollector(_options); + collector.once('end', (interactions, reason) => { + const interaction = interactions.first(); + if (interaction) resolve(interaction); + else reject(new DiscordjsError(ErrorCodes.InteractionCollectorError, reason)); + }); + }); + } + + /** + * Creates a message component interaction collector + * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector + * @returns {InteractionCollector} + */ + createMessageComponentCollector(options = {}) { + return new InteractionCollector(this.client, { + ...options, + interactionResponse: this, + interactionType: InteractionType.MessageComponent, + }); + } + + /** + * Fetches the response as a {@link Message} object. + * @returns {Promise<Message>} + */ + fetch() { + return this.interaction.fetchReply(); + } + + /** + * Deletes the response. + * @returns {Promise<void>} + */ + delete() { + return this.interaction.deleteReply(); + } + + /** + * Edits the response. + * @param {string|MessagePayload|WebhookMessageEditOptions} options The new options for the response. + * @returns {Promise<Message>} + */ + edit(options) { + return this.interaction.editReply(options); + } +} + +// eslint-disable-next-line import/order +const InteractionCollector = require('./InteractionCollector'); +module.exports = InteractionResponse; diff --git a/node_modules/discord.js/src/structures/InteractionWebhook.js b/node_modules/discord.js/src/structures/InteractionWebhook.js new file mode 100644 index 0000000..58eb531 --- /dev/null +++ b/node_modules/discord.js/src/structures/InteractionWebhook.js @@ -0,0 +1,59 @@ +'use strict'; + +const Webhook = require('./Webhook'); + +/** + * Represents a webhook for an Interaction + * @implements {Webhook} + */ +class InteractionWebhook { + /** + * @param {Client} client The instantiating client + * @param {Snowflake} id The application's id + * @param {string} token The interaction's token + */ + constructor(client, id, token) { + /** + * The client that instantiated the interaction webhook + * @name InteractionWebhook#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + this.id = id; + Object.defineProperty(this, 'token', { value: token, writable: true, configurable: true }); + } + + // These are here only for documentation purposes - they are implemented by Webhook + /* eslint-disable no-empty-function */ + /** + * Sends a message with this webhook. + * @param {string|MessagePayload|InteractionReplyOptions} options The content for the reply + * @returns {Promise<Message>} + */ + + send() {} + + /** + * Gets a message that was sent by this webhook. + * @param {Snowflake|'@original'} message The id of the message to fetch + * @returns {Promise<Message>} Returns the message sent by this webhook + */ + + fetchMessage() {} + + /** + * Edits a message that was sent by this webhook. + * @param {MessageResolvable|'@original'} message The message to edit + * @param {string|MessagePayload|WebhookMessageEditOptions} options The options to provide + * @returns {Promise<Message>} Returns the message edited by this webhook + */ + + editMessage() {} + deleteMessage() {} + get url() {} +} + +Webhook.applyToClass(InteractionWebhook, ['sendSlackMessage', 'edit', 'delete', 'createdTimestamp', 'createdAt']); + +module.exports = InteractionWebhook; diff --git a/node_modules/discord.js/src/structures/Invite.js b/node_modules/discord.js/src/structures/Invite.js new file mode 100644 index 0000000..19014ff --- /dev/null +++ b/node_modules/discord.js/src/structures/Invite.js @@ -0,0 +1,322 @@ +'use strict'; + +const { RouteBases, Routes, PermissionFlagsBits } = require('discord-api-types/v10'); +const Base = require('./Base'); +const { GuildScheduledEvent } = require('./GuildScheduledEvent'); +const IntegrationApplication = require('./IntegrationApplication'); +const InviteStageInstance = require('./InviteStageInstance'); +const { DiscordjsError, ErrorCodes } = require('../errors'); + +/** + * Represents an invitation to a guild channel. + * @extends {Base} + */ +class Invite extends Base { + /** + * A regular expression that matches Discord invite links. + * The `code` group property is present on the `exec()` result of this expression. + * @type {RegExp} + * @memberof Invite + */ + static InvitesPattern = /discord(?:(?:app)?\.com\/invite|\.gg(?:\/invite)?)\/(?<code>[\w-]{2,255})/i; + + constructor(client, data) { + super(client); + this._patch(data); + } + + _patch(data) { + const InviteGuild = require('./InviteGuild'); + /** + * The guild the invite is for including welcome screen data if present + * @type {?(Guild|InviteGuild)} + */ + this.guild ??= null; + if (data.guild) { + this.guild = this.client.guilds.resolve(data.guild.id) ?? new InviteGuild(this.client, data.guild); + } + + if ('code' in data) { + /** + * The code for this invite + * @type {string} + */ + this.code = data.code; + } + + if ('approximate_presence_count' in data) { + /** + * The approximate number of online members of the guild this invite is for + * <info>This is only available when the invite was fetched through {@link Client#fetchInvite}.</info> + * @type {?number} + */ + this.presenceCount = data.approximate_presence_count; + } else { + this.presenceCount ??= null; + } + + if ('approximate_member_count' in data) { + /** + * The approximate total number of members of the guild this invite is for + * <info>This is only available when the invite was fetched through {@link Client#fetchInvite}.</info> + * @type {?number} + */ + this.memberCount = data.approximate_member_count; + } else { + this.memberCount ??= null; + } + + if ('temporary' in data) { + /** + * Whether or not this invite only grants temporary membership + * <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch} + * or created through {@link GuildInviteManager#create}.</info> + * @type {?boolean} + */ + this.temporary = data.temporary ?? null; + } else { + this.temporary ??= null; + } + + if ('max_age' in data) { + /** + * The maximum age of the invite, in seconds, 0 if never expires + * <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch} + * or created through {@link GuildInviteManager#create}.</info> + * @type {?number} + */ + this.maxAge = data.max_age; + } else { + this.maxAge ??= null; + } + + if ('uses' in data) { + /** + * How many times this invite has been used + * <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch} + * or created through {@link GuildInviteManager#create}.</info> + * @type {?number} + */ + this.uses = data.uses; + } else { + this.uses ??= null; + } + + if ('max_uses' in data) { + /** + * The maximum uses of this invite + * <info>This is only available when the invite was fetched through {@link GuildInviteManager#fetch} + * or created through {@link GuildInviteManager#create}.</info> + * @type {?number} + */ + this.maxUses = data.max_uses; + } else { + this.maxUses ??= null; + } + + if ('inviter_id' in data) { + /** + * The user's id who created this invite + * @type {?Snowflake} + */ + this.inviterId = data.inviter_id; + } else { + this.inviterId ??= null; + } + + if ('inviter' in data) { + this.client.users._add(data.inviter); + this.inviterId = data.inviter.id; + } + + if ('target_user' in data) { + /** + * The user whose stream to display for this voice channel stream invite + * @type {?User} + */ + this.targetUser = this.client.users._add(data.target_user); + } else { + this.targetUser ??= null; + } + + if ('target_application' in data) { + /** + * The embedded application to open for this voice channel embedded application invite + * @type {?IntegrationApplication} + */ + this.targetApplication = new IntegrationApplication(this.client, data.target_application); + } else { + this.targetApplication ??= null; + } + + if ('target_type' in data) { + /** + * The target type + * @type {?InviteTargetType} + */ + this.targetType = data.target_type; + } else { + this.targetType ??= null; + } + + if ('channel_id' in data) { + /** + * The id of the channel this invite is for + * @type {?Snowflake} + */ + this.channelId = data.channel_id; + } + + if ('channel' in data) { + /** + * The channel this invite is for + * @type {?BaseChannel} + */ + this.channel = + this.client.channels._add(data.channel, this.guild, { cache: false }) ?? + this.client.channels.resolve(this.channelId); + + this.channelId ??= data.channel.id; + } + + if ('created_at' in data) { + /** + * The timestamp this invite was created at + * @type {?number} + */ + this.createdTimestamp = Date.parse(data.created_at); + } else { + this.createdTimestamp ??= null; + } + + if ('expires_at' in data) { + this._expiresTimestamp = data.expires_at && Date.parse(data.expires_at); + } else { + this._expiresTimestamp ??= null; + } + + if ('stage_instance' in data) { + /** + * The stage instance data if there is a public {@link StageInstance} in the stage channel this invite is for + * @type {?InviteStageInstance} + * @deprecated + */ + this.stageInstance = new InviteStageInstance(this.client, data.stage_instance, this.channel.id, this.guild.id); + } else { + this.stageInstance ??= null; + } + + if ('guild_scheduled_event' in data) { + /** + * The guild scheduled event data if there is a {@link GuildScheduledEvent} in the channel this invite is for + * @type {?GuildScheduledEvent} + */ + this.guildScheduledEvent = new GuildScheduledEvent(this.client, data.guild_scheduled_event); + } else { + this.guildScheduledEvent ??= null; + } + } + + /** + * The time the invite was created at + * @type {?Date} + * @readonly + */ + get createdAt() { + return this.createdTimestamp && new Date(this.createdTimestamp); + } + + /** + * Whether the invite is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + const guild = this.guild; + if (!guild || !this.client.guilds.cache.has(guild.id)) return false; + if (!guild.members.me) throw new DiscordjsError(ErrorCodes.GuildUncachedMe); + return Boolean( + this.channel?.permissionsFor(this.client.user).has(PermissionFlagsBits.ManageChannels, false) || + guild.members.me.permissions.has(PermissionFlagsBits.ManageGuild), + ); + } + + /** + * The timestamp the invite will expire at + * @type {?number} + * @readonly + */ + get expiresTimestamp() { + return ( + this._expiresTimestamp ?? + (this.createdTimestamp && this.maxAge ? this.createdTimestamp + this.maxAge * 1_000 : null) + ); + } + + /** + * The time the invite will expire at + * @type {?Date} + * @readonly + */ + get expiresAt() { + return this.expiresTimestamp && new Date(this.expiresTimestamp); + } + + /** + * The user who created this invite + * @type {?User} + * @readonly + */ + get inviter() { + return this.inviterId && this.client.users.resolve(this.inviterId); + } + + /** + * The URL to the invite + * @type {string} + * @readonly + */ + get url() { + return `${RouteBases.invite}/${this.code}`; + } + + /** + * Deletes this invite. + * @param {string} [reason] Reason for deleting this invite + * @returns {Promise<Invite>} + */ + async delete(reason) { + await this.client.rest.delete(Routes.invite(this.code), { reason }); + return this; + } + + /** + * When concatenated with a string, this automatically concatenates the invite's URL instead of the object. + * @returns {string} + * @example + * // Logs: Invite: https://discord.gg/A1b2C3 + * console.log(`Invite: ${invite}`); + */ + toString() { + return this.url; + } + + toJSON() { + return super.toJSON({ + url: true, + expiresTimestamp: true, + presenceCount: false, + memberCount: false, + uses: false, + channel: 'channelId', + inviter: 'inviterId', + guild: 'guildId', + }); + } + + valueOf() { + return this.code; + } +} + +module.exports = Invite; diff --git a/node_modules/discord.js/src/structures/InviteGuild.js b/node_modules/discord.js/src/structures/InviteGuild.js new file mode 100644 index 0000000..8efd980 --- /dev/null +++ b/node_modules/discord.js/src/structures/InviteGuild.js @@ -0,0 +1,22 @@ +'use strict'; + +const AnonymousGuild = require('./AnonymousGuild'); +const WelcomeScreen = require('./WelcomeScreen'); + +/** + * Represents a guild received from an invite, includes welcome screen data if available. + * @extends {AnonymousGuild} + */ +class InviteGuild extends AnonymousGuild { + constructor(client, data) { + super(client, data); + + /** + * The welcome screen for this invite guild + * @type {?WelcomeScreen} + */ + this.welcomeScreen = data.welcome_screen !== undefined ? new WelcomeScreen(this, data.welcome_screen) : null; + } +} + +module.exports = InviteGuild; diff --git a/node_modules/discord.js/src/structures/InviteStageInstance.js b/node_modules/discord.js/src/structures/InviteStageInstance.js new file mode 100644 index 0000000..21ede43 --- /dev/null +++ b/node_modules/discord.js/src/structures/InviteStageInstance.js @@ -0,0 +1,87 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const Base = require('./Base'); + +/** + * Represents the data about a public {@link StageInstance} in an {@link Invite}. + * @extends {Base} + * @deprecated + */ +class InviteStageInstance extends Base { + constructor(client, data, channelId, guildId) { + super(client); + + /** + * The id of the stage channel this invite is for + * @type {Snowflake} + */ + this.channelId = channelId; + + /** + * The stage channel's guild id + * @type {Snowflake} + */ + this.guildId = guildId; + + /** + * The members speaking in the stage channel + * @type {Collection<Snowflake, GuildMember>} + */ + this.members = new Collection(); + + this._patch(data); + } + + _patch(data) { + if ('topic' in data) { + /** + * The topic of the stage instance + * @type {string} + */ + this.topic = data.topic; + } + + if ('participant_count' in data) { + /** + * The number of users in the stage channel + * @type {number} + */ + this.participantCount = data.participant_count; + } + + if ('speaker_count' in data) { + /** + * The number of users speaking in the stage channel + * @type {number} + */ + this.speakerCount = data.speaker_count; + } + + this.members.clear(); + for (const rawMember of data.members) { + const member = this.guild.members._add(rawMember); + this.members.set(member.id, member); + } + } + + /** + * The stage channel this invite is for + * @type {?StageChannel} + * @readonly + */ + get channel() { + return this.client.channels.resolve(this.channelId); + } + + /** + * The guild of the stage channel this invite is for + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.resolve(this.guildId); + } +} + +module.exports = InviteStageInstance; diff --git a/node_modules/discord.js/src/structures/MentionableSelectMenuBuilder.js b/node_modules/discord.js/src/structures/MentionableSelectMenuBuilder.js new file mode 100644 index 0000000..b22f600 --- /dev/null +++ b/node_modules/discord.js/src/structures/MentionableSelectMenuBuilder.js @@ -0,0 +1,32 @@ +'use strict'; + +const { MentionableSelectMenuBuilder: BuildersMentionableSelectMenu } = require('@discordjs/builders'); +const { isJSONEncodable } = require('@discordjs/util'); +const { toSnakeCase } = require('../util/Transformers'); + +/** + * Class used to build select menu components to be sent through the API + * @extends {BuildersMentionableSelectMenu} + */ +class MentionableSelectMenuBuilder extends BuildersMentionableSelectMenu { + constructor(data = {}) { + super(toSnakeCase(data)); + } + + /** + * Creates a new select menu builder from JSON data + * @param {MentionableSelectMenuBuilder|MentionableSelectMenuComponent|APIMentionableSelectComponent} other + * The other data + * @returns {MentionableSelectMenuBuilder} + */ + static from(other) { + return new this(isJSONEncodable(other) ? other.toJSON() : other); + } +} + +module.exports = MentionableSelectMenuBuilder; + +/** + * @external BuildersMentionableSelectMenu + * @see {@link https://discord.js.org/docs/packages/builders/stable/MentionableSelectMenuBuilder:Class} + */ diff --git a/node_modules/discord.js/src/structures/MentionableSelectMenuComponent.js b/node_modules/discord.js/src/structures/MentionableSelectMenuComponent.js new file mode 100644 index 0000000..d0f75c3 --- /dev/null +++ b/node_modules/discord.js/src/structures/MentionableSelectMenuComponent.js @@ -0,0 +1,11 @@ +'use strict'; + +const BaseSelectMenuComponent = require('./BaseSelectMenuComponent'); + +/** + * Represents a mentionable select menu component + * @extends {BaseSelectMenuComponent} + */ +class MentionableSelectMenuComponent extends BaseSelectMenuComponent {} + +module.exports = MentionableSelectMenuComponent; diff --git a/node_modules/discord.js/src/structures/MentionableSelectMenuInteraction.js b/node_modules/discord.js/src/structures/MentionableSelectMenuInteraction.js new file mode 100644 index 0000000..416d5ce --- /dev/null +++ b/node_modules/discord.js/src/structures/MentionableSelectMenuInteraction.js @@ -0,0 +1,71 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const MessageComponentInteraction = require('./MessageComponentInteraction'); +const Events = require('../util/Events'); + +/** + * Represents a {@link ComponentType.MentionableSelect} select menu interaction. + * @extends {MessageComponentInteraction} + */ +class MentionableSelectMenuInteraction extends MessageComponentInteraction { + constructor(client, data) { + super(client, data); + const { resolved, values } = data.data; + const { members, users, roles } = resolved ?? {}; + + /** + * An array of the selected user and role ids + * @type {Snowflake[]} + */ + this.values = values ?? []; + + /** + * Collection of the selected users + * @type {Collection<Snowflake, User>} + */ + this.users = new Collection(); + + /** + * Collection of the selected users + * @type {Collection<Snowflake, GuildMember|APIGuildMember>} + */ + this.members = new Collection(); + + /** + * Collection of the selected roles + * @type {Collection<Snowflake, Role|APIRole>} + */ + this.roles = new Collection(); + + if (members) { + for (const [id, member] of Object.entries(members)) { + const user = users[id]; + if (!user) { + this.client.emit( + Events.Debug, + `[MentionableSelectMenuInteraction] Received a member without a user, skipping ${id}`, + ); + + continue; + } + + this.members.set(id, this.guild?.members._add({ user, ...member }) ?? { user, ...member }); + } + } + + if (users) { + for (const user of Object.values(users)) { + this.users.set(user.id, this.client.users._add(user)); + } + } + + if (roles) { + for (const role of Object.values(roles)) { + this.roles.set(role.id, this.guild?.roles._add(role) ?? role); + } + } + } +} + +module.exports = MentionableSelectMenuInteraction; diff --git a/node_modules/discord.js/src/structures/Message.js b/node_modules/discord.js/src/structures/Message.js new file mode 100644 index 0000000..c82c177 --- /dev/null +++ b/node_modules/discord.js/src/structures/Message.js @@ -0,0 +1,997 @@ +'use strict'; + +const { messageLink } = require('@discordjs/builders'); +const { Collection } = require('@discordjs/collection'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { + InteractionType, + ChannelType, + MessageType, + MessageFlags, + PermissionFlagsBits, +} = require('discord-api-types/v10'); +const Attachment = require('./Attachment'); +const Base = require('./Base'); +const ClientApplication = require('./ClientApplication'); +const Embed = require('./Embed'); +const InteractionCollector = require('./InteractionCollector'); +const Mentions = require('./MessageMentions'); +const MessagePayload = require('./MessagePayload'); +const ReactionCollector = require('./ReactionCollector'); +const { Sticker } = require('./Sticker'); +const { DiscordjsError, ErrorCodes } = require('../errors'); +const ReactionManager = require('../managers/ReactionManager'); +const { createComponent } = require('../util/Components'); +const { NonSystemMessageTypes, MaxBulkDeletableMessageAge, DeletableMessageTypes } = require('../util/Constants'); +const MessageFlagsBitField = require('../util/MessageFlagsBitField'); +const PermissionsBitField = require('../util/PermissionsBitField'); +const { cleanContent, resolvePartialEmoji } = require('../util/Util'); + +/** + * Represents a message on Discord. + * @extends {Base} + */ +class Message extends Base { + constructor(client, data) { + super(client); + + /** + * The id of the channel the message was sent in + * @type {Snowflake} + */ + this.channelId = data.channel_id; + + /** + * The id of the guild the message was sent in, if any + * @type {?Snowflake} + */ + this.guildId = data.guild_id ?? this.channel?.guild?.id ?? null; + + this._patch(data); + } + + _patch(data) { + /** + * The message's id + * @type {Snowflake} + */ + this.id = data.id; + + /** + * The timestamp the message was sent at + * @type {number} + */ + this.createdTimestamp = DiscordSnowflake.timestampFrom(this.id); + + if ('type' in data) { + /** + * The type of the message + * @type {?MessageType} + */ + this.type = data.type; + + /** + * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications) + * @type {?boolean} + */ + this.system = !NonSystemMessageTypes.includes(this.type); + } else { + this.system ??= null; + this.type ??= null; + } + + if ('content' in data) { + /** + * The content of the message. + * <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged intent + * in a guild for messages that do not mention the client.</info> + * @type {?string} + */ + this.content = data.content; + } else { + this.content ??= null; + } + + if ('author' in data) { + /** + * The author of the message + * @type {?User} + */ + this.author = this.client.users._add(data.author, !data.webhook_id); + } else { + this.author ??= null; + } + + if ('pinned' in data) { + /** + * Whether or not this message is pinned + * @type {?boolean} + */ + this.pinned = Boolean(data.pinned); + } else { + this.pinned ??= null; + } + + if ('tts' in data) { + /** + * Whether or not the message was Text-To-Speech + * @type {?boolean} + */ + this.tts = data.tts; + } else { + this.tts ??= null; + } + + if ('nonce' in data) { + /** + * A random number or string used for checking message delivery + * <warn>This is only received after the message was sent successfully, and + * lost if re-fetched</warn> + * @type {?string} + */ + this.nonce = data.nonce; + } else { + this.nonce ??= null; + } + + if ('embeds' in data) { + /** + * An array of embeds in the message - e.g. YouTube Player. + * <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged intent + * in a guild for messages that do not mention the client.</info> + * @type {Embed[]} + */ + this.embeds = data.embeds.map(e => new Embed(e)); + } else { + this.embeds = this.embeds?.slice() ?? []; + } + + if ('components' in data) { + /** + * An array of of action rows in the message. + * <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged intent + * in a guild for messages that do not mention the client.</info> + * @type {ActionRow[]} + */ + this.components = data.components.map(c => createComponent(c)); + } else { + this.components = this.components?.slice() ?? []; + } + + if ('attachments' in data) { + /** + * A collection of attachments in the message - e.g. Pictures - mapped by their ids. + * <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged intent + * in a guild for messages that do not mention the client.</info> + * @type {Collection<Snowflake, Attachment>} + */ + this.attachments = new Collection(); + if (data.attachments) { + for (const attachment of data.attachments) { + this.attachments.set(attachment.id, new Attachment(attachment)); + } + } + } else { + this.attachments = new Collection(this.attachments); + } + + if ('sticker_items' in data || 'stickers' in data) { + /** + * A collection of stickers in the message + * @type {Collection<Snowflake, Sticker>} + */ + this.stickers = new Collection( + (data.sticker_items ?? data.stickers)?.map(s => [s.id, new Sticker(this.client, s)]), + ); + } else { + this.stickers = new Collection(this.stickers); + } + + if ('position' in data) { + /** + * A generally increasing integer (there may be gaps or duplicates) that represents + * the approximate position of the message in a thread. + * @type {?number} + */ + this.position = data.position; + } else { + this.position ??= null; + } + + if ('role_subscription_data' in data) { + /** + * Role subscription data found on {@link MessageType.RoleSubscriptionPurchase} messages. + * @typedef {Object} RoleSubscriptionData + * @property {Snowflake} roleSubscriptionListingId The id of the SKU and listing the user is subscribed to + * @property {string} tierName The name of the tier the user is subscribed to + * @property {number} totalMonthsSubscribed The total number of months the user has been subscribed for + * @property {boolean} isRenewal Whether this notification is a renewal + */ + + /** + * The data of the role subscription purchase or renewal. + * <info>This is present on {@link MessageType.RoleSubscriptionPurchase} messages.</info> + * @type {?RoleSubscriptionData} + */ + this.roleSubscriptionData = { + roleSubscriptionListingId: data.role_subscription_data.role_subscription_listing_id, + tierName: data.role_subscription_data.tier_name, + totalMonthsSubscribed: data.role_subscription_data.total_months_subscribed, + isRenewal: data.role_subscription_data.is_renewal, + }; + } else { + this.roleSubscriptionData ??= null; + } + + // Discord sends null if the message has not been edited + if (data.edited_timestamp) { + /** + * The timestamp the message was last edited at (if applicable) + * @type {?number} + */ + this.editedTimestamp = Date.parse(data.edited_timestamp); + } else { + this.editedTimestamp ??= null; + } + + if ('reactions' in data) { + /** + * A manager of the reactions belonging to this message + * @type {ReactionManager} + */ + this.reactions = new ReactionManager(this); + if (data.reactions?.length > 0) { + for (const reaction of data.reactions) { + this.reactions._add(reaction); + } + } + } else { + this.reactions ??= new ReactionManager(this); + } + + if (!this.mentions) { + /** + * All valid mentions that the message contains + * @type {MessageMentions} + */ + this.mentions = new Mentions( + this, + data.mentions, + data.mention_roles, + data.mention_everyone, + data.mention_channels, + data.referenced_message?.author, + ); + } else { + this.mentions = new Mentions( + this, + data.mentions ?? this.mentions.users, + data.mention_roles ?? this.mentions.roles, + data.mention_everyone ?? this.mentions.everyone, + data.mention_channels ?? this.mentions.crosspostedChannels, + data.referenced_message?.author ?? this.mentions.repliedUser, + ); + } + + if ('webhook_id' in data) { + /** + * The id of the webhook that sent the message, if applicable + * @type {?Snowflake} + */ + this.webhookId = data.webhook_id; + } else { + this.webhookId ??= null; + } + + if ('application' in data) { + /** + * Supplemental application information for group activities + * @type {?ClientApplication} + */ + this.groupActivityApplication = new ClientApplication(this.client, data.application); + } else { + this.groupActivityApplication ??= null; + } + + if ('application_id' in data) { + /** + * The id of the application of the interaction that sent this message, if any + * @type {?Snowflake} + */ + this.applicationId = data.application_id; + } else { + this.applicationId ??= null; + } + + if ('activity' in data) { + /** + * Group activity + * @type {?MessageActivity} + */ + this.activity = { + partyId: data.activity.party_id, + type: data.activity.type, + }; + } else { + this.activity ??= null; + } + + if ('thread' in data) { + this.client.channels._add(data.thread, this.guild); + } + + if (this.member && data.member) { + this.member._patch(data.member); + } else if (data.member && this.guild && this.author) { + this.guild.members._add(Object.assign(data.member, { user: this.author })); + } + + if ('flags' in data) { + /** + * Flags that are applied to the message + * @type {Readonly<MessageFlagsBitField>} + */ + this.flags = new MessageFlagsBitField(data.flags).freeze(); + } else { + this.flags = new MessageFlagsBitField(this.flags).freeze(); + } + + /** + * Reference data sent in a message that contains ids identifying the referenced message. + * This can be present in the following types of message: + * * Crossposted messages (`MessageFlags.Crossposted`) + * * {@link MessageType.ChannelFollowAdd} + * * {@link MessageType.ChannelPinnedMessage} + * * {@link MessageType.Reply} + * * {@link MessageType.ThreadStarterMessage} + * @see {@link https://discord.com/developers/docs/resources/channel#message-types} + * @typedef {Object} MessageReference + * @property {Snowflake} channelId The channel's id the message was referenced + * @property {?Snowflake} guildId The guild's id the message was referenced + * @property {?Snowflake} messageId The message's id that was referenced + */ + + if ('message_reference' in data) { + /** + * Message reference data + * @type {?MessageReference} + */ + this.reference = { + channelId: data.message_reference.channel_id, + guildId: data.message_reference.guild_id, + messageId: data.message_reference.message_id, + }; + } else { + this.reference ??= null; + } + + if (data.referenced_message) { + this.channel?.messages._add({ guild_id: data.message_reference?.guild_id, ...data.referenced_message }); + } + + /** + * Partial data of the interaction that a message is a reply to + * @typedef {Object} MessageInteraction + * @property {Snowflake} id The interaction's id + * @property {InteractionType} type The type of the interaction + * @property {string} commandName The name of the interaction's application command, + * as well as the subcommand and subcommand group, where applicable + * @property {User} user The user that invoked the interaction + */ + + if (data.interaction) { + /** + * Partial data of the interaction that this message is a reply to + * @type {?MessageInteraction} + */ + this.interaction = { + id: data.interaction.id, + type: data.interaction.type, + commandName: data.interaction.name, + user: this.client.users._add(data.interaction.user), + }; + } else { + this.interaction ??= null; + } + } + + /** + * The channel that the message was sent in + * @type {TextBasedChannels} + * @readonly + */ + get channel() { + return this.client.channels.resolve(this.channelId); + } + + /** + * Whether or not this message is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return typeof this.content !== 'string' || !this.author; + } + + /** + * Represents the author of the message as a guild member. + * Only available if the message comes from a guild where the author is still a member + * @type {?GuildMember} + * @readonly + */ + get member() { + return this.guild?.members.resolve(this.author) ?? null; + } + + /** + * The time the message was sent at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The time the message was last edited at (if applicable) + * @type {?Date} + * @readonly + */ + get editedAt() { + return this.editedTimestamp && new Date(this.editedTimestamp); + } + + /** + * The guild the message was sent in (if in a guild channel) + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.resolve(this.guildId) ?? this.channel?.guild ?? null; + } + + /** + * Whether this message has a thread associated with it + * @type {boolean} + * @readonly + */ + get hasThread() { + return this.flags.has(MessageFlags.HasThread); + } + + /** + * The thread started by this message + * <info>This property is not suitable for checking whether a message has a thread, + * use {@link Message#hasThread} instead.</info> + * @type {?ThreadChannel} + * @readonly + */ + get thread() { + return this.channel?.threads?.resolve(this.id) ?? null; + } + + /** + * The URL to jump to this message + * @type {string} + * @readonly + */ + get url() { + return this.inGuild() ? messageLink(this.channelId, this.id, this.guildId) : messageLink(this.channelId, this.id); + } + + /** + * The message contents with all mentions replaced by the equivalent text. + * If mentions cannot be resolved to a name, the relevant mention in the message content will not be converted. + * @type {?string} + * @readonly + */ + get cleanContent() { + // eslint-disable-next-line eqeqeq + return this.content != null ? cleanContent(this.content, this.channel) : null; + } + + /** + * Creates a reaction collector. + * @param {ReactionCollectorOptions} [options={}] Options to send to the collector + * @returns {ReactionCollector} + * @example + * // Create a reaction collector + * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId'; + * const collector = message.createReactionCollector({ filter, time: 15_000 }); + * collector.on('collect', r => console.log(`Collected ${r.emoji.name}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createReactionCollector(options = {}) { + return new ReactionCollector(this, options); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {ReactionCollectorOptions} AwaitReactionsOptions + * @property {string[]} [errors] Stop/end reasons that cause the promise to reject + */ + + /** + * Similar to createReactionCollector but in promise form. + * Resolves with a collection of reactions that pass the specified filter. + * @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise<Collection<string | Snowflake, MessageReaction>>} + * @example + * // Create a reaction collector + * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId' + * message.awaitReactions({ filter, time: 15_000 }) + * .then(collected => console.log(`Collected ${collected.size} reactions`)) + * .catch(console.error); + */ + awaitReactions(options = {}) { + return new Promise((resolve, reject) => { + const collector = this.createReactionCollector(options); + collector.once('end', (reactions, reason) => { + if (options.errors?.includes(reason)) reject(reactions); + else resolve(reactions); + }); + }); + } + + /** + * @typedef {CollectorOptions} MessageComponentCollectorOptions + * @property {ComponentType} [componentType] The type of component to listen for + * @property {number} [max] The maximum total amount of interactions to collect + * @property {number} [maxComponents] The maximum number of components to collect + * @property {number} [maxUsers] The maximum number of users to interact + */ + + /** + * Creates a message component interaction collector. + * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector + * @returns {InteractionCollector} + * @example + * // Create a message component interaction collector + * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId'; + * const collector = message.createMessageComponentCollector({ filter, time: 15_000 }); + * collector.on('collect', i => console.log(`Collected ${i.customId}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createMessageComponentCollector(options = {}) { + return new InteractionCollector(this.client, { + ...options, + interactionType: InteractionType.MessageComponent, + message: this, + }); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {Object} AwaitMessageComponentOptions + * @property {CollectorFilter} [filter] The filter applied to this collector + * @property {number} [time] Time to wait for an interaction before rejecting + * @property {ComponentType} [componentType] The type of component interaction to collect + * @property {number} [idle] Time to wait without another message component interaction before ending the collector + * @property {boolean} [dispose] Whether to remove the message component interaction after collecting + * @property {InteractionResponse} [interactionResponse] The interaction response to collect interactions from + */ + + /** + * Collects a single component interaction that passes the filter. + * The Promise will reject if the time expires. + * @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector + * @returns {Promise<MessageComponentInteraction>} + * @example + * // Collect a message component interaction + * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId'; + * message.awaitMessageComponent({ filter, time: 15_000 }) + * .then(interaction => console.log(`${interaction.customId} was clicked!`)) + * .catch(console.error); + */ + awaitMessageComponent(options = {}) { + const _options = { ...options, max: 1 }; + return new Promise((resolve, reject) => { + const collector = this.createMessageComponentCollector(_options); + collector.once('end', (interactions, reason) => { + const interaction = interactions.first(); + if (interaction) resolve(interaction); + else reject(new DiscordjsError(ErrorCodes.InteractionCollectorError, reason)); + }); + }); + } + + /** + * Whether the message is editable by the client user + * @type {boolean} + * @readonly + */ + get editable() { + const precheck = Boolean(this.author.id === this.client.user.id && (!this.guild || this.channel?.viewable)); + + // Regardless of permissions thread messages cannot be edited if + // the thread is archived or the thread is locked and the bot does not have permission to manage threads. + if (this.channel?.isThread()) { + if (this.channel.archived) return false; + if (this.channel.locked) { + const permissions = this.channel.permissionsFor(this.client.user); + if (!permissions?.has(PermissionFlagsBits.ManageThreads, true)) return false; + } + } + + return precheck; + } + + /** + * Whether the message is deletable by the client user + * @type {boolean} + * @readonly + */ + get deletable() { + if (!DeletableMessageTypes.includes(this.type)) return false; + + if (!this.guild) { + return this.author.id === this.client.user.id; + } + // DMChannel does not have viewable property, so check viewable after proved that message is on a guild. + if (!this.channel?.viewable) { + return false; + } + + const permissions = this.channel?.permissionsFor(this.client.user); + if (!permissions) return false; + // This flag allows deleting even if timed out + if (permissions.has(PermissionFlagsBits.Administrator, false)) return true; + + // The auto moderation action message author is the reference message author + return ( + (this.type !== MessageType.AutoModerationAction && this.author.id === this.client.user.id) || + (permissions.has(PermissionFlagsBits.ManageMessages, false) && !this.guild.members.me.isCommunicationDisabled()) + ); + } + + /** + * Whether the message is bulk deletable by the client user + * @type {boolean} + * @readonly + * @example + * // Filter for bulk deletable messages + * channel.bulkDelete(messages.filter(message => message.bulkDeletable)); + */ + get bulkDeletable() { + return ( + (this.inGuild() && + Date.now() - this.createdTimestamp < MaxBulkDeletableMessageAge && + this.deletable && + this.channel?.permissionsFor(this.client.user).has(PermissionFlagsBits.ManageMessages, false)) ?? + false + ); + } + + /** + * Whether the message is pinnable by the client user + * @type {boolean} + * @readonly + */ + get pinnable() { + const { channel } = this; + return Boolean( + !this.system && + (!this.guild || + (channel?.viewable && + channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))), + ); + } + + /** + * Fetches the Message this crosspost/reply/pin-add references, if available to the client + * @returns {Promise<Message>} + */ + async fetchReference() { + if (!this.reference) throw new DiscordjsError(ErrorCodes.MessageReferenceMissing); + const { channelId, messageId } = this.reference; + const channel = this.client.channels.resolve(channelId); + if (!channel) throw new DiscordjsError(ErrorCodes.GuildChannelResolve); + const message = await channel.messages.fetch(messageId); + return message; + } + + /** + * Whether the message is crosspostable by the client user + * @type {boolean} + * @readonly + */ + get crosspostable() { + const bitfield = + PermissionFlagsBits.SendMessages | + (this.author.id === this.client.user.id ? PermissionsBitField.DefaultBit : PermissionFlagsBits.ManageMessages); + const { channel } = this; + return Boolean( + channel?.type === ChannelType.GuildAnnouncement && + !this.flags.has(MessageFlags.Crossposted) && + this.type === MessageType.Default && + channel.viewable && + channel.permissionsFor(this.client.user)?.has(bitfield, false), + ); + } + + /** + * Edits the content of the message. + * @param {string|MessagePayload|MessageEditOptions} options The options to provide + * @returns {Promise<Message>} + * @example + * // Update the content of a message + * message.edit('This is my new content!') + * .then(msg => console.log(`Updated the content of a message to ${msg.content}`)) + * .catch(console.error); + */ + edit(options) { + if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached)); + return this.channel.messages.edit(this, options); + } + + /** + * Publishes a message in an announcement channel to all channels following it. + * @returns {Promise<Message>} + * @example + * // Crosspost a message + * if (message.channel.type === ChannelType.GuildAnnouncement) { + * message.crosspost() + * .then(() => console.log('Crossposted message')) + * .catch(console.error); + * } + */ + crosspost() { + if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached)); + return this.channel.messages.crosspost(this.id); + } + + /** + * Pins this message to the channel's pinned messages. + * @param {string} [reason] Reason for pinning + * @returns {Promise<Message>} + * @example + * // Pin a message + * message.pin() + * .then(console.log) + * .catch(console.error) + */ + async pin(reason) { + if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached); + await this.channel.messages.pin(this.id, reason); + return this; + } + + /** + * Unpins this message from the channel's pinned messages. + * @param {string} [reason] Reason for unpinning + * @returns {Promise<Message>} + * @example + * // Unpin a message + * message.unpin() + * .then(console.log) + * .catch(console.error) + */ + async unpin(reason) { + if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached); + await this.channel.messages.unpin(this.id, reason); + return this; + } + + /** + * Adds a reaction to the message. + * @param {EmojiIdentifierResolvable} emoji The emoji to react with + * @returns {Promise<MessageReaction>} + * @example + * // React to a message with a unicode emoji + * message.react('🤔') + * .then(console.log) + * .catch(console.error); + * @example + * // React to a message with a custom emoji + * message.react(message.guild.emojis.cache.get('123456789012345678')) + * .then(console.log) + * .catch(console.error); + */ + async react(emoji) { + if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached); + await this.channel.messages.react(this.id, emoji); + + return this.client.actions.MessageReactionAdd.handle( + { + [this.client.actions.injectedUser]: this.client.user, + [this.client.actions.injectedChannel]: this.channel, + [this.client.actions.injectedMessage]: this, + emoji: resolvePartialEmoji(emoji), + }, + true, + ).reaction; + } + + /** + * Deletes the message. + * @returns {Promise<Message>} + * @example + * // Delete a message + * message.delete() + * .then(msg => console.log(`Deleted message from ${msg.author.username}`)) + * .catch(console.error); + */ + async delete() { + if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached); + await this.channel.messages.delete(this.id); + return this; + } + + /** + * Options provided when sending a message as an inline reply. + * @typedef {BaseMessageCreateOptions} MessageReplyOptions + * @property {boolean} [failIfNotExists=this.client.options.failIfNotExists] Whether to error if the referenced + * message does not exist (creates a standard message in this case when false) + */ + + /** + * Send an inline reply to this message. + * @param {string|MessagePayload|MessageReplyOptions} options The options to provide + * @returns {Promise<Message>} + * @example + * // Reply to a message + * message.reply('This is a reply!') + * .then(() => console.log(`Replied to message "${message.content}"`)) + * .catch(console.error); + */ + reply(options) { + if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached)); + let data; + + if (options instanceof MessagePayload) { + data = options; + } else { + data = MessagePayload.create(this, options, { + reply: { + messageReference: this, + failIfNotExists: options?.failIfNotExists ?? this.client.options.failIfNotExists, + }, + }); + } + return this.channel.send(data); + } + + /** + * Options for starting a thread on a message. + * @typedef {Object} StartThreadOptions + * @property {string} name The name of the new thread + * @property {ThreadAutoArchiveDuration} [autoArchiveDuration=this.channel.defaultAutoArchiveDuration] The amount of + * time after which the thread should automatically archive in case of no recent activity + * @property {string} [reason] Reason for creating the thread + * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the thread in seconds + */ + + /** + * Create a new public thread from this message + * @see GuildTextThreadManager#create + * @param {StartThreadOptions} [options] Options for starting a thread on this message + * @returns {Promise<ThreadChannel>} + */ + startThread(options = {}) { + if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached)); + if (![ChannelType.GuildText, ChannelType.GuildAnnouncement].includes(this.channel.type)) { + return Promise.reject(new DiscordjsError(ErrorCodes.MessageThreadParent)); + } + if (this.hasThread) return Promise.reject(new DiscordjsError(ErrorCodes.MessageExistingThread)); + return this.channel.threads.create({ ...options, startMessage: this }); + } + + /** + * Fetch this message. + * @param {boolean} [force=true] Whether to skip the cache check and request the API + * @returns {Promise<Message>} + */ + fetch(force = true) { + if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached)); + return this.channel.messages.fetch({ message: this.id, force }); + } + + /** + * Fetches the webhook used to create this message. + * @returns {Promise<?Webhook>} + */ + fetchWebhook() { + if (!this.webhookId) return Promise.reject(new DiscordjsError(ErrorCodes.WebhookMessage)); + if (this.webhookId === this.applicationId) return Promise.reject(new DiscordjsError(ErrorCodes.WebhookApplication)); + return this.client.fetchWebhook(this.webhookId); + } + + /** + * Suppresses or unsuppresses embeds on a message. + * @param {boolean} [suppress=true] If the embeds should be suppressed or not + * @returns {Promise<Message>} + */ + suppressEmbeds(suppress = true) { + const flags = new MessageFlagsBitField(this.flags.bitfield); + + if (suppress) { + flags.add(MessageFlags.SuppressEmbeds); + } else { + flags.remove(MessageFlags.SuppressEmbeds); + } + + return this.edit({ flags }); + } + + /** + * Removes the attachments from this message. + * @returns {Promise<Message>} + */ + removeAttachments() { + return this.edit({ attachments: [] }); + } + + /** + * Resolves a component by a custom id. + * @param {string} customId The custom id to resolve against + * @returns {?MessageActionRowComponent} + */ + resolveComponent(customId) { + return this.components.flatMap(row => row.components).find(component => component.customId === customId) ?? null; + } + + /** + * Used mainly internally. Whether two messages are identical in properties. If you want to compare messages + * without checking all the properties, use `message.id === message2.id`, which is much more efficient. This + * method allows you to see if there are differences in content, embeds, attachments, nonce and tts properties. + * @param {Message} message The message to compare it to + * @param {APIMessage} rawData Raw data passed through the WebSocket about this message + * @returns {boolean} + */ + equals(message, rawData) { + if (!message) return false; + const embedUpdate = !message.author && !message.attachments; + if (embedUpdate) return this.id === message.id && this.embeds.length === message.embeds.length; + + let equal = + this.id === message.id && + this.author.id === message.author.id && + this.content === message.content && + this.tts === message.tts && + this.nonce === message.nonce && + this.embeds.length === message.embeds.length && + this.attachments.length === message.attachments.length; + + if (equal && rawData) { + equal = + this.mentions.everyone === message.mentions.everyone && + this.createdTimestamp === Date.parse(rawData.timestamp) && + this.editedTimestamp === Date.parse(rawData.edited_timestamp); + } + + return equal; + } + + /** + * Whether this message is from a guild. + * @returns {boolean} + */ + inGuild() { + return Boolean(this.guildId); + } + + /** + * When concatenated with a string, this automatically concatenates the message's content instead of the object. + * @returns {string} + * @example + * // Logs: Message: This is a message! + * console.log(`Message: ${message}`); + */ + toString() { + return this.content; + } + + toJSON() { + return super.toJSON({ + channel: 'channelId', + author: 'authorId', + groupActivityApplication: 'groupActivityApplicationId', + guild: 'guildId', + cleanContent: true, + member: false, + reactions: false, + }); + } +} + +exports.Message = Message; diff --git a/node_modules/discord.js/src/structures/MessageCollector.js b/node_modules/discord.js/src/structures/MessageCollector.js new file mode 100644 index 0000000..7101965 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageCollector.js @@ -0,0 +1,146 @@ +'use strict'; + +const Collector = require('./interfaces/Collector'); +const Events = require('../util/Events'); + +/** + * @typedef {CollectorOptions} MessageCollectorOptions + * @property {number} max The maximum amount of messages to collect + * @property {number} maxProcessed The maximum amount of messages to process + */ + +/** + * Collects messages on a channel. + * Will automatically stop if the channel ({@link Client#event:channelDelete channelDelete}), + * thread ({@link Client#event:threadDelete threadDelete}), or + * guild ({@link Client#event:guildDelete guildDelete}) is deleted. + * @extends {Collector} + */ +class MessageCollector extends Collector { + /** + * @param {TextBasedChannels} channel The channel + * @param {MessageCollectorOptions} options The options to be applied to this collector + * @emits MessageCollector#message + */ + constructor(channel, options = {}) { + super(channel.client, options); + + /** + * The channel + * @type {TextBasedChannels} + */ + this.channel = channel; + + /** + * Total number of messages that were received in the channel during message collection + * @type {number} + */ + this.received = 0; + + const bulkDeleteListener = messages => { + for (const message of messages.values()) this.handleDispose(message); + }; + + this._handleChannelDeletion = this._handleChannelDeletion.bind(this); + this._handleThreadDeletion = this._handleThreadDeletion.bind(this); + this._handleGuildDeletion = this._handleGuildDeletion.bind(this); + + this.client.incrementMaxListeners(); + this.client.on(Events.MessageCreate, this.handleCollect); + this.client.on(Events.MessageDelete, this.handleDispose); + this.client.on(Events.MessageBulkDelete, bulkDeleteListener); + this.client.on(Events.ChannelDelete, this._handleChannelDeletion); + this.client.on(Events.ThreadDelete, this._handleThreadDeletion); + this.client.on(Events.GuildDelete, this._handleGuildDeletion); + + this.once('end', () => { + this.client.removeListener(Events.MessageCreate, this.handleCollect); + this.client.removeListener(Events.MessageDelete, this.handleDispose); + this.client.removeListener(Events.MessageBulkDelete, bulkDeleteListener); + this.client.removeListener(Events.ChannelDelete, this._handleChannelDeletion); + this.client.removeListener(Events.ThreadDelete, this._handleThreadDeletion); + this.client.removeListener(Events.GuildDelete, this._handleGuildDeletion); + this.client.decrementMaxListeners(); + }); + } + + /** + * Handles a message for possible collection. + * @param {Message} message The message that could be collected + * @returns {?Snowflake} + * @private + */ + collect(message) { + /** + * Emitted whenever a message is collected. + * @event MessageCollector#collect + * @param {Message} message The message that was collected + */ + if (message.channelId !== this.channel.id) return null; + this.received++; + return message.id; + } + + /** + * Handles a message for possible disposal. + * @param {Message} message The message that could be disposed of + * @returns {?Snowflake} + */ + dispose(message) { + /** + * Emitted whenever a message is disposed of. + * @event MessageCollector#dispose + * @param {Message} message The message that was disposed of + */ + return message.channelId === this.channel.id ? message.id : null; + } + + /** + * The reason this collector has ended with, or null if it hasn't ended yet + * @type {?string} + * @readonly + */ + get endReason() { + if (this.options.max && this.collected.size >= this.options.max) return 'limit'; + if (this.options.maxProcessed && this.received === this.options.maxProcessed) return 'processedLimit'; + return super.endReason; + } + + /** + * Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'. + * @private + * @param {GuildChannel} channel The channel that was deleted + * @returns {void} + */ + _handleChannelDeletion(channel) { + if (channel.id === this.channel.id || channel.id === this.channel.parentId) { + this.stop('channelDelete'); + } + } + + /** + * Handles checking if the thread has been deleted, and if so, stops the collector with the reason 'threadDelete'. + * @private + * @param {ThreadChannel} thread The thread that was deleted + * @returns {void} + */ + _handleThreadDeletion(thread) { + if (thread.id === this.channel.id) { + this.stop('threadDelete'); + } + } + + /** + * Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'. + * @private + * @param {Guild} guild The guild that was deleted + * @returns {void} + */ + _handleGuildDeletion(guild) { + if (guild.id === this.channel.guild?.id) { + this.stop('guildDelete'); + } + } +} + +module.exports = MessageCollector; diff --git a/node_modules/discord.js/src/structures/MessageComponentInteraction.js b/node_modules/discord.js/src/structures/MessageComponentInteraction.js new file mode 100644 index 0000000..47b31e0 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageComponentInteraction.js @@ -0,0 +1,107 @@ +'use strict'; + +const { lazy } = require('@discordjs/util'); +const BaseInteraction = require('./BaseInteraction'); +const InteractionWebhook = require('./InteractionWebhook'); +const InteractionResponses = require('./interfaces/InteractionResponses'); + +const getMessage = lazy(() => require('./Message').Message); + +/** + * Represents a message component interaction. + * @extends {BaseInteraction} + * @implements {InteractionResponses} + */ +class MessageComponentInteraction extends BaseInteraction { + constructor(client, data) { + super(client, data); + + /** + * The id of the channel this interaction was sent in + * @type {Snowflake} + * @name MessageComponentInteraction#channelId + */ + + /** + * The message to which the component was attached + * @type {Message} + */ + this.message = this.channel?.messages._add(data.message) ?? new (getMessage())(client, data.message); + + /** + * The custom id of the component which was interacted with + * @type {string} + */ + this.customId = data.data.custom_id; + + /** + * The type of component which was interacted with + * @type {ComponentType} + */ + this.componentType = data.data.component_type; + + /** + * Whether the reply to this interaction has been deferred + * @type {boolean} + */ + this.deferred = false; + + /** + * Whether the reply to this interaction is ephemeral + * @type {?boolean} + */ + this.ephemeral = null; + + /** + * Whether this interaction has already been replied to + * @type {boolean} + */ + this.replied = false; + + /** + * An associated interaction webhook, can be used to further interact with this interaction + * @type {InteractionWebhook} + */ + this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token); + } + + /** + * Components that can be placed in an action row for messages. + * * ButtonComponent + * * StringSelectMenuComponent + * * UserSelectMenuComponent + * * RoleSelectMenuComponent + * * MentionableSelectMenuComponent + * * ChannelSelectMenuComponent + * @typedef {ButtonComponent|StringSelectMenuComponent|UserSelectMenuComponent| + * RoleSelectMenuComponent|MentionableSelectMenuComponent|ChannelSelectMenuComponent} MessageActionRowComponent + */ + + /** + * The component which was interacted with + * @type {MessageActionRowComponent|APIMessageActionRowComponent} + * @readonly + */ + get component() { + return this.message.components + .flatMap(row => row.components) + .find(component => (component.customId ?? component.custom_id) === this.customId); + } + + // These are here only for documentation purposes - they are implemented by InteractionResponses + /* eslint-disable no-empty-function */ + deferReply() {} + reply() {} + fetchReply() {} + editReply() {} + deleteReply() {} + followUp() {} + deferUpdate() {} + update() {} + showModal() {} + awaitModalSubmit() {} +} + +InteractionResponses.applyToClass(MessageComponentInteraction); + +module.exports = MessageComponentInteraction; diff --git a/node_modules/discord.js/src/structures/MessageContextMenuCommandInteraction.js b/node_modules/discord.js/src/structures/MessageContextMenuCommandInteraction.js new file mode 100644 index 0000000..1100591 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageContextMenuCommandInteraction.js @@ -0,0 +1,20 @@ +'use strict'; + +const ContextMenuCommandInteraction = require('./ContextMenuCommandInteraction'); + +/** + * Represents a message context menu interaction. + * @extends {ContextMenuCommandInteraction} + */ +class MessageContextMenuCommandInteraction extends ContextMenuCommandInteraction { + /** + * The message this interaction was sent from + * @type {Message|APIMessage} + * @readonly + */ + get targetMessage() { + return this.options.getMessage('message'); + } +} + +module.exports = MessageContextMenuCommandInteraction; diff --git a/node_modules/discord.js/src/structures/MessageMentions.js b/node_modules/discord.js/src/structures/MessageMentions.js new file mode 100644 index 0000000..a07e77f --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageMentions.js @@ -0,0 +1,297 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { FormattingPatterns } = require('discord-api-types/v10'); +const { flatten } = require('../util/Util'); + +/** + * Keeps track of mentions in a {@link Message}. + */ +class MessageMentions { + /** + * A regular expression that matches `@everyone` and `@here`. + * The `mention` group property is present on the `exec` result of this expression. + * @type {RegExp} + * @memberof MessageMentions + */ + static EveryonePattern = /@(?<mention>everyone|here)/; + + /** + * A regular expression that matches user mentions like `<@81440962496172032>`. + * The `id` group property is present on the `exec` result of this expression. + * @type {RegExp} + * @memberof MessageMentions + */ + static UsersPattern = FormattingPatterns.UserWithOptionalNickname; + + /** + * A regular expression that matches role mentions like `<@&297577916114403338>`. + * The `id` group property is present on the `exec` result of this expression. + * @type {RegExp} + * @memberof MessageMentions + */ + static RolesPattern = FormattingPatterns.Role; + + /** + * A regular expression that matches channel mentions like `<#222079895583457280>`. + * The `id` group property is present on the `exec` result of this expression. + * @type {RegExp} + * @memberof MessageMentions + */ + static ChannelsPattern = FormattingPatterns.Channel; + + /** + * A global regular expression variant of {@link MessageMentions.ChannelsPattern}. + * @type {RegExp} + * @memberof MessageMentions + * @private + */ + static GlobalChannelsPattern = new RegExp(this.ChannelsPattern.source, 'g'); + + /** + * A global regular expression variant of {@link MessageMentions.UsersPattern}. + * @type {RegExp} + * @memberof MessageMentions + * @private + */ + static GlobalUsersPattern = new RegExp(this.UsersPattern.source, 'g'); + + constructor(message, users, roles, everyone, crosspostedChannels, repliedUser) { + /** + * The client the message is from + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: message.client }); + + /** + * The guild the message is in + * @type {?Guild} + * @readonly + */ + Object.defineProperty(this, 'guild', { value: message.guild }); + + /** + * The initial message content + * @type {string} + * @readonly + * @private + */ + Object.defineProperty(this, '_content', { value: message.content }); + + /** + * Whether `@everyone` or `@here` were mentioned + * @type {boolean} + */ + this.everyone = Boolean(everyone); + + if (users) { + if (users instanceof Collection) { + /** + * Any users that were mentioned + * <info>Order as received from the API, not as they appear in the message content</info> + * @type {Collection<Snowflake, User>} + */ + this.users = new Collection(users); + } else { + this.users = new Collection(); + for (const mention of users) { + if (mention.member && message.guild) { + message.guild.members._add(Object.assign(mention.member, { user: mention })); + } + const user = message.client.users._add(mention); + this.users.set(user.id, user); + } + } + } else { + this.users = new Collection(); + } + + if (roles instanceof Collection) { + /** + * Any roles that were mentioned + * <info>Order as received from the API, not as they appear in the message content</info> + * @type {Collection<Snowflake, Role>} + */ + this.roles = new Collection(roles); + } else if (roles) { + this.roles = new Collection(); + const guild = message.guild; + if (guild) { + for (const mention of roles) { + const role = guild.roles.cache.get(mention); + if (role) this.roles.set(role.id, role); + } + } + } else { + this.roles = new Collection(); + } + + /** + * Cached members for {@link MessageMentions#members} + * @type {?Collection<Snowflake, GuildMember>} + * @private + */ + this._members = null; + + /** + * Cached channels for {@link MessageMentions#channels} + * @type {?Collection<Snowflake, BaseChannel>} + * @private + */ + this._channels = null; + + /** + * Cached users for {@link MessageMentions#parsedUsers} + * @type {?Collection<Snowflake, User>} + * @private + */ + this._parsedUsers = null; + + /** + * Crossposted channel data. + * @typedef {Object} CrosspostedChannel + * @property {Snowflake} channelId The mentioned channel's id + * @property {Snowflake} guildId The id of the guild that has the channel + * @property {ChannelType} type The channel's type + * @property {string} name The channel's name + */ + + if (crosspostedChannels) { + if (crosspostedChannels instanceof Collection) { + /** + * A collection of crossposted channels + * <info>Order as received from the API, not as they appear in the message content</info> + * @type {Collection<Snowflake, CrosspostedChannel>} + */ + this.crosspostedChannels = new Collection(crosspostedChannels); + } else { + this.crosspostedChannels = new Collection(); + for (const d of crosspostedChannels) { + this.crosspostedChannels.set(d.id, { + channelId: d.id, + guildId: d.guild_id, + type: d.type, + name: d.name, + }); + } + } + } else { + this.crosspostedChannels = new Collection(); + } + + /** + * The author of the message that this message is a reply to + * @type {?User} + */ + this.repliedUser = repliedUser ? this.client.users._add(repliedUser) : null; + } + + /** + * Any members that were mentioned (only in {@link Guild}s) + * <info>Order as received from the API, not as they appear in the message content</info> + * @type {?Collection<Snowflake, GuildMember>} + * @readonly + */ + get members() { + if (this._members) return this._members; + if (!this.guild) return null; + this._members = new Collection(); + this.users.forEach(user => { + const member = this.guild.members.resolve(user); + if (member) this._members.set(member.user.id, member); + }); + return this._members; + } + + /** + * Any channels that were mentioned + * <info>Order as they appear first in the message content</info> + * @type {Collection<Snowflake, BaseChannel>} + * @readonly + */ + get channels() { + if (this._channels) return this._channels; + this._channels = new Collection(); + let matches; + + while ((matches = this.constructor.GlobalChannelsPattern.exec(this._content)) !== null) { + const channel = this.client.channels.cache.get(matches.groups.id); + if (channel) this._channels.set(channel.id, channel); + } + + return this._channels; + } + + /** + * Any user mentions that were included in the message content + * <info>Order as they appear first in the message content</info> + * @type {Collection<Snowflake, User>} + * @readonly + */ + get parsedUsers() { + if (this._parsedUsers) return this._parsedUsers; + this._parsedUsers = new Collection(); + let matches; + while ((matches = this.constructor.GlobalUsersPattern.exec(this._content)) !== null) { + const user = this.client.users.cache.get(matches[1]); + if (user) this._parsedUsers.set(user.id, user); + } + return this._parsedUsers; + } + + /** + * Options used to check for a mention. + * @typedef {Object} MessageMentionsHasOptions + * @property {boolean} [ignoreDirect=false] Whether to ignore direct mentions to the item + * @property {boolean} [ignoreRoles=false] Whether to ignore role mentions to a guild member + * @property {boolean} [ignoreRepliedUser=false] Whether to ignore replied user mention to an user + * @property {boolean} [ignoreEveryone=false] Whether to ignore `@everyone`/`@here` mentions + */ + + /** + * Checks if a user, guild member, thread member, role, or channel is mentioned. + * Takes into account user mentions, role mentions, channel mentions, + * replied user mention, and `@everyone`/`@here` mentions. + * @param {UserResolvable|RoleResolvable|ChannelResolvable} data The User/Role/Channel to check for + * @param {MessageMentionsHasOptions} [options] The options for the check + * @returns {boolean} + */ + has(data, { ignoreDirect = false, ignoreRoles = false, ignoreRepliedUser = false, ignoreEveryone = false } = {}) { + const user = this.client.users.resolve(data); + + if (!ignoreEveryone && user && this.everyone) return true; + + const userWasRepliedTo = user && this.repliedUser?.id === user.id; + + if (!ignoreRepliedUser && userWasRepliedTo && this.users.has(user.id)) return true; + + if (!ignoreDirect) { + if (user && (!ignoreRepliedUser || this.parsedUsers.has(user.id)) && this.users.has(user.id)) return true; + + const role = this.guild?.roles.resolve(data); + if (role && this.roles.has(role.id)) return true; + + const channel = this.client.channels.resolve(data); + if (channel && this.channels.has(channel.id)) return true; + } + + if (!ignoreRoles) { + const member = this.guild?.members.resolve(data); + if (member) { + for (const mentionedRole of this.roles.values()) if (member.roles.cache.has(mentionedRole.id)) return true; + } + } + + return false; + } + + toJSON() { + return flatten(this, { + members: true, + channels: true, + }); + } +} + +module.exports = MessageMentions; diff --git a/node_modules/discord.js/src/structures/MessagePayload.js b/node_modules/discord.js/src/structures/MessagePayload.js new file mode 100644 index 0000000..e237309 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessagePayload.js @@ -0,0 +1,299 @@ +'use strict'; + +const { Buffer } = require('node:buffer'); +const { lazy, isJSONEncodable } = require('@discordjs/util'); +const { MessageFlags } = require('discord-api-types/v10'); +const ActionRowBuilder = require('./ActionRowBuilder'); +const { DiscordjsRangeError, ErrorCodes } = require('../errors'); +const DataResolver = require('../util/DataResolver'); +const MessageFlagsBitField = require('../util/MessageFlagsBitField'); +const { basename, verifyString } = require('../util/Util'); + +const getBaseInteraction = lazy(() => require('./BaseInteraction')); + +/** + * Represents a message to be sent to the API. + */ +class MessagePayload { + /** + * @param {MessageTarget} target The target for this message to be sent to + * @param {MessagePayloadOption} options The payload of this message + */ + constructor(target, options) { + /** + * The target for this message to be sent to + * @type {MessageTarget} + */ + this.target = target; + + /** + * The payload of this message. + * @type {MessagePayloadOption} + */ + this.options = options; + + /** + * Body sendable to the API + * @type {?APIMessage} + */ + this.body = null; + + /** + * Files sendable to the API + * @type {?RawFile[]} + */ + this.files = null; + } + + /** + * Whether or not the target is a {@link Webhook} or a {@link WebhookClient} + * @type {boolean} + * @readonly + */ + get isWebhook() { + const Webhook = require('./Webhook'); + const WebhookClient = require('../client/WebhookClient'); + return this.target instanceof Webhook || this.target instanceof WebhookClient; + } + + /** + * Whether or not the target is a {@link User} + * @type {boolean} + * @readonly + */ + get isUser() { + const User = require('./User'); + const { GuildMember } = require('./GuildMember'); + return this.target instanceof User || this.target instanceof GuildMember; + } + + /** + * Whether or not the target is a {@link Message} + * @type {boolean} + * @readonly + */ + get isMessage() { + const { Message } = require('./Message'); + return this.target instanceof Message; + } + + /** + * Whether or not the target is a {@link MessageManager} + * @type {boolean} + * @readonly + */ + get isMessageManager() { + const MessageManager = require('../managers/MessageManager'); + return this.target instanceof MessageManager; + } + + /** + * Whether or not the target is an {@link BaseInteraction} or an {@link InteractionWebhook} + * @type {boolean} + * @readonly + */ + get isInteraction() { + const BaseInteraction = getBaseInteraction(); + const InteractionWebhook = require('./InteractionWebhook'); + return this.target instanceof BaseInteraction || this.target instanceof InteractionWebhook; + } + + /** + * Makes the content of this message. + * @returns {?string} + */ + makeContent() { + let content; + if (this.options.content === null) { + content = ''; + } else if (this.options.content !== undefined) { + content = verifyString(this.options.content, DiscordjsRangeError, ErrorCodes.MessageContentType, true); + } + + return content; + } + + /** + * Resolves the body. + * @returns {MessagePayload} + */ + resolveBody() { + if (this.body) return this; + const isInteraction = this.isInteraction; + const isWebhook = this.isWebhook; + + const content = this.makeContent(); + const tts = Boolean(this.options.tts); + + let nonce; + if (this.options.nonce !== undefined) { + nonce = this.options.nonce; + if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') { + throw new DiscordjsRangeError(ErrorCodes.MessageNonceType); + } + } + + const components = this.options.components?.map(c => (isJSONEncodable(c) ? c : new ActionRowBuilder(c)).toJSON()); + + let username; + let avatarURL; + let threadName; + if (isWebhook) { + username = this.options.username ?? this.target.name; + if (this.options.avatarURL) avatarURL = this.options.avatarURL; + if (this.options.threadName) threadName = this.options.threadName; + } + + let flags; + if ( + this.options.flags !== undefined || + (this.isMessage && this.options.reply === undefined) || + this.isMessageManager + ) { + flags = + // eslint-disable-next-line eqeqeq + this.options.flags != null + ? new MessageFlagsBitField(this.options.flags).bitfield + : this.target.flags?.bitfield; + } + + if (isInteraction && this.options.ephemeral) { + flags |= MessageFlags.Ephemeral; + } + + let allowedMentions = + this.options.allowedMentions === undefined + ? this.target.client.options.allowedMentions + : this.options.allowedMentions; + + if (allowedMentions?.repliedUser !== undefined) { + allowedMentions = { ...allowedMentions, replied_user: allowedMentions.repliedUser }; + delete allowedMentions.repliedUser; + } + + let message_reference; + if (typeof this.options.reply === 'object') { + const reference = this.options.reply.messageReference; + const message_id = this.isMessage ? reference.id ?? reference : this.target.messages.resolveId(reference); + if (message_id) { + message_reference = { + message_id, + fail_if_not_exists: this.options.reply.failIfNotExists ?? this.target.client.options.failIfNotExists, + }; + } + } + + const attachments = this.options.files?.map((file, index) => ({ + id: index.toString(), + description: file.description, + })); + if (Array.isArray(this.options.attachments)) { + this.options.attachments.push(...(attachments ?? [])); + } else { + this.options.attachments = attachments; + } + + this.body = { + content, + tts, + nonce, + embeds: this.options.embeds?.map(embed => + isJSONEncodable(embed) ? embed.toJSON() : this.target.client.options.jsonTransformer(embed), + ), + components, + username, + avatar_url: avatarURL, + allowed_mentions: content === undefined && message_reference === undefined ? undefined : allowedMentions, + flags, + message_reference, + attachments: this.options.attachments, + sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker), + thread_name: threadName, + }; + return this; + } + + /** + * Resolves files. + * @returns {Promise<MessagePayload>} + */ + async resolveFiles() { + if (this.files) return this; + + this.files = await Promise.all(this.options.files?.map(file => this.constructor.resolveFile(file)) ?? []); + return this; + } + + /** + * Resolves a single file into an object sendable to the API. + * @param {AttachmentPayload|BufferResolvable|Stream} fileLike Something that could be resolved to a file + * @returns {Promise<RawFile>} + */ + static async resolveFile(fileLike) { + let attachment; + let name; + + const findName = thing => { + if (typeof thing === 'string') { + return basename(thing); + } + + if (thing.path) { + return basename(thing.path); + } + + return 'file.jpg'; + }; + + const ownAttachment = + typeof fileLike === 'string' || fileLike instanceof Buffer || typeof fileLike.pipe === 'function'; + if (ownAttachment) { + attachment = fileLike; + name = findName(attachment); + } else { + attachment = fileLike.attachment; + name = fileLike.name ?? findName(attachment); + } + + const { data, contentType } = await DataResolver.resolveFile(attachment); + return { data, name, contentType }; + } + + /** + * Creates a {@link MessagePayload} from user-level arguments. + * @param {MessageTarget} target Target to send to + * @param {string|MessagePayloadOption} options Options or content to use + * @param {MessagePayloadOption} [extra={}] Extra options to add onto specified options + * @returns {MessagePayload} + */ + static create(target, options, extra = {}) { + return new this( + target, + typeof options !== 'object' || options === null ? { content: options, ...extra } : { ...options, ...extra }, + ); + } +} + +module.exports = MessagePayload; + +/** + * A target for a message. + * @typedef {TextBasedChannels|User|GuildMember|Webhook|WebhookClient|BaseInteraction|InteractionWebhook| + * Message|MessageManager} MessageTarget + */ + +/** + * A possible payload option. + * @typedef {MessageCreateOptions|MessageEditOptions|WebhookMessageCreateOptions|WebhookMessageEditOptions| + * InteractionReplyOptions|InteractionUpdateOptions} MessagePayloadOption + */ + +/** + * @external APIMessage + * @see {@link https://discord.com/developers/docs/resources/channel#message-object} + */ + +/** + * @external RawFile + * @see {@link https://discord.js.org/docs/packages/rest/stable/RawFile:Interface} + */ diff --git a/node_modules/discord.js/src/structures/MessageReaction.js b/node_modules/discord.js/src/structures/MessageReaction.js new file mode 100644 index 0000000..43f05e3 --- /dev/null +++ b/node_modules/discord.js/src/structures/MessageReaction.js @@ -0,0 +1,142 @@ +'use strict'; + +const { Routes } = require('discord-api-types/v10'); +const GuildEmoji = require('./GuildEmoji'); +const ReactionEmoji = require('./ReactionEmoji'); +const ReactionUserManager = require('../managers/ReactionUserManager'); +const { flatten } = require('../util/Util'); + +/** + * Represents a reaction to a message. + */ +class MessageReaction { + constructor(client, data, message) { + /** + * The client that instantiated this message reaction + * @name MessageReaction#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The message that this reaction refers to + * @type {Message} + */ + this.message = message; + + /** + * Whether the client has given this reaction + * @type {boolean} + */ + this.me = data.me; + + /** + * A manager of the users that have given this reaction + * @type {ReactionUserManager} + */ + this.users = new ReactionUserManager(this, this.me ? [client.user] : []); + + this._emoji = new ReactionEmoji(this, data.emoji); + + this._patch(data); + } + + _patch(data) { + if ('count' in data) { + /** + * The number of people that have given the same reaction + * @type {?number} + */ + this.count ??= data.count; + } + } + + /** + * Makes the client user react with this reaction + * @returns {Promise<MessageReaction>} + */ + react() { + return this.message.react(this.emoji); + } + + /** + * Removes all users from this reaction. + * @returns {Promise<MessageReaction>} + */ + async remove() { + await this.client.rest.delete( + Routes.channelMessageReaction(this.message.channelId, this.message.id, this._emoji.identifier), + ); + return this; + } + + /** + * The emoji of this reaction. Either a {@link GuildEmoji} object for known custom emojis, or a {@link ReactionEmoji} + * object which has fewer properties. Whatever the prototype of the emoji, it will still have + * `name`, `id`, `identifier` and `toString()` + * @type {GuildEmoji|ReactionEmoji} + * @readonly + */ + get emoji() { + if (this._emoji instanceof GuildEmoji) return this._emoji; + // Check to see if the emoji has become known to the client + if (this._emoji.id) { + const emojis = this.message.client.emojis.cache; + if (emojis.has(this._emoji.id)) { + const emoji = emojis.get(this._emoji.id); + this._emoji = emoji; + return emoji; + } + } + return this._emoji; + } + + /** + * Whether or not this reaction is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return this.count === null; + } + + /** + * Fetch this reaction. + * @returns {Promise<MessageReaction>} + */ + async fetch() { + const message = await this.message.fetch(); + const existing = message.reactions.cache.get(this.emoji.id ?? this.emoji.name); + // The reaction won't get set when it has been completely removed + this._patch(existing ?? { count: 0 }); + return this; + } + + toJSON() { + return flatten(this, { emoji: 'emojiId', message: 'messageId' }); + } + + valueOf() { + return this._emoji.id ?? this._emoji.name; + } + + _add(user) { + if (this.partial) return; + this.users.cache.set(user.id, user); + if (!this.me || user.id !== this.message.client.user.id || this.count === 0) this.count++; + this.me ||= user.id === this.message.client.user.id; + } + + _remove(user) { + if (this.partial) return; + this.users.cache.delete(user.id); + if (!this.me || user.id !== this.message.client.user.id) this.count--; + if (user.id === this.message.client.user.id) this.me = false; + if (this.count <= 0 && this.users.cache.size === 0) { + this.message.reactions.cache.delete(this.emoji.id ?? this.emoji.name); + } + } +} + +module.exports = MessageReaction; diff --git a/node_modules/discord.js/src/structures/ModalBuilder.js b/node_modules/discord.js/src/structures/ModalBuilder.js new file mode 100644 index 0000000..535b4a5 --- /dev/null +++ b/node_modules/discord.js/src/structures/ModalBuilder.js @@ -0,0 +1,34 @@ +'use strict'; + +const { ModalBuilder: BuildersModal, ComponentBuilder } = require('@discordjs/builders'); +const { isJSONEncodable } = require('@discordjs/util'); +const { toSnakeCase } = require('../util/Transformers'); + +/** + * Represents a modal builder. + * @extends {BuildersModal} + */ +class ModalBuilder extends BuildersModal { + constructor({ components, ...data } = {}) { + super({ + ...toSnakeCase(data), + components: components?.map(c => (c instanceof ComponentBuilder ? c : toSnakeCase(c))), + }); + } + + /** + * Creates a new modal builder from JSON data + * @param {ModalBuilder|APIModalComponent} other The other data + * @returns {ModalBuilder} + */ + static from(other) { + return new this(isJSONEncodable(other) ? other.toJSON() : other); + } +} + +module.exports = ModalBuilder; + +/** + * @external BuildersModal + * @see {@link https://discord.js.org/docs/packages/builders/stable/ModalBuilder:Class} + */ diff --git a/node_modules/discord.js/src/structures/ModalSubmitFields.js b/node_modules/discord.js/src/structures/ModalSubmitFields.js new file mode 100644 index 0000000..8e67b21 --- /dev/null +++ b/node_modules/discord.js/src/structures/ModalSubmitFields.js @@ -0,0 +1,55 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { ComponentType } = require('discord-api-types/v10'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); + +/** + * Represents the serialized fields from a modal submit interaction + */ +class ModalSubmitFields { + constructor(components) { + /** + * The components within the modal + * @type {ActionRowModalData[]} + */ + this.components = components; + + /** + * The extracted fields from the modal + * @type {Collection<string, ModalData>} + */ + this.fields = components.reduce((accumulator, next) => { + next.components.forEach(c => accumulator.set(c.customId, c)); + return accumulator; + }, new Collection()); + } + + /** + * Gets a field given a custom id from a component + * @param {string} customId The custom id of the component + * @param {ComponentType} [type] The type of the component + * @returns {ModalData} + */ + getField(customId, type) { + const field = this.fields.get(customId); + if (!field) throw new DiscordjsTypeError(ErrorCodes.ModalSubmitInteractionFieldNotFound, customId); + + if (type !== undefined && type !== field.type) { + throw new DiscordjsTypeError(ErrorCodes.ModalSubmitInteractionFieldType, customId, field.type, type); + } + + return field; + } + + /** + * Gets the value of a text input component given a custom id + * @param {string} customId The custom id of the text input component + * @returns {string} + */ + getTextInputValue(customId) { + return this.getField(customId, ComponentType.TextInput).value; + } +} + +module.exports = ModalSubmitFields; diff --git a/node_modules/discord.js/src/structures/ModalSubmitInteraction.js b/node_modules/discord.js/src/structures/ModalSubmitInteraction.js new file mode 100644 index 0000000..8f0ccf1 --- /dev/null +++ b/node_modules/discord.js/src/structures/ModalSubmitInteraction.js @@ -0,0 +1,122 @@ +'use strict'; + +const { lazy } = require('@discordjs/util'); +const BaseInteraction = require('./BaseInteraction'); +const InteractionWebhook = require('./InteractionWebhook'); +const ModalSubmitFields = require('./ModalSubmitFields'); +const InteractionResponses = require('./interfaces/InteractionResponses'); + +const getMessage = lazy(() => require('./Message').Message); + +/** + * @typedef {Object} ModalData + * @property {string} value The value of the field + * @property {ComponentType} type The component type of the field + * @property {string} customId The custom id of the field + */ + +/** + * @typedef {Object} ActionRowModalData + * @property {ModalData[]} components The components of this action row + * @property {ComponentType} type The component type of the action row + */ + +/** + * Represents a modal interaction + * @extends {BaseInteraction} + * @implements {InteractionResponses} + */ +class ModalSubmitInteraction extends BaseInteraction { + constructor(client, data) { + super(client, data); + /** + * The custom id of the modal. + * @type {string} + */ + this.customId = data.data.custom_id; + + if ('message' in data) { + /** + * The message associated with this interaction + * @type {?Message} + */ + this.message = this.channel?.messages._add(data.message) ?? new (getMessage())(this.client, data.message); + } else { + this.message = null; + } + + /** + * The components within the modal + * @type {ActionRowModalData[]} + */ + this.components = data.data.components?.map(c => ModalSubmitInteraction.transformComponent(c)); + + /** + * The fields within the modal + * @type {ModalSubmitFields} + */ + this.fields = new ModalSubmitFields(this.components); + + /** + * Whether the reply to this interaction has been deferred + * @type {boolean} + */ + this.deferred = false; + + /** + * Whether this interaction has already been replied to + * @type {boolean} + */ + this.replied = false; + + /** + * Whether the reply to this interaction is ephemeral + * @type {?boolean} + */ + this.ephemeral = null; + + /** + * An associated interaction webhook, can be used to further interact with this interaction + * @type {InteractionWebhook} + */ + this.webhook = new InteractionWebhook(this.client, this.applicationId, this.token); + } + + /** + * Transforms component data to discord.js-compatible data + * @param {*} rawComponent The data to transform + * @returns {ModalData[]} + */ + static transformComponent(rawComponent) { + return rawComponent.components + ? { type: rawComponent.type, components: rawComponent.components.map(c => this.transformComponent(c)) } + : { + value: rawComponent.value, + type: rawComponent.type, + customId: rawComponent.custom_id, + }; + } + + /** + * Whether this is from a {@link MessageComponentInteraction}. + * @returns {boolean} + */ + isFromMessage() { + return Boolean(this.message); + } + + // These are here only for documentation purposes - they are implemented by InteractionResponses + /* eslint-disable no-empty-function */ + deferReply() {} + reply() {} + fetchReply() {} + editReply() {} + deleteReply() {} + followUp() {} + deferUpdate() {} + update() {} +} + +InteractionResponses.applyToClass(ModalSubmitInteraction, 'showModal'); + +module.exports = ModalSubmitInteraction; diff --git a/node_modules/discord.js/src/structures/NewsChannel.js b/node_modules/discord.js/src/structures/NewsChannel.js new file mode 100644 index 0000000..3f5aff3 --- /dev/null +++ b/node_modules/discord.js/src/structures/NewsChannel.js @@ -0,0 +1,32 @@ +'use strict'; + +const { Routes } = require('discord-api-types/v10'); +const BaseGuildTextChannel = require('./BaseGuildTextChannel'); +const { DiscordjsError, ErrorCodes } = require('../errors'); + +/** + * Represents a guild news channel on Discord. + * @extends {BaseGuildTextChannel} + */ +class NewsChannel extends BaseGuildTextChannel { + /** + * Adds the target to this channel's followers. + * @param {TextChannelResolvable} channel The channel where the webhook should be created + * @param {string} [reason] Reason for creating the webhook + * @returns {Promise<NewsChannel>} + * @example + * if (channel.type === ChannelType.GuildAnnouncement) { + * channel.addFollower('222197033908436994', 'Important announcements') + * .then(() => console.log('Added follower')) + * .catch(console.error); + * } + */ + async addFollower(channel, reason) { + const channelId = this.guild.channels.resolveId(channel); + if (!channelId) throw new DiscordjsError(ErrorCodes.GuildChannelResolve); + await this.client.rest.post(Routes.channelFollowers(this.id), { body: { webhook_channel_id: channelId }, reason }); + return this; + } +} + +module.exports = NewsChannel; diff --git a/node_modules/discord.js/src/structures/OAuth2Guild.js b/node_modules/discord.js/src/structures/OAuth2Guild.js new file mode 100644 index 0000000..d5104ac --- /dev/null +++ b/node_modules/discord.js/src/structures/OAuth2Guild.js @@ -0,0 +1,28 @@ +'use strict'; + +const BaseGuild = require('./BaseGuild'); +const PermissionsBitField = require('../util/PermissionsBitField'); + +/** + * A partial guild received when using {@link GuildManager#fetch} to fetch multiple guilds. + * @extends {BaseGuild} + */ +class OAuth2Guild extends BaseGuild { + constructor(client, data) { + super(client, data); + + /** + * Whether the client user is the owner of the guild + * @type {boolean} + */ + this.owner = data.owner; + + /** + * The permissions that the client user has in this guild + * @type {Readonly<PermissionsBitField>} + */ + this.permissions = new PermissionsBitField(BigInt(data.permissions)).freeze(); + } +} + +module.exports = OAuth2Guild; diff --git a/node_modules/discord.js/src/structures/PartialGroupDMChannel.js b/node_modules/discord.js/src/structures/PartialGroupDMChannel.js new file mode 100644 index 0000000..ecbb878 --- /dev/null +++ b/node_modules/discord.js/src/structures/PartialGroupDMChannel.js @@ -0,0 +1,60 @@ +'use strict'; + +const { BaseChannel } = require('./BaseChannel'); +const { DiscordjsError, ErrorCodes } = require('../errors'); + +/** + * Represents a Partial Group DM Channel on Discord. + * @extends {BaseChannel} + */ +class PartialGroupDMChannel extends BaseChannel { + constructor(client, data) { + super(client, data); + + // No flags are present when fetching partial group DM channels. + this.flags = null; + + /** + * The name of this Group DM Channel + * @type {?string} + */ + this.name = data.name; + + /** + * The hash of the channel icon + * @type {?string} + */ + this.icon = data.icon; + + /** + * Recipient data received in a {@link PartialGroupDMChannel}. + * @typedef {Object} PartialRecipient + * @property {string} username The username of the recipient + */ + + /** + * The recipients of this Group DM Channel. + * @type {PartialRecipient[]} + */ + this.recipients = data.recipients; + } + + /** + * The URL to this channel's icon. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + iconURL(options = {}) { + return this.icon && this.client.rest.cdn.channelIcon(this.id, this.icon, options); + } + + delete() { + return Promise.reject(new DiscordjsError(ErrorCodes.DeleteGroupDMChannel)); + } + + fetch() { + return Promise.reject(new DiscordjsError(ErrorCodes.FetchGroupDMChannel)); + } +} + +module.exports = PartialGroupDMChannel; diff --git a/node_modules/discord.js/src/structures/PermissionOverwrites.js b/node_modules/discord.js/src/structures/PermissionOverwrites.js new file mode 100644 index 0000000..2cdc827 --- /dev/null +++ b/node_modules/discord.js/src/structures/PermissionOverwrites.js @@ -0,0 +1,196 @@ +'use strict'; + +const { OverwriteType } = require('discord-api-types/v10'); +const Base = require('./Base'); +const { Role } = require('./Role'); +const { DiscordjsTypeError, ErrorCodes } = require('../errors'); +const PermissionsBitField = require('../util/PermissionsBitField'); + +/** + * Represents a permission overwrite for a role or member in a guild channel. + * @extends {Base} + */ +class PermissionOverwrites extends Base { + constructor(client, data, channel) { + super(client); + + /** + * The GuildChannel this overwrite is for + * @name PermissionOverwrites#channel + * @type {GuildChannel} + * @readonly + */ + Object.defineProperty(this, 'channel', { value: channel }); + + if (data) this._patch(data); + } + + _patch(data) { + /** + * The overwrite's id, either a {@link User} or a {@link Role} id + * @type {Snowflake} + */ + this.id = data.id; + + if ('type' in data) { + /** + * The type of this overwrite + * @type {OverwriteType} + */ + this.type = data.type; + } + + if ('deny' in data) { + /** + * The permissions that are denied for the user or role. + * @type {Readonly<PermissionsBitField>} + */ + this.deny = new PermissionsBitField(BigInt(data.deny)).freeze(); + } + + if ('allow' in data) { + /** + * The permissions that are allowed for the user or role. + * @type {Readonly<PermissionsBitField>} + */ + this.allow = new PermissionsBitField(BigInt(data.allow)).freeze(); + } + } + + /** + * Edits this Permission Overwrite. + * @param {PermissionOverwriteOptions} options The options for the update + * @param {string} [reason] Reason for creating/editing this overwrite + * @returns {Promise<PermissionOverwrites>} + * @example + * // Update permission overwrites + * permissionOverwrites.edit({ + * SendMessages: false + * }) + * .then(channel => console.log(channel.permissionOverwrites.get(message.author.id))) + * .catch(console.error); + */ + async edit(options, reason) { + await this.channel.permissionOverwrites.upsert(this.id, options, { type: this.type, reason }, this); + return this; + } + + /** + * Deletes this Permission Overwrite. + * @param {string} [reason] Reason for deleting this overwrite + * @returns {Promise<PermissionOverwrites>} + */ + async delete(reason) { + await this.channel.permissionOverwrites.delete(this.id, reason); + return this; + } + + toJSON() { + return { + id: this.id, + type: this.type, + allow: this.allow, + deny: this.deny, + }; + } + + /** + * An object mapping permission flags to `true` (enabled), `null` (unset) or `false` (disabled). + * ```js + * { + * 'SendMessages': true, + * 'EmbedLinks': null, + * 'AttachFiles': false, + * } + * ``` + * @typedef {Object} PermissionOverwriteOptions + */ + + /** + * @typedef {Object} ResolvedOverwriteOptions + * @property {PermissionsBitField} allow The allowed permissions + * @property {PermissionsBitField} deny The denied permissions + */ + + /** + * Resolves bitfield permissions overwrites from an object. + * @param {PermissionOverwriteOptions} options The options for the update + * @param {ResolvedOverwriteOptions} initialPermissions The initial permissions + * @returns {ResolvedOverwriteOptions} + */ + static resolveOverwriteOptions(options, { allow, deny } = {}) { + allow = new PermissionsBitField(allow); + deny = new PermissionsBitField(deny); + + for (const [perm, value] of Object.entries(options)) { + if (value === true) { + allow.add(perm); + deny.remove(perm); + } else if (value === false) { + allow.remove(perm); + deny.add(perm); + } else if (value === null) { + allow.remove(perm); + deny.remove(perm); + } + } + + return { allow, deny }; + } + + /** + * The raw data for a permission overwrite + * @typedef {Object} RawOverwriteData + * @property {Snowflake} id The id of the {@link Role} or {@link User} this overwrite belongs to + * @property {string} allow The permissions to allow + * @property {string} deny The permissions to deny + * @property {number} type The type of this OverwriteData + */ + + /** + * Data that can be resolved into {@link RawOverwriteData}. This can be: + * * PermissionOverwrites + * * OverwriteData + * @typedef {PermissionOverwrites|OverwriteData} OverwriteResolvable + */ + + /** + * Data that can be used for a permission overwrite + * @typedef {Object} OverwriteData + * @property {GuildMemberResolvable|RoleResolvable} id Member or role this overwrite is for + * @property {PermissionResolvable} [allow] The permissions to allow + * @property {PermissionResolvable} [deny] The permissions to deny + * @property {OverwriteType} [type] The type of this OverwriteData + */ + + /** + * Resolves an overwrite into {@link RawOverwriteData}. + * @param {OverwriteResolvable} overwrite The overwrite-like data to resolve + * @param {Guild} [guild] The guild to resolve from + * @returns {RawOverwriteData} + */ + static resolve(overwrite, guild) { + if (overwrite instanceof this) return overwrite.toJSON(); + if (typeof overwrite.id === 'string' && overwrite.type in OverwriteType) { + return { + id: overwrite.id, + type: overwrite.type, + allow: PermissionsBitField.resolve(overwrite.allow ?? PermissionsBitField.DefaultBit).toString(), + deny: PermissionsBitField.resolve(overwrite.deny ?? PermissionsBitField.DefaultBit).toString(), + }; + } + + const userOrRole = guild.roles.resolve(overwrite.id) ?? guild.client.users.resolve(overwrite.id); + if (!userOrRole) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'parameter', 'User nor a Role'); + const type = userOrRole instanceof Role ? OverwriteType.Role : OverwriteType.Member; + + return { + id: userOrRole.id, + type, + allow: PermissionsBitField.resolve(overwrite.allow ?? PermissionsBitField.DefaultBit).toString(), + deny: PermissionsBitField.resolve(overwrite.deny ?? PermissionsBitField.DefaultBit).toString(), + }; + } +} + +module.exports = PermissionOverwrites; diff --git a/node_modules/discord.js/src/structures/Presence.js b/node_modules/discord.js/src/structures/Presence.js new file mode 100644 index 0000000..79ecb29 --- /dev/null +++ b/node_modules/discord.js/src/structures/Presence.js @@ -0,0 +1,378 @@ +'use strict'; + +const Base = require('./Base'); +const { Emoji } = require('./Emoji'); +const ActivityFlagsBitField = require('../util/ActivityFlagsBitField'); +const { flatten } = require('../util/Util'); + +/** + * Activity sent in a message. + * @typedef {Object} MessageActivity + * @property {string} [partyId] Id of the party represented in activity + * @property {MessageActivityType} type Type of activity sent + */ + +/** + * The status of this presence: + * * **`online`** - user is online + * * **`idle`** - user is AFK + * * **`offline`** - user is offline or invisible + * * **`dnd`** - user is in Do Not Disturb + * @typedef {string} PresenceStatus + */ + +/** + * The status of this presence: + * * **`online`** - user is online + * * **`idle`** - user is AFK + * * **`dnd`** - user is in Do Not Disturb + * @typedef {string} ClientPresenceStatus + */ + +/** + * Represents a user's presence. + * @extends {Base} + */ +class Presence extends Base { + constructor(client, data = {}) { + super(client); + + /** + * The presence's user id + * @type {Snowflake} + */ + this.userId = data.user.id; + + /** + * The guild this presence is in + * @type {?Guild} + */ + this.guild = data.guild ?? null; + + this._patch(data); + } + + /** + * The user of this presence + * @type {?User} + * @readonly + */ + get user() { + return this.client.users.resolve(this.userId); + } + + /** + * The member of this presence + * @type {?GuildMember} + * @readonly + */ + get member() { + return this.guild.members.resolve(this.userId); + } + + _patch(data) { + if ('status' in data) { + /** + * The status of this presence + * @type {PresenceStatus} + */ + this.status = data.status; + } else { + this.status ??= 'offline'; + } + + if ('activities' in data) { + /** + * The activities of this presence + * @type {Activity[]} + */ + this.activities = data.activities.map(activity => new Activity(this, activity)); + } else { + this.activities ??= []; + } + + if ('client_status' in data) { + /** + * The devices this presence is on + * @type {?Object} + * @property {?ClientPresenceStatus} web The current presence in the web application + * @property {?ClientPresenceStatus} mobile The current presence in the mobile application + * @property {?ClientPresenceStatus} desktop The current presence in the desktop application + */ + this.clientStatus = data.client_status; + } else { + this.clientStatus ??= null; + } + + return this; + } + + _clone() { + const clone = Object.assign(Object.create(this), this); + clone.activities = this.activities.map(activity => activity._clone()); + return clone; + } + + /** + * Whether this presence is equal to another. + * @param {Presence} presence The presence to compare with + * @returns {boolean} + */ + equals(presence) { + return ( + this === presence || + (presence && + this.status === presence.status && + this.activities.length === presence.activities.length && + this.activities.every((activity, index) => activity.equals(presence.activities[index])) && + this.clientStatus?.web === presence.clientStatus?.web && + this.clientStatus?.mobile === presence.clientStatus?.mobile && + this.clientStatus?.desktop === presence.clientStatus?.desktop) + ); + } + + toJSON() { + return flatten(this); + } +} + +/** + * Represents an activity that is part of a user's presence. + */ +class Activity { + constructor(presence, data) { + /** + * The presence of the Activity + * @type {Presence} + * @readonly + * @name Activity#presence + */ + Object.defineProperty(this, 'presence', { value: presence }); + + /** + * The activity's name + * @type {string} + */ + this.name = data.name; + + /** + * The activity status's type + * @type {ActivityType} + */ + this.type = data.type; + + /** + * If the activity is being streamed, a link to the stream + * @type {?string} + */ + this.url = data.url ?? null; + + /** + * Details about the activity + * @type {?string} + */ + this.details = data.details ?? null; + + /** + * State of the activity + * @type {?string} + */ + this.state = data.state ?? null; + + /** + * The id of the application associated with this activity + * @type {?Snowflake} + */ + this.applicationId = data.application_id ?? null; + + /** + * Represents timestamps of an activity + * @typedef {Object} ActivityTimestamps + * @property {?Date} start When the activity started + * @property {?Date} end When the activity will end + */ + + /** + * Timestamps for the activity + * @type {?ActivityTimestamps} + */ + this.timestamps = data.timestamps + ? { + start: data.timestamps.start ? new Date(Number(data.timestamps.start)) : null, + end: data.timestamps.end ? new Date(Number(data.timestamps.end)) : null, + } + : null; + + /** + * Represents a party of an activity + * @typedef {Object} ActivityParty + * @property {?string} id The party's id + * @property {number[]} size Size of the party as `[current, max]` + */ + + /** + * Party of the activity + * @type {?ActivityParty} + */ + this.party = data.party ?? null; + + /** + * Assets for rich presence + * @type {?RichPresenceAssets} + */ + this.assets = data.assets ? new RichPresenceAssets(this, data.assets) : null; + + /** + * Flags that describe the activity + * @type {Readonly<ActivityFlagsBitField>} + */ + this.flags = new ActivityFlagsBitField(data.flags).freeze(); + + /** + * Emoji for a custom activity + * @type {?Emoji} + */ + this.emoji = data.emoji ? new Emoji(presence.client, data.emoji) : null; + + /** + * The labels of the buttons of this rich presence + * @type {string[]} + */ + this.buttons = data.buttons ?? []; + + /** + * Creation date of the activity + * @type {number} + */ + this.createdTimestamp = data.created_at; + } + + /** + * Whether this activity is equal to another activity. + * @param {Activity} activity The activity to compare with + * @returns {boolean} + */ + equals(activity) { + return ( + this === activity || + (activity && + this.name === activity.name && + this.type === activity.type && + this.url === activity.url && + this.state === activity.state && + this.details === activity.details && + this.emoji?.id === activity.emoji?.id && + this.emoji?.name === activity.emoji?.name) + ); + } + + /** + * The time the activity was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * When concatenated with a string, this automatically returns the activity's name instead of the Activity object. + * @returns {string} + */ + toString() { + return this.name; + } + + _clone() { + return Object.assign(Object.create(this), this); + } +} + +/** + * Assets for a rich presence + */ +class RichPresenceAssets { + constructor(activity, assets) { + /** + * The activity of the RichPresenceAssets + * @type {Activity} + * @readonly + * @name RichPresenceAssets#activity + */ + Object.defineProperty(this, 'activity', { value: activity }); + + /** + * Hover text for the large image + * @type {?string} + */ + this.largeText = assets.large_text ?? null; + + /** + * Hover text for the small image + * @type {?string} + */ + this.smallText = assets.small_text ?? null; + + /** + * The large image asset's id + * @type {?Snowflake} + */ + this.largeImage = assets.large_image ?? null; + + /** + * The small image asset's id + * @type {?Snowflake} + */ + this.smallImage = assets.small_image ?? null; + } + + /** + * Gets the URL of the small image asset + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + smallImageURL(options = {}) { + if (!this.smallImage) return null; + if (this.smallImage.includes(':')) { + const [platform, id] = this.smallImage.split(':'); + switch (platform) { + case 'mp': + return `https://media.discordapp.net/${id}`; + default: + return null; + } + } + + return this.activity.presence.client.rest.cdn.appAsset(this.activity.applicationId, this.smallImage, options); + } + + /** + * Gets the URL of the large image asset + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + largeImageURL(options = {}) { + if (!this.largeImage) return null; + if (this.largeImage.includes(':')) { + const [platform, id] = this.largeImage.split(':'); + switch (platform) { + case 'mp': + return `https://media.discordapp.net/${id}`; + case 'spotify': + return `https://i.scdn.co/image/${id}`; + case 'youtube': + return `https://i.ytimg.com/vi/${id}/hqdefault_live.jpg`; + case 'twitch': + return `https://static-cdn.jtvnw.net/previews-ttv/live_user_${id}.png`; + default: + return null; + } + } + + return this.activity.presence.client.rest.cdn.appAsset(this.activity.applicationId, this.largeImage, options); + } +} + +exports.Presence = Presence; +exports.Activity = Activity; +exports.RichPresenceAssets = RichPresenceAssets; diff --git a/node_modules/discord.js/src/structures/ReactionCollector.js b/node_modules/discord.js/src/structures/ReactionCollector.js new file mode 100644 index 0000000..924b33b --- /dev/null +++ b/node_modules/discord.js/src/structures/ReactionCollector.js @@ -0,0 +1,229 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const Collector = require('./interfaces/Collector'); +const Events = require('../util/Events'); + +/** + * @typedef {CollectorOptions} ReactionCollectorOptions + * @property {number} max The maximum total amount of reactions to collect + * @property {number} maxEmojis The maximum number of emojis to collect + * @property {number} maxUsers The maximum number of users to react + */ + +/** + * Collects reactions on messages. + * Will automatically stop if the message ({@link Client#event:messageDelete messageDelete} or + * {@link Client#event:messageDeleteBulk messageDeleteBulk}), + * channel ({@link Client#event:channelDelete channelDelete}), + * thread ({@link Client#event:threadDelete threadDelete}), or + * guild ({@link Client#event:guildDelete guildDelete}) is deleted. + * @extends {Collector} + */ +class ReactionCollector extends Collector { + /** + * @param {Message} message The message upon which to collect reactions + * @param {ReactionCollectorOptions} [options={}] The options to apply to this collector + */ + constructor(message, options = {}) { + super(message.client, options); + + /** + * The message upon which to collect reactions + * @type {Message} + */ + this.message = message; + + /** + * The users that have reacted to this message + * @type {Collection} + */ + this.users = new Collection(); + + /** + * The total number of reactions collected + * @type {number} + */ + this.total = 0; + + this.empty = this.empty.bind(this); + this._handleChannelDeletion = this._handleChannelDeletion.bind(this); + this._handleThreadDeletion = this._handleThreadDeletion.bind(this); + this._handleGuildDeletion = this._handleGuildDeletion.bind(this); + this._handleMessageDeletion = this._handleMessageDeletion.bind(this); + + const bulkDeleteListener = messages => { + if (messages.has(this.message.id)) this.stop('messageDelete'); + }; + + this.client.incrementMaxListeners(); + this.client.on(Events.MessageReactionAdd, this.handleCollect); + this.client.on(Events.MessageReactionRemove, this.handleDispose); + this.client.on(Events.MessageReactionRemoveAll, this.empty); + this.client.on(Events.MessageDelete, this._handleMessageDeletion); + this.client.on(Events.MessageBulkDelete, bulkDeleteListener); + this.client.on(Events.ChannelDelete, this._handleChannelDeletion); + this.client.on(Events.ThreadDelete, this._handleThreadDeletion); + this.client.on(Events.GuildDelete, this._handleGuildDeletion); + + this.once('end', () => { + this.client.removeListener(Events.MessageReactionAdd, this.handleCollect); + this.client.removeListener(Events.MessageReactionRemove, this.handleDispose); + this.client.removeListener(Events.MessageReactionRemoveAll, this.empty); + this.client.removeListener(Events.MessageDelete, this._handleMessageDeletion); + this.client.removeListener(Events.MessageBulkDelete, bulkDeleteListener); + this.client.removeListener(Events.ChannelDelete, this._handleChannelDeletion); + this.client.removeListener(Events.ThreadDelete, this._handleThreadDeletion); + this.client.removeListener(Events.GuildDelete, this._handleGuildDeletion); + this.client.decrementMaxListeners(); + }); + + this.on('collect', (reaction, user) => { + /** + * Emitted whenever a reaction is newly created on a message. Will emit only when a new reaction is + * added to the message, as opposed to {@link Collector#collect} which will + * be emitted even when a reaction has already been added to the message. + * @event ReactionCollector#create + * @param {MessageReaction} reaction The reaction that was added + * @param {User} user The user that added the reaction + */ + if (reaction.count === 1) { + this.emit('create', reaction, user); + } + this.total++; + this.users.set(user.id, user); + }); + + this.on('remove', (reaction, user) => { + this.total--; + if (!this.collected.some(r => r.users.cache.has(user.id))) this.users.delete(user.id); + }); + } + + /** + * Handles an incoming reaction for possible collection. + * @param {MessageReaction} reaction The reaction to possibly collect + * @param {User} user The user that added the reaction + * @returns {?(Snowflake|string)} + * @private + */ + collect(reaction) { + /** + * Emitted whenever a reaction is collected. + * @event ReactionCollector#collect + * @param {MessageReaction} reaction The reaction that was collected + * @param {User} user The user that added the reaction + */ + if (reaction.message.id !== this.message.id) return null; + + return ReactionCollector.key(reaction); + } + + /** + * Handles a reaction deletion for possible disposal. + * @param {MessageReaction} reaction The reaction to possibly dispose of + * @param {User} user The user that removed the reaction + * @returns {?(Snowflake|string)} + */ + dispose(reaction, user) { + /** + * Emitted when the reaction had all the users removed and the `dispose` option is set to true. + * @event ReactionCollector#dispose + * @param {MessageReaction} reaction The reaction that was disposed of + * @param {User} user The user that removed the reaction + */ + if (reaction.message.id !== this.message.id) return null; + + /** + * Emitted when the reaction had one user removed and the `dispose` option is set to true. + * @event ReactionCollector#remove + * @param {MessageReaction} reaction The reaction that was removed + * @param {User} user The user that removed the reaction + */ + if (this.collected.has(ReactionCollector.key(reaction)) && this.users.has(user.id)) { + this.emit('remove', reaction, user); + } + return reaction.count ? null : ReactionCollector.key(reaction); + } + + /** + * Empties this reaction collector. + */ + empty() { + this.total = 0; + this.collected.clear(); + this.users.clear(); + this.checkEnd(); + } + + /** + * The reason this collector has ended with, or null if it hasn't ended yet + * @type {?string} + * @readonly + */ + get endReason() { + if (this.options.max && this.total >= this.options.max) return 'limit'; + if (this.options.maxEmojis && this.collected.size >= this.options.maxEmojis) return 'emojiLimit'; + if (this.options.maxUsers && this.users.size >= this.options.maxUsers) return 'userLimit'; + return super.endReason; + } + + /** + * Handles checking if the message has been deleted, and if so, stops the collector with the reason 'messageDelete'. + * @private + * @param {Message} message The message that was deleted + * @returns {void} + */ + _handleMessageDeletion(message) { + if (message.id === this.message.id) { + this.stop('messageDelete'); + } + } + + /** + * Handles checking if the channel has been deleted, and if so, stops the collector with the reason 'channelDelete'. + * @private + * @param {GuildChannel} channel The channel that was deleted + * @returns {void} + */ + _handleChannelDeletion(channel) { + if (channel.id === this.message.channelId || channel.threads?.cache.has(this.message.channelId)) { + this.stop('channelDelete'); + } + } + + /** + * Handles checking if the thread has been deleted, and if so, stops the collector with the reason 'threadDelete'. + * @private + * @param {ThreadChannel} thread The thread that was deleted + * @returns {void} + */ + _handleThreadDeletion(thread) { + if (thread.id === this.message.channelId) { + this.stop('threadDelete'); + } + } + + /** + * Handles checking if the guild has been deleted, and if so, stops the collector with the reason 'guildDelete'. + * @private + * @param {Guild} guild The guild that was deleted + * @returns {void} + */ + _handleGuildDeletion(guild) { + if (guild.id === this.message.guild?.id) { + this.stop('guildDelete'); + } + } + + /** + * Gets the collector key for a reaction. + * @param {MessageReaction} reaction The message reaction to get the key for + * @returns {Snowflake|string} + */ + static key(reaction) { + return reaction.emoji.id ?? reaction.emoji.name; + } +} + +module.exports = ReactionCollector; diff --git a/node_modules/discord.js/src/structures/ReactionEmoji.js b/node_modules/discord.js/src/structures/ReactionEmoji.js new file mode 100644 index 0000000..08e2ea0 --- /dev/null +++ b/node_modules/discord.js/src/structures/ReactionEmoji.js @@ -0,0 +1,31 @@ +'use strict'; + +const { Emoji } = require('./Emoji'); +const { flatten } = require('../util/Util'); + +/** + * Represents a limited emoji set used for both custom and unicode emojis. Custom emojis + * will use this class opposed to the Emoji class when the client doesn't know enough + * information about them. + * @extends {Emoji} + */ +class ReactionEmoji extends Emoji { + constructor(reaction, emoji) { + super(reaction.message.client, emoji); + /** + * The message reaction this emoji refers to + * @type {MessageReaction} + */ + this.reaction = reaction; + } + + toJSON() { + return flatten(this, { identifier: true }); + } + + valueOf() { + return this.id; + } +} + +module.exports = ReactionEmoji; diff --git a/node_modules/discord.js/src/structures/Role.js b/node_modules/discord.js/src/structures/Role.js new file mode 100644 index 0000000..09a2a52 --- /dev/null +++ b/node_modules/discord.js/src/structures/Role.js @@ -0,0 +1,471 @@ +'use strict'; + +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { PermissionFlagsBits } = require('discord-api-types/v10'); +const Base = require('./Base'); +const { DiscordjsError, ErrorCodes } = require('../errors'); +const PermissionsBitField = require('../util/PermissionsBitField'); +const RoleFlagsBitField = require('../util/RoleFlagsBitField'); + +/** + * Represents a role on Discord. + * @extends {Base} + */ +class Role extends Base { + constructor(client, data, guild) { + super(client); + + /** + * The guild that the role belongs to + * @type {Guild} + */ + this.guild = guild; + + /** + * The icon hash of the role + * @type {?string} + */ + this.icon = null; + + /** + * The unicode emoji for the role + * @type {?string} + */ + this.unicodeEmoji = null; + + if (data) this._patch(data); + } + + _patch(data) { + /** + * The role's id (unique to the guild it is part of) + * @type {Snowflake} + */ + this.id = data.id; + if ('name' in data) { + /** + * The name of the role + * @type {string} + */ + this.name = data.name; + } + + if ('color' in data) { + /** + * The base 10 color of the role + * @type {number} + */ + this.color = data.color; + } + + if ('hoist' in data) { + /** + * If true, users that are part of this role will appear in a separate category in the users list + * @type {boolean} + */ + this.hoist = data.hoist; + } + + if ('position' in data) { + /** + * The raw position of the role from the API + * @type {number} + */ + this.rawPosition = data.position; + } + + if ('permissions' in data) { + /** + * The permissions of the role + * @type {Readonly<PermissionsBitField>} + */ + this.permissions = new PermissionsBitField(BigInt(data.permissions)).freeze(); + } + + if ('managed' in data) { + /** + * Whether or not the role is managed by an external service + * @type {boolean} + */ + this.managed = data.managed; + } + + if ('mentionable' in data) { + /** + * Whether or not the role can be mentioned by anyone + * @type {boolean} + */ + this.mentionable = data.mentionable; + } + + if ('icon' in data) this.icon = data.icon; + + if ('unicode_emoji' in data) this.unicodeEmoji = data.unicode_emoji; + + if ('flags' in data) { + /** + * The flags of this role + * @type {Readonly<RoleFlagsBitField>} + */ + this.flags = new RoleFlagsBitField(data.flags).freeze(); + } else { + this.flags ??= new RoleFlagsBitField().freeze(); + } + + /** + * The tags this role has + * @type {?Object} + * @property {Snowflake} [botId] The id of the bot this role belongs to + * @property {Snowflake|string} [integrationId] The id of the integration this role belongs to + * @property {true} [premiumSubscriberRole] Whether this is the guild's premium subscription role + * @property {Snowflake} [subscriptionListingId] The id of this role's subscription SKU and listing + * @property {true} [availableForPurchase] Whether this role is available for purchase + * @property {true} [guildConnections] Whether this role is a guild's linked role + */ + this.tags = data.tags ? {} : null; + if (data.tags) { + if ('bot_id' in data.tags) { + this.tags.botId = data.tags.bot_id; + } + if ('integration_id' in data.tags) { + this.tags.integrationId = data.tags.integration_id; + } + if ('premium_subscriber' in data.tags) { + this.tags.premiumSubscriberRole = true; + } + if ('subscription_listing_id' in data.tags) { + this.tags.subscriptionListingId = data.tags.subscription_listing_id; + } + if ('available_for_purchase' in data.tags) { + this.tags.availableForPurchase = true; + } + if ('guild_connections' in data.tags) { + this.tags.guildConnections = true; + } + } + } + + /** + * The timestamp the role was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the role was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The hexadecimal version of the role color, with a leading hashtag + * @type {string} + * @readonly + */ + get hexColor() { + return `#${this.color.toString(16).padStart(6, '0')}`; + } + + /** + * The cached guild members that have this role + * @type {Collection<Snowflake, GuildMember>} + * @readonly + */ + get members() { + return this.id === this.guild.id + ? this.guild.members.cache.clone() + : this.guild.members.cache.filter(m => m._roles.includes(this.id)); + } + + /** + * Whether the role is editable by the client user + * @type {boolean} + * @readonly + */ + get editable() { + if (this.managed) return false; + const clientMember = this.guild.members.resolve(this.client.user); + if (!clientMember.permissions.has(PermissionFlagsBits.ManageRoles)) return false; + return clientMember.roles.highest.comparePositionTo(this) > 0; + } + + /** + * The position of the role in the role manager + * @type {number} + * @readonly + */ + get position() { + return this.guild.roles.cache.reduce( + (acc, role) => + acc + + (this.rawPosition === role.rawPosition + ? BigInt(this.id) > BigInt(role.id) + : this.rawPosition > role.rawPosition), + 0, + ); + } + + /** + * Compares this role's position to another role's. + * @param {RoleResolvable} role Role to compare to this one + * @returns {number} Negative number if this role's position is lower (other role's is higher), + * positive number if this one is higher (other's is lower), 0 if equal + * @example + * // Compare the position of a role to another + * const roleCompare = role.comparePositionTo(otherRole); + * if (roleCompare >= 1) console.log(`${role.name} is higher than ${otherRole.name}`); + */ + comparePositionTo(role) { + return this.guild.roles.comparePositions(this, role); + } + + /** + * The data for a role. + * @typedef {Object} RoleData + * @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 or not 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 or not the 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 + */ + + /** + * Edits the role. + * @param {RoleEditOptions} options The options to provide + * @returns {Promise<Role>} + * @example + * // Edit a role + * role.edit({ name: 'new role' }) + * .then(updated => console.log(`Edited role name to ${updated.name}`)) + * .catch(console.error); + */ + edit(options) { + return this.guild.roles.edit(this, options); + } + + /** + * Returns `channel.permissionsFor(role)`. Returns permissions for a role in a guild channel, + * taking into account permission overwrites. + * @param {GuildChannel|Snowflake} channel The guild channel to use as context + * @param {boolean} [checkAdmin=true] Whether having the {@link PermissionFlagsBits.Administrator} permission + * will return all permissions + * @returns {Readonly<PermissionsBitField>} + */ + permissionsIn(channel, checkAdmin = true) { + channel = this.guild.channels.resolve(channel); + if (!channel) throw new DiscordjsError(ErrorCodes.GuildChannelResolve); + return channel.rolePermissions(this, checkAdmin); + } + + /** + * Sets a new name for the role. + * @param {string} name The new name of the role + * @param {string} [reason] Reason for changing the role's name + * @returns {Promise<Role>} + * @example + * // Set the name of the role + * role.setName('new role') + * .then(updated => console.log(`Updated role name to ${updated.name}`)) + * .catch(console.error); + */ + setName(name, reason) { + return this.edit({ name, reason }); + } + + /** + * Sets a new color for the role. + * @param {ColorResolvable} color The color of the role + * @param {string} [reason] Reason for changing the role's color + * @returns {Promise<Role>} + * @example + * // Set the color of a role + * role.setColor('#FF0000') + * .then(updated => console.log(`Set color of role to ${updated.color}`)) + * .catch(console.error); + */ + setColor(color, reason) { + return this.edit({ color, reason }); + } + + /** + * Sets whether or not the role should be hoisted. + * @param {boolean} [hoist=true] Whether or not to hoist the role + * @param {string} [reason] Reason for setting whether or not the role should be hoisted + * @returns {Promise<Role>} + * @example + * // Set the hoist of the role + * role.setHoist(true) + * .then(updated => console.log(`Role hoisted: ${updated.hoist}`)) + * .catch(console.error); + */ + setHoist(hoist = true, reason) { + return this.edit({ hoist, reason }); + } + + /** + * Sets the permissions of the role. + * @param {PermissionResolvable} permissions The permissions of the role + * @param {string} [reason] Reason for changing the role's permissions + * @returns {Promise<Role>} + * @example + * // Set the permissions of the role + * role.setPermissions([PermissionFlagsBits.KickMembers, PermissionFlagsBits.BanMembers]) + * .then(updated => console.log(`Updated permissions to ${updated.permissions.bitfield}`)) + * .catch(console.error); + * @example + * // Remove all permissions from a role + * role.setPermissions(0n) + * .then(updated => console.log(`Updated permissions to ${updated.permissions.bitfield}`)) + * .catch(console.error); + */ + setPermissions(permissions, reason) { + return this.edit({ permissions, reason }); + } + + /** + * Sets whether this role is mentionable. + * @param {boolean} [mentionable=true] Whether this role should be mentionable + * @param {string} [reason] Reason for setting whether or not this role should be mentionable + * @returns {Promise<Role>} + * @example + * // Make the role mentionable + * role.setMentionable(true) + * .then(updated => console.log(`Role updated ${updated.name}`)) + * .catch(console.error); + */ + setMentionable(mentionable = true, reason) { + return this.edit({ mentionable, reason }); + } + + /** + * Sets a new icon for the role. + * @param {?(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> + * @param {string} [reason] Reason for changing the role's icon + * @returns {Promise<Role>} + */ + setIcon(icon, reason) { + return this.edit({ icon, reason }); + } + + /** + * Sets a new unicode emoji for the role. + * @param {?string} unicodeEmoji The new unicode emoji for the role + * @param {string} [reason] Reason for changing the role's unicode emoji + * @returns {Promise<Role>} + * @example + * // Set a new unicode emoji for the role + * role.setUnicodeEmoji('🤖') + * .then(updated => console.log(`Set unicode emoji for the role to ${updated.unicodeEmoji}`)) + * .catch(console.error); + */ + setUnicodeEmoji(unicodeEmoji, reason) { + return this.edit({ unicodeEmoji, reason }); + } + + /** + * Options used to set the position of a role. + * @typedef {Object} SetRolePositionOptions + * @property {boolean} [relative=false] Whether to change the position relative to its current value or not + * @property {string} [reason] The reason for changing the position + */ + + /** + * Sets the new position of the role. + * @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 + * role.setPosition(1) + * .then(updated => console.log(`Role position: ${updated.position}`)) + * .catch(console.error); + */ + setPosition(position, options = {}) { + return this.guild.roles.setPosition(this, position, options); + } + + /** + * Deletes the role. + * @param {string} [reason] Reason for deleting this role + * @returns {Promise<Role>} + * @example + * // Delete a role + * role.delete('The role needed to go') + * .then(deleted => console.log(`Deleted role ${deleted.name}`)) + * .catch(console.error); + */ + async delete(reason) { + await this.guild.roles.delete(this.id, reason); + return this; + } + + /** + * A link to the role's icon + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + iconURL(options = {}) { + return this.icon && this.client.rest.cdn.roleIcon(this.id, this.icon, options); + } + + /** + * Whether this role equals another role. It compares all properties, so for most operations + * it is advisable to just compare `role.id === role2.id` as it is much faster and is often + * what most users need. + * @param {Role} role Role to compare with + * @returns {boolean} + */ + equals(role) { + return ( + role && + this.id === role.id && + this.name === role.name && + this.color === role.color && + this.hoist === role.hoist && + this.position === role.position && + this.permissions.bitfield === role.permissions.bitfield && + this.managed === role.managed && + this.icon === role.icon && + this.unicodeEmoji === role.unicodeEmoji + ); + } + + /** + * When concatenated with a string, this automatically returns the role's mention instead of the Role object. + * @returns {string} + * @example + * // Logs: Role: <@&123456789012345678> + * console.log(`Role: ${role}`); + */ + toString() { + if (this.id === this.guild.id) return '@everyone'; + return `<@&${this.id}>`; + } + + toJSON() { + return { + ...super.toJSON({ createdTimestamp: true }), + permissions: this.permissions.toJSON(), + }; + } +} + +exports.Role = Role; + +/** + * @external APIRole + * @see {@link https://discord.com/developers/docs/topics/permissions#role-object} + */ diff --git a/node_modules/discord.js/src/structures/RoleSelectMenuBuilder.js b/node_modules/discord.js/src/structures/RoleSelectMenuBuilder.js new file mode 100644 index 0000000..0d80de5 --- /dev/null +++ b/node_modules/discord.js/src/structures/RoleSelectMenuBuilder.js @@ -0,0 +1,31 @@ +'use strict'; + +const { RoleSelectMenuBuilder: BuildersRoleSelectMenu } = require('@discordjs/builders'); +const { isJSONEncodable } = require('@discordjs/util'); +const { toSnakeCase } = require('../util/Transformers'); + +/** + * Class used to build select menu components to be sent through the API + * @extends {BuildersRoleSelectMenu} + */ +class RoleSelectMenuBuilder extends BuildersRoleSelectMenu { + constructor(data = {}) { + super(toSnakeCase(data)); + } + + /** + * Creates a new select menu builder from JSON data + * @param {RoleSelectMenuBuilder|RoleSelectMenuComponent|APIRoleSelectComponent} other The other data + * @returns {RoleSelectMenuBuilder} + */ + static from(other) { + return new this(isJSONEncodable(other) ? other.toJSON() : other); + } +} + +module.exports = RoleSelectMenuBuilder; + +/** + * @external BuildersRoleSelectMenu + * @see {@link https://discord.js.org/docs/packages/builders/stable/RoleSelectMenuBuilder:Class} + */ diff --git a/node_modules/discord.js/src/structures/RoleSelectMenuComponent.js b/node_modules/discord.js/src/structures/RoleSelectMenuComponent.js new file mode 100644 index 0000000..1b27942 --- /dev/null +++ b/node_modules/discord.js/src/structures/RoleSelectMenuComponent.js @@ -0,0 +1,11 @@ +'use strict'; + +const BaseSelectMenuComponent = require('./BaseSelectMenuComponent'); + +/** + * Represents a role select menu component + * @extends {BaseSelectMenuComponent} + */ +class RoleSelectMenuComponent extends BaseSelectMenuComponent {} + +module.exports = RoleSelectMenuComponent; diff --git a/node_modules/discord.js/src/structures/RoleSelectMenuInteraction.js b/node_modules/discord.js/src/structures/RoleSelectMenuInteraction.js new file mode 100644 index 0000000..eb42eff --- /dev/null +++ b/node_modules/discord.js/src/structures/RoleSelectMenuInteraction.js @@ -0,0 +1,33 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const MessageComponentInteraction = require('./MessageComponentInteraction'); + +/** + * Represents a {@link ComponentType.RoleSelect} select menu interaction. + * @extends {MessageComponentInteraction} + */ +class RoleSelectMenuInteraction extends MessageComponentInteraction { + constructor(client, data) { + super(client, data); + const { resolved, values } = data.data; + + /** + * An array of the selected role ids + * @type {Snowflake[]} + */ + this.values = values ?? []; + + /** + * Collection of the selected roles + * @type {Collection<Snowflake, Role|APIRole>} + */ + this.roles = new Collection(); + + for (const role of Object.values(resolved?.roles ?? {})) { + this.roles.set(role.id, this.guild?.roles._add(role) ?? role); + } + } +} + +module.exports = RoleSelectMenuInteraction; diff --git a/node_modules/discord.js/src/structures/SelectMenuBuilder.js b/node_modules/discord.js/src/structures/SelectMenuBuilder.js new file mode 100644 index 0000000..a779370 --- /dev/null +++ b/node_modules/discord.js/src/structures/SelectMenuBuilder.js @@ -0,0 +1,26 @@ +'use strict'; + +const process = require('node:process'); +const StringSelectMenuBuilder = require('./StringSelectMenuBuilder'); + +let deprecationEmitted = false; + +/** + * @deprecated Use {@link StringSelectMenuBuilder} instead. + * @extends {StringSelectMenuBuilder} + */ +class SelectMenuBuilder extends StringSelectMenuBuilder { + constructor(...params) { + super(...params); + + if (!deprecationEmitted) { + process.emitWarning( + 'The SelectMenuBuilder class is deprecated. Use StringSelectMenuBuilder instead.', + 'DeprecationWarning', + ); + deprecationEmitted = true; + } + } +} + +module.exports = SelectMenuBuilder; diff --git a/node_modules/discord.js/src/structures/SelectMenuComponent.js b/node_modules/discord.js/src/structures/SelectMenuComponent.js new file mode 100644 index 0000000..2cd8097 --- /dev/null +++ b/node_modules/discord.js/src/structures/SelectMenuComponent.js @@ -0,0 +1,26 @@ +'use strict'; + +const process = require('node:process'); +const StringSelectMenuComponent = require('./StringSelectMenuComponent'); + +let deprecationEmitted = false; + +/** + * @deprecated Use {@link StringSelectMenuComponent} instead. + * @extends {StringSelectMenuComponent} + */ +class SelectMenuComponent extends StringSelectMenuComponent { + constructor(...params) { + super(...params); + + if (!deprecationEmitted) { + process.emitWarning( + 'The SelectMenuComponent class is deprecated. Use StringSelectMenuComponent instead.', + 'DeprecationWarning', + ); + deprecationEmitted = true; + } + } +} + +module.exports = SelectMenuComponent; diff --git a/node_modules/discord.js/src/structures/SelectMenuInteraction.js b/node_modules/discord.js/src/structures/SelectMenuInteraction.js new file mode 100644 index 0000000..a096559 --- /dev/null +++ b/node_modules/discord.js/src/structures/SelectMenuInteraction.js @@ -0,0 +1,26 @@ +'use strict'; + +const process = require('node:process'); +const StringSelectMenuInteraction = require('./StringSelectMenuInteraction'); + +let deprecationEmitted = false; + +/** + * @deprecated Use {@link StringSelectMenuInteraction} instead. + * @extends {StringSelectMenuInteraction} + */ +class SelectMenuInteraction extends StringSelectMenuInteraction { + constructor(...params) { + super(...params); + + if (!deprecationEmitted) { + process.emitWarning( + 'The SelectMenuInteraction class is deprecated. Use StringSelectMenuInteraction instead.', + 'DeprecationWarning', + ); + deprecationEmitted = true; + } + } +} + +module.exports = SelectMenuInteraction; diff --git a/node_modules/discord.js/src/structures/SelectMenuOptionBuilder.js b/node_modules/discord.js/src/structures/SelectMenuOptionBuilder.js new file mode 100644 index 0000000..85309d1 --- /dev/null +++ b/node_modules/discord.js/src/structures/SelectMenuOptionBuilder.js @@ -0,0 +1,26 @@ +'use strict'; + +const process = require('node:process'); +const StringSelectMenuOptionBuilder = require('./StringSelectMenuOptionBuilder'); + +let deprecationEmitted = false; + +/** + * @deprecated Use {@link StringSelectMenuOptionBuilder} instead. + * @extends {StringSelectMenuOptionBuilder} + */ +class SelectMenuOptionBuilder extends StringSelectMenuOptionBuilder { + constructor(...params) { + super(...params); + + if (!deprecationEmitted) { + process.emitWarning( + 'The SelectMenuOptionBuilder class is deprecated. Use StringSelectMenuOptionBuilder instead.', + 'DeprecationWarning', + ); + deprecationEmitted = true; + } + } +} + +module.exports = SelectMenuOptionBuilder; diff --git a/node_modules/discord.js/src/structures/StageChannel.js b/node_modules/discord.js/src/structures/StageChannel.js new file mode 100644 index 0000000..2661489 --- /dev/null +++ b/node_modules/discord.js/src/structures/StageChannel.js @@ -0,0 +1,112 @@ +'use strict'; + +const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel'); + +/** + * Represents a guild stage channel on Discord. + * @extends {BaseGuildVoiceChannel} + */ +class StageChannel extends BaseGuildVoiceChannel { + _patch(data) { + super._patch(data); + + if ('topic' in data) { + /** + * The topic of the stage channel + * @type {?string} + */ + this.topic = data.topic; + } + } + + /** + * The stage instance of this stage channel, if it exists + * @type {?StageInstance} + * @readonly + */ + get stageInstance() { + return this.guild.stageInstances.cache.find(stageInstance => stageInstance.channelId === this.id) ?? null; + } + + /** + * Creates a stage instance associated with this stage channel. + * @param {StageInstanceCreateOptions} options The options to create the stage instance + * @returns {Promise<StageInstance>} + */ + createStageInstance(options) { + return this.guild.stageInstances.create(this.id, options); + } + + /** + * Sets a new topic for the guild channel. + * @param {?string} topic The new topic for the guild channel + * @param {string} [reason] Reason for changing the guild channel's topic + * @returns {Promise<StageChannel>} + * @example + * // Set a new channel topic + * stageChannel.setTopic('needs more rate limiting') + * .then(channel => console.log(`Channel's new topic is ${channel.topic}`)) + * .catch(console.error); + */ + setTopic(topic, reason) { + return this.edit({ topic, reason }); + } +} + +/** + * Sets the bitrate of the channel. + * @method setBitrate + * @memberof StageChannel + * @instance + * @param {number} bitrate The new bitrate + * @param {string} [reason] Reason for changing the channel's bitrate + * @returns {Promise<StageChannel>} + * @example + * // Set the bitrate of a voice channel + * stageChannel.setBitrate(48_000) + * .then(channel => console.log(`Set bitrate to ${channel.bitrate}bps for ${channel.name}`)) + * .catch(console.error); + */ + +/** + * Sets the RTC region of the channel. + * @method setRTCRegion + * @memberof StageChannel + * @instance + * @param {?string} rtcRegion The new region of the channel. Set to `null` to remove a specific region for the channel + * @param {string} [reason] The reason for modifying this region. + * @returns {Promise<StageChannel>} + * @example + * // Set the RTC region to sydney + * stageChannel.setRTCRegion('sydney'); + * @example + * // Remove a fixed region for this channel - let Discord decide automatically + * stageChannel.setRTCRegion(null, 'We want to let Discord decide.'); + */ + +/** + * Sets the user limit of the channel. + * @method setUserLimit + * @memberof StageChannel + * @instance + * @param {number} userLimit The new user limit + * @param {string} [reason] Reason for changing the user limit + * @returns {Promise<StageChannel>} + * @example + * // Set the user limit of a voice channel + * stageChannel.setUserLimit(42) + * .then(channel => console.log(`Set user limit to ${channel.userLimit} for ${channel.name}`)) + * .catch(console.error); + */ + +/** + * Sets the camera video quality mode of the channel. + * @method setVideoQualityMode + * @memberof StageChannel + * @instance + * @param {VideoQualityMode} videoQualityMode The new camera video quality mode. + * @param {string} [reason] Reason for changing the camera video quality mode. + * @returns {Promise<StageChannel>} + */ + +module.exports = StageChannel; diff --git a/node_modules/discord.js/src/structures/StageInstance.js b/node_modules/discord.js/src/structures/StageInstance.js new file mode 100644 index 0000000..97f65df --- /dev/null +++ b/node_modules/discord.js/src/structures/StageInstance.js @@ -0,0 +1,167 @@ +'use strict'; + +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const Base = require('./Base'); + +/** + * Represents a stage instance. + * @extends {Base} + */ +class StageInstance extends Base { + constructor(client, data) { + super(client); + + /** + * The stage instance's id + * @type {Snowflake} + */ + this.id = data.id; + + this._patch(data); + } + + _patch(data) { + if ('guild_id' in data) { + /** + * The id of the guild associated with the stage channel + * @type {Snowflake} + */ + this.guildId = data.guild_id; + } + + if ('channel_id' in data) { + /** + * The id of the channel associated with the stage channel + * @type {Snowflake} + */ + this.channelId = data.channel_id; + } + + if ('topic' in data) { + /** + * The topic of the stage instance + * @type {string} + */ + this.topic = data.topic; + } + + if ('privacy_level' in data) { + /** + * The privacy level of the stage instance + * @type {StageInstancePrivacyLevel} + */ + this.privacyLevel = data.privacy_level; + } + + if ('discoverable_disabled' in data) { + /** + * Whether or not stage discovery is disabled + * @type {?boolean} + * @deprecated See https://github.com/discord/discord-api-docs/pull/4296 for more information + */ + this.discoverableDisabled = data.discoverable_disabled; + } else { + this.discoverableDisabled ??= null; + } + + if ('guild_scheduled_event_id' in data) { + /** + * The associated guild scheduled event id of this stage instance + * @type {?Snowflake} + */ + this.guildScheduledEventId = data.guild_scheduled_event_id; + } else { + this.guildScheduledEventId ??= null; + } + } + + /** + * The stage channel associated with this stage instance + * @type {?StageChannel} + * @readonly + */ + get channel() { + return this.client.channels.resolve(this.channelId); + } + + /** + * The guild this stage instance belongs to + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.resolve(this.guildId); + } + + /** + * The associated guild scheduled event of this stage instance + * @type {?GuildScheduledEvent} + * @readonly + */ + get guildScheduledEvent() { + return this.guild?.scheduledEvents.resolve(this.guildScheduledEventId) ?? null; + } + + /** + * Edits this stage instance. + * @param {StageInstanceEditOptions} options The options to edit the stage instance + * @returns {Promise<StageInstance>} + * @example + * // Edit a stage instance + * stageInstance.edit({ topic: 'new topic' }) + * .then(stageInstance => console.log(stageInstance)) + * .catch(console.error) + */ + edit(options) { + return this.guild.stageInstances.edit(this.channelId, options); + } + + /** + * Deletes this stage instance. + * @returns {Promise<StageInstance>} + * @example + * // Delete a stage instance + * stageInstance.delete() + * .then(stageInstance => console.log(stageInstance)) + * .catch(console.error); + */ + async delete() { + await this.guild.stageInstances.delete(this.channelId); + const clone = this._clone(); + return clone; + } + + /** + * Sets the topic of this stage instance. + * @param {string} topic The topic for the stage instance + * @returns {Promise<StageInstance>} + * @example + * // Set topic of a stage instance + * stageInstance.setTopic('new topic') + * .then(stageInstance => console.log(`Set the topic to: ${stageInstance.topic}`)) + * .catch(console.error); + */ + setTopic(topic) { + return this.guild.stageInstances.edit(this.channelId, { topic }); + } + + /** + * The timestamp this stage instances was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time this stage instance was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } +} + +exports.StageInstance = StageInstance; diff --git a/node_modules/discord.js/src/structures/Sticker.js b/node_modules/discord.js/src/structures/Sticker.js new file mode 100644 index 0000000..b0f2ef6 --- /dev/null +++ b/node_modules/discord.js/src/structures/Sticker.js @@ -0,0 +1,272 @@ +'use strict'; + +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { Routes } = require('discord-api-types/v10'); +const Base = require('./Base'); +const { DiscordjsError, ErrorCodes } = require('../errors'); +const { StickerFormatExtensionMap } = require('../util/Constants'); + +/** + * Represents a Sticker. + * @extends {Base} + */ +class Sticker extends Base { + constructor(client, sticker) { + super(client); + + this._patch(sticker); + } + + _patch(sticker) { + /** + * The sticker's id + * @type {Snowflake} + */ + this.id = sticker.id; + + if ('description' in sticker) { + /** + * The description of the sticker + * @type {?string} + */ + this.description = sticker.description; + } else { + this.description ??= null; + } + + if ('type' in sticker) { + /** + * The type of the sticker + * @type {?StickerType} + */ + this.type = sticker.type; + } else { + this.type ??= null; + } + + if ('format_type' in sticker) { + /** + * The format of the sticker + * @type {StickerFormatType} + */ + this.format = sticker.format_type; + } + + if ('name' in sticker) { + /** + * The name of the sticker + * @type {string} + */ + this.name = sticker.name; + } + + if ('pack_id' in sticker) { + /** + * The id of the pack the sticker is from, for standard stickers + * @type {?Snowflake} + */ + this.packId = sticker.pack_id; + } else { + this.packId ??= null; + } + + if ('tags' in sticker) { + /** + * Autocomplete/suggestions for the sticker + * @type {?string} + */ + this.tags = sticker.tags; + } else { + this.tags ??= null; + } + + if ('available' in sticker) { + /** + * Whether or not the guild sticker is available + * @type {?boolean} + */ + this.available = sticker.available; + } else { + this.available ??= null; + } + + if ('guild_id' in sticker) { + /** + * The id of the guild that owns this sticker + * @type {?Snowflake} + */ + this.guildId = sticker.guild_id; + } else { + this.guildId ??= null; + } + + if ('user' in sticker) { + /** + * The user that uploaded the guild sticker + * @type {?User} + */ + this.user = this.client.users._add(sticker.user); + } else { + this.user ??= null; + } + + if ('sort_value' in sticker) { + /** + * The standard sticker's sort order within its pack + * @type {?number} + */ + this.sortValue = sticker.sort_value; + } else { + this.sortValue ??= null; + } + } + + /** + * The timestamp the sticker was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the sticker was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * Whether this sticker is partial + * @type {boolean} + * @readonly + */ + get partial() { + return !this.type; + } + + /** + * The guild that owns this sticker + * @type {?Guild} + * @readonly + */ + get guild() { + return this.client.guilds.resolve(this.guildId); + } + + /** + * A link to the sticker + * <info>If the sticker's format is {@link StickerFormatType.Lottie}, it returns + * the URL of the Lottie JSON file.</info> + * @type {string} + * @readonly + */ + get url() { + return this.client.rest.cdn.sticker(this.id, StickerFormatExtensionMap[this.format]); + } + + /** + * Fetches this sticker. + * @returns {Promise<Sticker>} + */ + async fetch() { + const data = await this.client.rest.get(Routes.sticker(this.id)); + this._patch(data); + return this; + } + + /** + * Fetches the pack this sticker is part of from Discord, if this is a Nitro sticker. + * @returns {Promise<?StickerPack>} + */ + async fetchPack() { + return (this.packId && (await this.client.fetchPremiumStickerPacks()).get(this.packId)) ?? null; + } + + /** + * Fetches the user who uploaded this sticker, if this is a guild sticker. + * @returns {Promise<?User>} + */ + async fetchUser() { + if (this.partial) await this.fetch(); + if (!this.guildId) throw new DiscordjsError(ErrorCodes.NotGuildSticker); + return this.guild.stickers.fetchUser(this); + } + + /** + * Data for editing a sticker. + * @typedef {Object} GuildStickerEditOptions + * @property {string} [name] The name of the sticker + * @property {?string} [description] The description of the sticker + * @property {string} [tags] The Discord name of a unicode emoji representing the sticker's expression + * @property {string} [reason] Reason for editing this sticker + */ + + /** + * Edits the sticker. + * @param {GuildStickerEditOptions} options The options to provide + * @returns {Promise<Sticker>} + * @example + * // Update the name of a sticker + * sticker.edit({ name: 'new name' }) + * .then(s => console.log(`Updated the name of the sticker to ${s.name}`)) + * .catch(console.error); + */ + edit(options) { + return this.guild.stickers.edit(this, options); + } + + /** + * Deletes the sticker. + * @returns {Promise<Sticker>} + * @param {string} [reason] Reason for deleting this sticker + * @example + * // Delete a message + * sticker.delete() + * .then(s => console.log(`Deleted sticker ${s.name}`)) + * .catch(console.error); + */ + async delete(reason) { + await this.guild.stickers.delete(this, reason); + return this; + } + + /** + * Whether this sticker is the same as another one. + * @param {Sticker|APISticker} other The sticker to compare it to + * @returns {boolean} + */ + equals(other) { + if (other instanceof Sticker) { + return ( + other.id === this.id && + other.description === this.description && + other.type === this.type && + other.format === this.format && + other.name === this.name && + other.packId === this.packId && + other.tags === this.tags && + other.available === this.available && + other.guildId === this.guildId && + other.sortValue === this.sortValue + ); + } else { + return ( + other.id === this.id && + other.description === this.description && + other.name === this.name && + other.tags === this.tags + ); + } + } +} + +exports.Sticker = Sticker; + +/** + * @external APISticker + * @see {@link https://discord.com/developers/docs/resources/sticker#sticker-object} + */ diff --git a/node_modules/discord.js/src/structures/StickerPack.js b/node_modules/discord.js/src/structures/StickerPack.js new file mode 100644 index 0000000..7e599b7 --- /dev/null +++ b/node_modules/discord.js/src/structures/StickerPack.js @@ -0,0 +1,95 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const Base = require('./Base'); +const { Sticker } = require('./Sticker'); + +/** + * Represents a pack of standard stickers. + * @extends {Base} + */ +class StickerPack extends Base { + constructor(client, pack) { + super(client); + /** + * The Sticker pack's id + * @type {Snowflake} + */ + this.id = pack.id; + + /** + * The stickers in the pack + * @type {Collection<Snowflake, Sticker>} + */ + this.stickers = new Collection(pack.stickers.map(s => [s.id, new Sticker(client, s)])); + + /** + * The name of the sticker pack + * @type {string} + */ + this.name = pack.name; + + /** + * The id of the pack's SKU + * @type {Snowflake} + */ + this.skuId = pack.sku_id; + + /** + * The id of a sticker in the pack which is shown as the pack's icon + * @type {?Snowflake} + */ + this.coverStickerId = pack.cover_sticker_id ?? null; + + /** + * The description of the sticker pack + * @type {string} + */ + this.description = pack.description; + + /** + * The id of the sticker pack's banner image + * @type {?Snowflake} + */ + this.bannerId = pack.banner_asset_id ?? null; + } + + /** + * The timestamp the sticker was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the sticker was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The sticker which is shown as the pack's icon + * @type {?Sticker} + * @readonly + */ + get coverSticker() { + return this.coverStickerId && this.stickers.get(this.coverStickerId); + } + + /** + * The URL to this sticker pack's banner. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + bannerURL(options = {}) { + return this.bannerId && this.client.rest.cdn.stickerPackBanner(this.bannerId, options); + } +} + +module.exports = StickerPack; diff --git a/node_modules/discord.js/src/structures/StringSelectMenuBuilder.js b/node_modules/discord.js/src/structures/StringSelectMenuBuilder.js new file mode 100644 index 0000000..ac555e7 --- /dev/null +++ b/node_modules/discord.js/src/structures/StringSelectMenuBuilder.js @@ -0,0 +1,79 @@ +'use strict'; + +const { SelectMenuBuilder: BuildersSelectMenu, normalizeArray } = require('@discordjs/builders'); +const { isJSONEncodable } = require('@discordjs/util'); +const { toSnakeCase } = require('../util/Transformers'); +const { resolvePartialEmoji } = require('../util/Util'); + +/** + * Class used to build select menu components to be sent through the API + * @extends {BuildersSelectMenu} + */ +class StringSelectMenuBuilder extends BuildersSelectMenu { + constructor({ options, ...data } = {}) { + super( + toSnakeCase({ + ...data, + options: options?.map(({ emoji, ...option }) => ({ + ...option, + emoji: emoji && typeof emoji === 'string' ? resolvePartialEmoji(emoji) : emoji, + })), + }), + ); + } + + /** + * Normalizes a select menu option emoji + * @param {SelectMenuOptionData|APISelectMenuOption} selectMenuOption The option to normalize + * @returns {SelectMenuOptionBuilder|APISelectMenuOption} + * @private + */ + static normalizeEmoji(selectMenuOption) { + if (isJSONEncodable(selectMenuOption)) { + return selectMenuOption; + } + + const { emoji, ...option } = selectMenuOption; + return { + ...option, + emoji: typeof emoji === 'string' ? resolvePartialEmoji(emoji) : emoji, + }; + } + + /** + * Adds options to this select menu + * @param {RestOrArray<APISelectMenuOption>} options The options to add to this select menu + * @returns {StringSelectMenuBuilder} + */ + addOptions(...options) { + return super.addOptions(normalizeArray(options).map(option => StringSelectMenuBuilder.normalizeEmoji(option))); + } + + /** + * Sets the options on this select menu + * @param {RestOrArray<APISelectMenuOption>} options The options to set on this select menu + * @returns {StringSelectMenuBuilder} + */ + setOptions(...options) { + return super.setOptions(normalizeArray(options).map(option => StringSelectMenuBuilder.normalizeEmoji(option))); + } + + /** + * Creates a new select menu builder from json data + * @param {StringSelectMenuBuilder|StringSelectMenuComponent|APIStringSelectComponent} other The other data + * @returns {StringSelectMenuBuilder} + */ + static from(other) { + if (isJSONEncodable(other)) { + return new this(other.toJSON()); + } + return new this(other); + } +} + +module.exports = StringSelectMenuBuilder; + +/** + * @external BuildersSelectMenu + * @see {@link https://discord.js.org/docs/packages/builders/stable/StringSelectMenuBuilder:Class} + */ diff --git a/node_modules/discord.js/src/structures/StringSelectMenuComponent.js b/node_modules/discord.js/src/structures/StringSelectMenuComponent.js new file mode 100644 index 0000000..e008ae5 --- /dev/null +++ b/node_modules/discord.js/src/structures/StringSelectMenuComponent.js @@ -0,0 +1,20 @@ +'use strict'; + +const BaseSelectMenuComponent = require('./BaseSelectMenuComponent'); + +/** + * Represents a string select menu component + * @extends {BaseSelectMenuComponent} + */ +class StringSelectMenuComponent extends BaseSelectMenuComponent { + /** + * The options in this select menu + * @type {APISelectMenuOption[]} + * @readonly + */ + get options() { + return this.data.options; + } +} + +module.exports = StringSelectMenuComponent; diff --git a/node_modules/discord.js/src/structures/StringSelectMenuInteraction.js b/node_modules/discord.js/src/structures/StringSelectMenuInteraction.js new file mode 100644 index 0000000..1db8c28 --- /dev/null +++ b/node_modules/discord.js/src/structures/StringSelectMenuInteraction.js @@ -0,0 +1,21 @@ +'use strict'; + +const MessageComponentInteraction = require('./MessageComponentInteraction'); + +/** + * Represents a {@link ComponentType.StringSelect} select menu interaction. + * @extends {MessageComponentInteraction} + */ +class StringSelectMenuInteraction extends MessageComponentInteraction { + constructor(client, data) { + super(client, data); + + /** + * The values selected + * @type {string[]} + */ + this.values = data.data.values ?? []; + } +} + +module.exports = StringSelectMenuInteraction; diff --git a/node_modules/discord.js/src/structures/StringSelectMenuOptionBuilder.js b/node_modules/discord.js/src/structures/StringSelectMenuOptionBuilder.js new file mode 100644 index 0000000..cc85750 --- /dev/null +++ b/node_modules/discord.js/src/structures/StringSelectMenuOptionBuilder.js @@ -0,0 +1,49 @@ +'use strict'; + +const { SelectMenuOptionBuilder: BuildersSelectMenuOption } = require('@discordjs/builders'); +const { isJSONEncodable } = require('@discordjs/util'); +const { toSnakeCase } = require('../util/Transformers'); +const { resolvePartialEmoji } = require('../util/Util'); + +/** + * Represents a select menu option builder. + * @extends {BuildersSelectMenuOption} + */ +class StringSelectMenuOptionBuilder extends BuildersSelectMenuOption { + constructor({ emoji, ...data } = {}) { + super( + toSnakeCase({ + ...data, + emoji: emoji && typeof emoji === 'string' ? resolvePartialEmoji(emoji) : emoji, + }), + ); + } + + /** + * Sets the emoji to display on this option + * @param {ComponentEmojiResolvable} emoji The emoji to display on this option + * @returns {StringSelectMenuOptionBuilder} + */ + setEmoji(emoji) { + if (typeof emoji === 'string') { + return super.setEmoji(resolvePartialEmoji(emoji)); + } + return super.setEmoji(emoji); + } + + /** + * Creates a new select menu option builder from JSON data + * @param {StringSelectMenuOptionBuilder|APISelectMenuOption} other The other data + * @returns {StringSelectMenuOptionBuilder} + */ + static from(other) { + return new this(isJSONEncodable(other) ? other.toJSON() : other); + } +} + +module.exports = StringSelectMenuOptionBuilder; + +/** + * @external BuildersSelectMenuOption + * @see {@link https://discord.js.org/docs/packages/builders/stable/StringSelectMenuOptionBuilder:Class} + */ diff --git a/node_modules/discord.js/src/structures/Team.js b/node_modules/discord.js/src/structures/Team.js new file mode 100644 index 0000000..98eb199 --- /dev/null +++ b/node_modules/discord.js/src/structures/Team.js @@ -0,0 +1,117 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const Base = require('./Base'); +const TeamMember = require('./TeamMember'); + +/** + * Represents a Client OAuth2 Application Team. + * @extends {Base} + */ +class Team extends Base { + constructor(client, data) { + super(client); + this._patch(data); + } + + _patch(data) { + /** + * The Team's id + * @type {Snowflake} + */ + this.id = data.id; + + if ('name' in data) { + /** + * The name of the Team + * @type {string} + */ + this.name = data.name; + } + + if ('icon' in data) { + /** + * The Team's icon hash + * @type {?string} + */ + this.icon = data.icon; + } else { + this.icon ??= null; + } + + if ('owner_user_id' in data) { + /** + * The Team's owner id + * @type {?Snowflake} + */ + this.ownerId = data.owner_user_id; + } else { + this.ownerId ??= null; + } + /** + * The Team's members + * @type {Collection<Snowflake, TeamMember>} + */ + this.members = new Collection(); + + for (const memberData of data.members) { + const member = new TeamMember(this, memberData); + this.members.set(member.id, member); + } + } + + /** + * The owner of this team + * @type {?TeamMember} + * @readonly + */ + get owner() { + return this.members.get(this.ownerId) ?? null; + } + + /** + * The timestamp the team was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the team was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A link to the team's icon. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + iconURL(options = {}) { + return this.icon && this.client.rest.cdn.teamIcon(this.id, this.icon, options); + } + + /** + * When concatenated with a string, this automatically returns the Team's name instead of the + * Team object. + * @returns {string} + * @example + * // Logs: Team name: My Team + * console.log(`Team name: ${team}`); + */ + toString() { + return this.name; + } + + toJSON() { + return super.toJSON({ createdTimestamp: true }); + } +} + +module.exports = Team; diff --git a/node_modules/discord.js/src/structures/TeamMember.js b/node_modules/discord.js/src/structures/TeamMember.js new file mode 100644 index 0000000..9270418 --- /dev/null +++ b/node_modules/discord.js/src/structures/TeamMember.js @@ -0,0 +1,70 @@ +'use strict'; + +const Base = require('./Base'); + +/** + * Represents a Client OAuth2 Application Team Member. + * @extends {Base} + */ +class TeamMember extends Base { + constructor(team, data) { + super(team.client); + + /** + * The Team this member is part of + * @type {Team} + */ + this.team = team; + + this._patch(data); + } + + _patch(data) { + if ('permissions' in data) { + /** + * The permissions this Team Member has with regard to the team + * @type {string[]} + */ + this.permissions = data.permissions; + } + + if ('membership_state' in data) { + /** + * The permissions this Team Member has with regard to the team + * @type {TeamMemberMembershipState} + */ + this.membershipState = data.membership_state; + } + + if ('user' in data) { + /** + * The user for this Team Member + * @type {User} + */ + this.user = this.client.users._add(data.user); + } + } + + /** + * The Team Member's id + * @type {Snowflake} + * @readonly + */ + get id() { + return this.user.id; + } + + /** + * When concatenated with a string, this automatically returns the team member's mention instead of the + * TeamMember object. + * @returns {string} + * @example + * // Logs: Team Member's mention: <@123456789012345678> + * console.log(`Team Member's mention: ${teamMember}`); + */ + toString() { + return this.user.toString(); + } +} + +module.exports = TeamMember; diff --git a/node_modules/discord.js/src/structures/TextChannel.js b/node_modules/discord.js/src/structures/TextChannel.js new file mode 100644 index 0000000..66cc8c4 --- /dev/null +++ b/node_modules/discord.js/src/structures/TextChannel.js @@ -0,0 +1,33 @@ +'use strict'; + +const BaseGuildTextChannel = require('./BaseGuildTextChannel'); + +/** + * Represents a guild text channel on Discord. + * @extends {BaseGuildTextChannel} + */ +class TextChannel extends BaseGuildTextChannel { + _patch(data) { + super._patch(data); + + if ('rate_limit_per_user' in data) { + /** + * The rate limit per user (slowmode) for this channel in seconds + * @type {number} + */ + this.rateLimitPerUser = data.rate_limit_per_user; + } + } + + /** + * Sets the rate limit per user (slowmode) for this channel. + * @param {number} rateLimitPerUser The new rate limit in seconds + * @param {string} [reason] Reason for changing the channel's rate limit + * @returns {Promise<TextChannel>} + */ + setRateLimitPerUser(rateLimitPerUser, reason) { + return this.edit({ rateLimitPerUser, reason }); + } +} + +module.exports = TextChannel; diff --git a/node_modules/discord.js/src/structures/TextInputBuilder.js b/node_modules/discord.js/src/structures/TextInputBuilder.js new file mode 100644 index 0000000..9382154 --- /dev/null +++ b/node_modules/discord.js/src/structures/TextInputBuilder.js @@ -0,0 +1,31 @@ +'use strict'; + +const { TextInputBuilder: BuildersTextInput } = require('@discordjs/builders'); +const { isJSONEncodable } = require('@discordjs/util'); +const { toSnakeCase } = require('../util/Transformers'); + +/** + * Represents a text input builder. + * @extends {BuildersTextInput} + */ +class TextInputBuilder extends BuildersTextInput { + constructor(data) { + super(toSnakeCase(data)); + } + + /** + * Creates a new text input builder from JSON data + * @param {TextInputBuilder|TextInputComponent|APITextInputComponent} other The other data + * @returns {TextInputBuilder} + */ + static from(other) { + return new this(isJSONEncodable(other) ? other.toJSON() : other); + } +} + +module.exports = TextInputBuilder; + +/** + * @external BuildersTextInput + * @see {@link https://discord.js.org/docs/packages/builders/stable/TextInputBuilder:Class} + */ diff --git a/node_modules/discord.js/src/structures/TextInputComponent.js b/node_modules/discord.js/src/structures/TextInputComponent.js new file mode 100644 index 0000000..3cc3115 --- /dev/null +++ b/node_modules/discord.js/src/structures/TextInputComponent.js @@ -0,0 +1,29 @@ +'use strict'; + +const Component = require('./Component'); + +/** + * Represents a text input component. + * @extends {Component} + */ +class TextInputComponent extends Component { + /** + * The custom id of this text input + * @type {string} + * @readonly + */ + get customId() { + return this.data.custom_id; + } + + /** + * The value for this text input + * @type {string} + * @readonly + */ + get value() { + return this.data.value; + } +} + +module.exports = TextInputComponent; diff --git a/node_modules/discord.js/src/structures/ThreadChannel.js b/node_modules/discord.js/src/structures/ThreadChannel.js new file mode 100644 index 0000000..96b4087 --- /dev/null +++ b/node_modules/discord.js/src/structures/ThreadChannel.js @@ -0,0 +1,606 @@ +'use strict'; + +const { ChannelType, PermissionFlagsBits, Routes, ChannelFlags } = require('discord-api-types/v10'); +const { BaseChannel } = require('./BaseChannel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const { DiscordjsRangeError, ErrorCodes } = require('../errors'); +const GuildMessageManager = require('../managers/GuildMessageManager'); +const ThreadMemberManager = require('../managers/ThreadMemberManager'); +const ChannelFlagsBitField = require('../util/ChannelFlagsBitField'); + +/** + * Represents a thread channel on Discord. + * @extends {BaseChannel} + * @implements {TextBasedChannel} + */ +class ThreadChannel extends BaseChannel { + constructor(guild, data, client) { + super(guild?.client ?? client, data, false); + + /** + * The guild the thread is in + * @type {Guild} + */ + this.guild = guild; + + /** + * The id of the guild the channel is in + * @type {Snowflake} + */ + this.guildId = guild?.id ?? data.guild_id; + + /** + * A manager of the messages sent to this thread + * @type {GuildMessageManager} + */ + this.messages = new GuildMessageManager(this); + + /** + * A manager of the members that are part of this thread + * @type {ThreadMemberManager} + */ + this.members = new ThreadMemberManager(this); + if (data) this._patch(data); + } + + _patch(data) { + super._patch(data); + + if ('message' in data) this.messages._add(data.message); + + if ('name' in data) { + /** + * The name of the thread + * @type {string} + */ + this.name = data.name; + } + + if ('guild_id' in data) { + this.guildId = data.guild_id; + } + + if ('parent_id' in data) { + /** + * The id of the parent channel of this thread + * @type {?Snowflake} + */ + this.parentId = data.parent_id; + } else { + this.parentId ??= null; + } + + if ('thread_metadata' in data) { + /** + * Whether the thread is locked + * @type {?boolean} + */ + this.locked = data.thread_metadata.locked ?? false; + + /** + * Whether members without the {@link PermissionFlagsBits.ManageThreads} permission + * can invite other members to this thread. + * <info>This property is always `null` in public threads.</info> + * @type {?boolean} + */ + this.invitable = this.type === ChannelType.PrivateThread ? data.thread_metadata.invitable ?? false : null; + + /** + * Whether the thread is archived + * @type {?boolean} + */ + this.archived = data.thread_metadata.archived; + + /** + * The amount of time (in minutes) after which the thread will automatically archive in case of no recent activity + * @type {?ThreadAutoArchiveDuration} + */ + this.autoArchiveDuration = data.thread_metadata.auto_archive_duration; + + /** + * The timestamp when the thread's archive status was last changed + * <info>If the thread was never archived or unarchived, this is the timestamp at which the thread was + * created</info> + * @type {?number} + */ + this.archiveTimestamp = Date.parse(data.thread_metadata.archive_timestamp); + + if ('create_timestamp' in data.thread_metadata) { + // Note: this is needed because we can't assign directly to getters + this._createdTimestamp = Date.parse(data.thread_metadata.create_timestamp); + } + } else { + this.locked ??= null; + this.archived ??= null; + this.autoArchiveDuration ??= null; + this.archiveTimestamp ??= null; + this.invitable ??= null; + } + + this._createdTimestamp ??= this.type === ChannelType.PrivateThread ? super.createdTimestamp : null; + + if ('owner_id' in data) { + /** + * The id of the member who created this thread + * @type {?Snowflake} + */ + this.ownerId = data.owner_id; + } else { + this.ownerId ??= null; + } + + if ('last_message_id' in data) { + /** + * The last message id sent in this thread, if one was sent + * @type {?Snowflake} + */ + this.lastMessageId = data.last_message_id; + } else { + this.lastMessageId ??= null; + } + + if ('last_pin_timestamp' in data) { + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = data.last_pin_timestamp ? Date.parse(data.last_pin_timestamp) : null; + } else { + this.lastPinTimestamp ??= null; + } + + if ('rate_limit_per_user' in data) { + /** + * The rate limit per user (slowmode) for this thread in seconds + * @type {?number} + */ + this.rateLimitPerUser = data.rate_limit_per_user ?? 0; + } else { + this.rateLimitPerUser ??= null; + } + + if ('message_count' in data) { + /** + * The approximate count of messages in this thread + * <info>Threads created before July 1, 2022 may have an inaccurate count. + * If you need an approximate value higher than that, use `ThreadChannel#messages.cache.size`</info> + * @type {?number} + */ + this.messageCount = data.message_count; + } else { + this.messageCount ??= null; + } + + if ('member_count' in data) { + /** + * The approximate count of users in this thread + * <info>This stops counting at 50. If you need an approximate value higher than that, use + * `ThreadChannel#members.cache.size`</info> + * @type {?number} + */ + this.memberCount = data.member_count; + } else { + this.memberCount ??= null; + } + + if ('total_message_sent' in data) { + /** + * The number of messages ever sent in a thread, similar to {@link ThreadChannel#messageCount} except it + * will not decrement whenever a message is deleted + * @type {?number} + */ + this.totalMessageSent = data.total_message_sent; + } else { + this.totalMessageSent ??= null; + } + + if (data.member && this.client.user) this.members._add({ user_id: this.client.user.id, ...data.member }); + if (data.messages) for (const message of data.messages) this.messages._add(message); + + if ('applied_tags' in data) { + /** + * The tags applied to this thread + * @type {Snowflake[]} + */ + this.appliedTags = data.applied_tags; + } else { + this.appliedTags ??= []; + } + } + + /** + * The timestamp when this thread was created. This isn't available for threads + * created before 2022-01-09 + * @type {?number} + * @readonly + */ + get createdTimestamp() { + return this._createdTimestamp; + } + + /** + * A collection of associated guild member objects of this thread's members + * @type {Collection<Snowflake, GuildMember>} + * @readonly + */ + get guildMembers() { + return this.members.cache.mapValues(member => member.guildMember); + } + + /** + * The time at which this thread's archive status was last changed + * <info>If the thread was never archived or unarchived, this is the time at which the thread was created</info> + * @type {?Date} + * @readonly + */ + get archivedAt() { + return this.archiveTimestamp && new Date(this.archiveTimestamp); + } + + /** + * The time the thread was created at + * @type {?Date} + * @readonly + */ + get createdAt() { + return this.createdTimestamp && new Date(this.createdTimestamp); + } + + /** + * The parent channel of this thread + * @type {?(NewsChannel|TextChannel|ForumChannel)} + * @readonly + */ + get parent() { + return this.guild.channels.resolve(this.parentId); + } + + /** + * Makes the client user join the thread. + * @returns {Promise<ThreadChannel>} + */ + async join() { + await this.members.add('@me'); + return this; + } + + /** + * Makes the client user leave the thread. + * @returns {Promise<ThreadChannel>} + */ + async leave() { + await this.members.remove('@me'); + return this; + } + + /** + * Gets the overall set of permissions for a member or role in this thread's parent channel, taking overwrites into + * account. + * @param {GuildMemberResolvable|RoleResolvable} memberOrRole The member or role to obtain the overall permissions for + * @param {boolean} [checkAdmin=true] Whether having the {@link PermissionFlagsBits.Administrator} permission + * will return all permissions + * @returns {?Readonly<PermissionsBitField>} + */ + permissionsFor(memberOrRole, checkAdmin) { + return this.parent?.permissionsFor(memberOrRole, checkAdmin) ?? null; + } + + /** + * Fetches the owner of this thread. If the thread member object isn't needed, + * use {@link ThreadChannel#ownerId} instead. + * @param {BaseFetchOptions} [options] The options for fetching the member + * @returns {Promise<?ThreadMember>} + */ + async fetchOwner({ cache = true, force = false } = {}) { + if (!force) { + const existing = this.members.cache.get(this.ownerId); + if (existing) return existing; + } + + // We cannot fetch a single thread member, as of this commit's date, Discord API responds with 405 + const members = await this.members.fetch({ cache }); + return members.get(this.ownerId) ?? null; + } + + /** + * Fetches the message that started this thread, if any. + * <info>The `Promise` will reject if the original message in a forum post is deleted + * or when the original message in the parent channel is deleted. + * If you just need the id of that message, use {@link ThreadChannel#id} instead.</info> + * @param {BaseFetchOptions} [options] Additional options for this fetch + * @returns {Promise<Message<true>|null>} + */ + // eslint-disable-next-line require-await + async fetchStarterMessage(options) { + const channel = this.parent?.type === ChannelType.GuildForum ? this : this.parent; + return channel?.messages.fetch({ message: this.id, ...options }) ?? null; + } + + /** + * The options used to edit a thread channel + * @typedef {Object} ThreadEditOptions + * @property {string} [name] The new name for the thread + * @property {boolean} [archived] Whether the thread is archived + * @property {ThreadAutoArchiveDuration} [autoArchiveDuration] The amount of time after which the thread + * should automatically archive in case of no recent activity + * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the thread in seconds + * @property {boolean} [locked] Whether the thread is locked + * @property {boolean} [invitable] Whether non-moderators can add other non-moderators to a thread + * <info>Can only be edited on {@link ChannelType.PrivateThread}</info> + * @property {Snowflake[]} [appliedTags] The tags to apply to the thread + * @property {ChannelFlagsResolvable} [flags] The flags to set on the channel + * @property {string} [reason] Reason for editing the thread + */ + + /** + * Edits this thread. + * @param {ThreadEditOptions} options The options to provide + * @returns {Promise<ThreadChannel>} + * @example + * // Edit a thread + * thread.edit({ name: 'new-thread' }) + * .then(editedThread => console.log(editedThread)) + * .catch(console.error); + */ + async edit(options) { + const newData = await this.client.rest.patch(Routes.channel(this.id), { + body: { + name: (options.name ?? this.name).trim(), + archived: options.archived, + auto_archive_duration: options.autoArchiveDuration, + rate_limit_per_user: options.rateLimitPerUser, + locked: options.locked, + invitable: this.type === ChannelType.PrivateThread ? options.invitable : undefined, + applied_tags: options.appliedTags, + flags: 'flags' in options ? ChannelFlagsBitField.resolve(options.flags) : undefined, + }, + reason: options.reason, + }); + + return this.client.actions.ChannelUpdate.handle(newData).updated; + } + + /** + * Sets whether the thread is archived. + * @param {boolean} [archived=true] Whether the thread is archived + * @param {string} [reason] Reason for archiving or unarchiving + * @returns {Promise<ThreadChannel>} + * @example + * // Archive the thread + * thread.setArchived(true) + * .then(newThread => console.log(`Thread is now ${newThread.archived ? 'archived' : 'active'}`)) + * .catch(console.error); + */ + setArchived(archived = true, reason) { + return this.edit({ archived, reason }); + } + + /** + * Sets the duration after which the thread will automatically archive in case of no recent activity. + * @param {ThreadAutoArchiveDuration} autoArchiveDuration The amount of time after which the thread + * should automatically archive in case of no recent activity + * @param {string} [reason] Reason for changing the auto archive duration + * @returns {Promise<ThreadChannel>} + * @example + * // Set the thread's auto archive time to 1 hour + * thread.setAutoArchiveDuration(ThreadAutoArchiveDuration.OneHour) + * .then(newThread => { + * console.log(`Thread will now archive after ${newThread.autoArchiveDuration} minutes of inactivity`); + * }); + * .catch(console.error); + */ + setAutoArchiveDuration(autoArchiveDuration, reason) { + return this.edit({ autoArchiveDuration, reason }); + } + + /** + * Sets whether members without the {@link PermissionFlagsBits.ManageThreads} permission + * can invite other members to this thread. + * @param {boolean} [invitable=true] Whether non-moderators can invite non-moderators to this thread + * @param {string} [reason] Reason for changing invite + * @returns {Promise<ThreadChannel>} + */ + setInvitable(invitable = true, reason) { + if (this.type !== ChannelType.PrivateThread) { + return Promise.reject(new DiscordjsRangeError(ErrorCodes.ThreadInvitableType, this.type)); + } + return this.edit({ invitable, reason }); + } + + /** + * Sets whether the thread can be **unarchived** by anyone with the + * {@link PermissionFlagsBits.SendMessages} permission. When a thread is locked, only members with the + * {@link PermissionFlagsBits.ManageThreads} permission can unarchive it. + * @param {boolean} [locked=true] Whether the thread is locked + * @param {string} [reason] Reason for locking or unlocking the thread + * @returns {Promise<ThreadChannel>} + * @example + * // Set the thread to locked + * thread.setLocked(true) + * .then(newThread => console.log(`Thread is now ${newThread.locked ? 'locked' : 'unlocked'}`)) + * .catch(console.error); + */ + setLocked(locked = true, reason) { + return this.edit({ locked, reason }); + } + + /** + * Sets a new name for this thread. + * @param {string} name The new name for the thread + * @param {string} [reason] Reason for changing the thread's name + * @returns {Promise<ThreadChannel>} + * @example + * // Change the thread's name + * thread.setName('not_general') + * .then(newThread => console.log(`Thread's new name is ${newThread.name}`)) + * .catch(console.error); + */ + setName(name, reason) { + return this.edit({ name, reason }); + } + + /** + * Sets the rate limit per user (slowmode) for this thread. + * @param {number} rateLimitPerUser The new rate limit in seconds + * @param {string} [reason] Reason for changing the thread's rate limit + * @returns {Promise<ThreadChannel>} + */ + setRateLimitPerUser(rateLimitPerUser, reason) { + return this.edit({ rateLimitPerUser, reason }); + } + + /** + * Set the applied tags for this channel (only applicable to forum threads) + * @param {Snowflake[]} appliedTags The tags to set for this channel + * @param {string} [reason] Reason for changing the thread's applied tags + * @returns {Promise<ThreadChannel>} + */ + setAppliedTags(appliedTags, reason) { + return this.edit({ appliedTags, reason }); + } + + /** + * Pins this thread from the forum channel (only applicable to forum threads). + * @param {string} [reason] Reason for pinning + * @returns {Promise<ThreadChannel>} + */ + pin(reason) { + return this.edit({ flags: this.flags.add(ChannelFlags.Pinned), reason }); + } + + /** + * Unpins this thread from the forum channel (only applicable to forum threads). + * @param {string} [reason] Reason for unpinning + * @returns {Promise<ThreadChannel>} + */ + unpin(reason) { + return this.edit({ flags: this.flags.remove(ChannelFlags.Pinned), reason }); + } + + /** + * Whether the client user is a member of the thread. + * @type {boolean} + * @readonly + */ + get joined() { + return this.members.cache.has(this.client.user?.id); + } + + /** + * Whether the thread is editable by the client user (name, archived, autoArchiveDuration) + * @type {boolean} + * @readonly + */ + get editable() { + return ( + (this.ownerId === this.client.user.id && (this.type !== ChannelType.PrivateThread || this.joined)) || + this.manageable + ); + } + + /** + * Whether the thread is joinable by the client user + * @type {boolean} + * @readonly + */ + get joinable() { + return ( + !this.archived && + !this.joined && + this.permissionsFor(this.client.user)?.has( + this.type === ChannelType.PrivateThread ? PermissionFlagsBits.ManageThreads : PermissionFlagsBits.ViewChannel, + false, + ) + ); + } + + /** + * Whether the thread is manageable by the client user, for deleting or editing rateLimitPerUser or locked. + * @type {boolean} + * @readonly + */ + get manageable() { + const permissions = this.permissionsFor(this.client.user); + if (!permissions) return false; + // This flag allows managing even if timed out + if (permissions.has(PermissionFlagsBits.Administrator, false)) return true; + + return ( + this.guild.members.me.communicationDisabledUntilTimestamp < Date.now() && + permissions.has(PermissionFlagsBits.ManageThreads, false) + ); + } + + /** + * Whether the thread is viewable by the client user + * @type {boolean} + * @readonly + */ + get viewable() { + if (this.client.user.id === this.guild.ownerId) return true; + const permissions = this.permissionsFor(this.client.user); + if (!permissions) return false; + return permissions.has(PermissionFlagsBits.ViewChannel, false); + } + + /** + * Whether the client user can send messages in this thread + * @type {boolean} + * @readonly + */ + get sendable() { + const permissions = this.permissionsFor(this.client.user); + if (!permissions) return false; + // This flag allows sending even if timed out + if (permissions.has(PermissionFlagsBits.Administrator, false)) return true; + + return ( + !(this.archived && this.locked && !this.manageable) && + (this.type !== ChannelType.PrivateThread || this.joined || this.manageable) && + permissions.has(PermissionFlagsBits.SendMessagesInThreads, false) && + this.guild.members.me.communicationDisabledUntilTimestamp < Date.now() + ); + } + + /** + * Whether the thread is unarchivable by the client user + * @type {boolean} + * @readonly + */ + get unarchivable() { + return this.archived && this.sendable && (!this.locked || this.manageable); + } + + /** + * Deletes this thread. + * @param {string} [reason] Reason for deleting this thread + * @returns {Promise<ThreadChannel>} + * @example + * // Delete the thread + * thread.delete('cleaning out old threads') + * .then(deletedThread => console.log(deletedThread)) + * .catch(console.error); + */ + async delete(reason) { + await this.guild.channels.delete(this.id, reason); + return this; + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + get lastMessage() {} + get lastPinAt() {} + send() {} + sendTyping() {} + createMessageCollector() {} + awaitMessages() {} + createMessageComponentCollector() {} + awaitMessageComponent() {} + bulkDelete() {} + // Doesn't work on Thread channels; setRateLimitPerUser() {} + // Doesn't work on Thread channels; setNSFW() {} +} + +TextBasedChannel.applyToClass(ThreadChannel, true, ['fetchWebhooks', 'setRateLimitPerUser', 'setNSFW']); + +module.exports = ThreadChannel; diff --git a/node_modules/discord.js/src/structures/ThreadMember.js b/node_modules/discord.js/src/structures/ThreadMember.js new file mode 100644 index 0000000..fc79dd0 --- /dev/null +++ b/node_modules/discord.js/src/structures/ThreadMember.js @@ -0,0 +1,113 @@ +'use strict'; + +const Base = require('./Base'); +const ThreadMemberFlagsBitField = require('../util/ThreadMemberFlagsBitField'); + +/** + * Represents a Member for a Thread. + * @extends {Base} + */ +class ThreadMember extends Base { + constructor(thread, data, extra = {}) { + super(thread.client); + + /** + * The thread that this member is a part of + * @type {ThreadChannel} + */ + this.thread = thread; + + /** + * The timestamp the member last joined the thread at + * @type {?number} + */ + this.joinedTimestamp = null; + + /** + * The flags for this thread member. This will be `null` if partial. + * @type {?ThreadMemberFlagsBitField} + */ + this.flags = null; + + /** + * The id of the thread member + * @type {Snowflake} + */ + this.id = data.user_id; + + this._patch(data, extra); + } + + _patch(data, extra = {}) { + if ('join_timestamp' in data) this.joinedTimestamp = Date.parse(data.join_timestamp); + if ('flags' in data) this.flags = new ThreadMemberFlagsBitField(data.flags).freeze(); + + if ('member' in data) { + /** + * The guild member associated with this thread member. + * @type {?GuildMember} + * @private + */ + this.member = this.thread.guild.members._add(data.member, extra.cache); + } else { + this.member ??= null; + } + } + + /** + * Whether this thread member is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return this.flags === null; + } + + /** + * The guild member associated with this thread member + * @type {?GuildMember} + * @readonly + */ + get guildMember() { + return this.member ?? this.thread.guild.members.resolve(this.id); + } + + /** + * The last time this member joined the thread + * @type {?Date} + * @readonly + */ + get joinedAt() { + return this.joinedTimestamp && new Date(this.joinedTimestamp); + } + + /** + * The user associated with this thread member + * @type {?User} + * @readonly + */ + get user() { + return this.client.users.resolve(this.id); + } + + /** + * Whether the client user can manage this thread member + * @type {boolean} + * @readonly + */ + get manageable() { + return !this.thread.archived && this.thread.editable; + } + + /** + * Removes this member from the thread. + * @param {string} [reason] Reason for removing the member + * @returns {ThreadMember} + */ + async remove(reason) { + await this.thread.members.remove(this.id, reason); + return this; + } +} + +module.exports = ThreadMember; diff --git a/node_modules/discord.js/src/structures/Typing.js b/node_modules/discord.js/src/structures/Typing.js new file mode 100644 index 0000000..341d7ca --- /dev/null +++ b/node_modules/discord.js/src/structures/Typing.js @@ -0,0 +1,74 @@ +'use strict'; + +const Base = require('./Base'); + +/** + * Represents a typing state for a user in a channel. + * @extends {Base} + */ +class Typing extends Base { + constructor(channel, user, data) { + super(channel.client); + + /** + * The channel the status is from + * @type {TextBasedChannels} + */ + this.channel = channel; + + /** + * The user who is typing + * @type {User} + */ + this.user = user; + + this._patch(data); + } + + _patch(data) { + if ('timestamp' in data) { + /** + * The UNIX timestamp in milliseconds the user started typing at + * @type {number} + */ + this.startedTimestamp = data.timestamp * 1_000; + } + } + + /** + * Indicates whether the status is received from a guild. + * @returns {boolean} + */ + inGuild() { + return this.guild !== null; + } + + /** + * The time the user started typing at + * @type {Date} + * @readonly + */ + get startedAt() { + return new Date(this.startedTimestamp); + } + + /** + * The guild the status is from + * @type {?Guild} + * @readonly + */ + get guild() { + return this.channel.guild ?? null; + } + + /** + * The member who is typing + * @type {?GuildMember} + * @readonly + */ + get member() { + return this.guild?.members.resolve(this.user) ?? null; + } +} + +module.exports = Typing; diff --git a/node_modules/discord.js/src/structures/User.js b/node_modules/discord.js/src/structures/User.js new file mode 100644 index 0000000..4e38d2d --- /dev/null +++ b/node_modules/discord.js/src/structures/User.js @@ -0,0 +1,380 @@ +'use strict'; + +const { userMention } = require('@discordjs/builders'); +const { calculateUserDefaultAvatarIndex } = require('@discordjs/rest'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const Base = require('./Base'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const UserFlagsBitField = require('../util/UserFlagsBitField'); + +/** + * Represents a user on Discord. + * @implements {TextBasedChannel} + * @extends {Base} + */ +class User extends Base { + constructor(client, data) { + super(client); + + /** + * The user's id + * @type {Snowflake} + */ + this.id = data.id; + + this.bot = null; + + this.system = null; + + this.flags = null; + + this._patch(data); + } + + _patch(data) { + if ('username' in data) { + /** + * The username of the user + * @type {?string} + */ + this.username = data.username; + } else { + this.username ??= null; + } + + if ('global_name' in data) { + /** + * The global name of this user + * @type {?string} + */ + this.globalName = data.global_name; + } else { + this.globalName ??= null; + } + + if ('bot' in data) { + /** + * Whether or not the user is a bot + * @type {?boolean} + */ + this.bot = Boolean(data.bot); + } else if (!this.partial && typeof this.bot !== 'boolean') { + this.bot = false; + } + + if ('discriminator' in data) { + /** + * The discriminator of this user + * <info>`'0'`, or a 4-digit stringified number if they're using the legacy username system</info> + * @type {?string} + */ + this.discriminator = data.discriminator; + } else { + this.discriminator ??= null; + } + + if ('avatar' in data) { + /** + * The user avatar's hash + * @type {?string} + */ + this.avatar = data.avatar; + } else { + this.avatar ??= null; + } + + if ('banner' in data) { + /** + * The user banner's hash + * <info>The user must be force fetched for this property to be present or be updated</info> + * @type {?string} + */ + this.banner = data.banner; + } else if (this.banner !== null) { + this.banner ??= undefined; + } + + if ('accent_color' in data) { + /** + * The base 10 accent color of the user's banner + * <info>The user must be force fetched for this property to be present or be updated</info> + * @type {?number} + */ + this.accentColor = data.accent_color; + } else if (this.accentColor !== null) { + this.accentColor ??= undefined; + } + + if ('system' in data) { + /** + * Whether the user is an Official Discord System user (part of the urgent message system) + * @type {?boolean} + */ + this.system = Boolean(data.system); + } else if (!this.partial && typeof this.system !== 'boolean') { + this.system = false; + } + + if ('public_flags' in data) { + /** + * The flags for this user + * @type {?UserFlagsBitField} + */ + this.flags = new UserFlagsBitField(data.public_flags); + } + + if ('avatar_decoration' in data) { + /** + * The user avatar decoration's hash + * @type {?string} + */ + this.avatarDecoration = data.avatar_decoration; + } else { + this.avatarDecoration ??= null; + } + } + + /** + * Whether this User is a partial + * @type {boolean} + * @readonly + */ + get partial() { + return typeof this.username !== 'string'; + } + + /** + * The timestamp the user was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the user was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A link to the user's avatar. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + avatarURL(options = {}) { + return this.avatar && this.client.rest.cdn.avatar(this.id, this.avatar, options); + } + + /** + * A link to the user's avatar decoration. + * @param {BaseImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + avatarDecorationURL(options = {}) { + return this.avatarDecoration && this.client.rest.cdn.avatarDecoration(this.id, this.avatarDecoration, options); + } + + /** + * A link to the user's default avatar + * @type {string} + * @readonly + */ + get defaultAvatarURL() { + const index = this.discriminator === '0' ? calculateUserDefaultAvatarIndex(this.id) : this.discriminator % 5; + return this.client.rest.cdn.defaultAvatar(index); + } + + /** + * A link to the user's avatar if they have one. + * Otherwise a link to their default avatar will be returned. + * @param {ImageURLOptions} [options={}] Options for the Image URL + * @returns {string} + */ + displayAvatarURL(options) { + return this.avatarURL(options) ?? this.defaultAvatarURL; + } + + /** + * The hexadecimal version of the user accent color, with a leading hash + * <info>The user must be force fetched for this property to be present</info> + * @type {?string} + * @readonly + */ + get hexAccentColor() { + if (typeof this.accentColor !== 'number') return this.accentColor; + return `#${this.accentColor.toString(16).padStart(6, '0')}`; + } + + /** + * A link to the user's banner. See {@link User#banner} for more info + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + bannerURL(options = {}) { + return this.banner && this.client.rest.cdn.banner(this.id, this.banner, options); + } + + /** + * The tag of this user + * <info>This user's username, or their legacy tag (e.g. `hydrabolt#0001`) + * if they're using the legacy username system</info> + * @type {?string} + * @readonly + */ + get tag() { + return typeof this.username === 'string' + ? this.discriminator === '0' + ? this.username + : `${this.username}#${this.discriminator}` + : null; + } + + /** + * The global name of this user, or their username if they don't have one + * @type {?string} + * @readonly + */ + get displayName() { + return this.globalName ?? this.username; + } + + /** + * The DM between the client's user and this user + * @type {?DMChannel} + * @readonly + */ + get dmChannel() { + return this.client.users.dmChannel(this.id); + } + + /** + * Creates a DM channel between the client and the user. + * @param {boolean} [force=false] Whether to skip the cache check and request the API + * @returns {Promise<DMChannel>} + */ + createDM(force = false) { + return this.client.users.createDM(this.id, { force }); + } + + /** + * Deletes a DM channel (if one exists) between the client and the user. Resolves with the channel if successful. + * @returns {Promise<DMChannel>} + */ + deleteDM() { + return this.client.users.deleteDM(this.id); + } + + /** + * Checks if the user is equal to another. + * It compares id, username, discriminator, avatar, banner, accent color, and bot flags. + * It is recommended to compare equality by using `user.id === user2.id` unless you want to compare all properties. + * @param {User} user User to compare with + * @returns {boolean} + */ + equals(user) { + return ( + user && + this.id === user.id && + this.username === user.username && + this.discriminator === user.discriminator && + this.globalName === user.globalName && + this.avatar === user.avatar && + this.flags?.bitfield === user.flags?.bitfield && + this.banner === user.banner && + this.accentColor === user.accentColor + ); + } + + /** + * Compares the user with an API user object + * @param {APIUser} user The API user object to compare + * @returns {boolean} + * @private + */ + _equals(user) { + return ( + user && + this.id === user.id && + this.username === user.username && + this.discriminator === user.discriminator && + this.globalName === user.global_name && + this.avatar === user.avatar && + this.flags?.bitfield === user.public_flags && + ('banner' in user ? this.banner === user.banner : true) && + ('accent_color' in user ? this.accentColor === user.accent_color : true) + ); + } + + /** + * Fetches this user's flags. + * @param {boolean} [force=false] Whether to skip the cache check and request the API + * @returns {Promise<UserFlagsBitField>} + */ + fetchFlags(force = false) { + return this.client.users.fetchFlags(this.id, { force }); + } + + /** + * Fetches this user. + * @param {boolean} [force=true] Whether to skip the cache check and request the API + * @returns {Promise<User>} + */ + fetch(force = true) { + return this.client.users.fetch(this.id, { force }); + } + + /** + * When concatenated with a string, this automatically returns the user's mention instead of the User object. + * @returns {string} + * @example + * // Logs: Hello from <@123456789012345678>! + * console.log(`Hello from ${user}!`); + */ + toString() { + return userMention(this.id); + } + + toJSON(...props) { + const json = super.toJSON( + { + createdTimestamp: true, + defaultAvatarURL: true, + hexAccentColor: true, + tag: true, + }, + ...props, + ); + json.avatarURL = this.avatarURL(); + json.displayAvatarURL = this.displayAvatarURL(); + json.bannerURL = this.banner ? this.bannerURL() : this.banner; + return json; + } +} + +/** + * Sends a message to this user. + * @method send + * @memberof User + * @instance + * @param {string|MessagePayload|MessageCreateOptions} options The options to provide + * @returns {Promise<Message>} + * @example + * // Send a direct message + * user.send('Hello!') + * .then(message => console.log(`Sent message: ${message.content} to ${user.tag}`)) + * .catch(console.error); + */ + +TextBasedChannel.applyToClass(User); + +module.exports = User; + +/** + * @external APIUser + * @see {@link https://discord.com/developers/docs/resources/user#user-object} + */ diff --git a/node_modules/discord.js/src/structures/UserContextMenuCommandInteraction.js b/node_modules/discord.js/src/structures/UserContextMenuCommandInteraction.js new file mode 100644 index 0000000..2e9dc7c --- /dev/null +++ b/node_modules/discord.js/src/structures/UserContextMenuCommandInteraction.js @@ -0,0 +1,29 @@ +'use strict'; + +const ContextMenuCommandInteraction = require('./ContextMenuCommandInteraction'); + +/** + * Represents a user context menu interaction. + * @extends {ContextMenuCommandInteraction} + */ +class UserContextMenuCommandInteraction extends ContextMenuCommandInteraction { + /** + * The target user from this interaction + * @type {User} + * @readonly + */ + get targetUser() { + return this.options.getUser('user'); + } + + /** + * The target member from this interaction + * @type {?(GuildMember|APIGuildMember)} + * @readonly + */ + get targetMember() { + return this.options.getMember('user'); + } +} + +module.exports = UserContextMenuCommandInteraction; diff --git a/node_modules/discord.js/src/structures/UserSelectMenuBuilder.js b/node_modules/discord.js/src/structures/UserSelectMenuBuilder.js new file mode 100644 index 0000000..61bdbb8 --- /dev/null +++ b/node_modules/discord.js/src/structures/UserSelectMenuBuilder.js @@ -0,0 +1,31 @@ +'use strict'; + +const { UserSelectMenuBuilder: BuildersUserSelectMenu } = require('@discordjs/builders'); +const { isJSONEncodable } = require('@discordjs/util'); +const { toSnakeCase } = require('../util/Transformers'); + +/** + * Class used to build select menu components to be sent through the API + * @extends {BuildersUserSelectMenu} + */ +class UserSelectMenuBuilder extends BuildersUserSelectMenu { + constructor(data = {}) { + super(toSnakeCase(data)); + } + + /** + * Creates a new select menu builder from JSON data + * @param {UserSelectMenuBuilder|UserSelectMenuComponent|APIUserSelectComponent} other The other data + * @returns {UserSelectMenuBuilder} + */ + static from(other) { + return new this(isJSONEncodable(other) ? other.toJSON() : other); + } +} + +module.exports = UserSelectMenuBuilder; + +/** + * @external BuildersUserSelectMenu + * @see {@link https://discord.js.org/docs/packages/builders/stable/UserSelectMenuBuilder:Class} + */ diff --git a/node_modules/discord.js/src/structures/UserSelectMenuComponent.js b/node_modules/discord.js/src/structures/UserSelectMenuComponent.js new file mode 100644 index 0000000..0acacdf --- /dev/null +++ b/node_modules/discord.js/src/structures/UserSelectMenuComponent.js @@ -0,0 +1,11 @@ +'use strict'; + +const BaseSelectMenuComponent = require('./BaseSelectMenuComponent'); + +/** + * Represents a user select menu component + * @extends {BaseSelectMenuComponent} + */ +class UserSelectMenuComponent extends BaseSelectMenuComponent {} + +module.exports = UserSelectMenuComponent; diff --git a/node_modules/discord.js/src/structures/UserSelectMenuInteraction.js b/node_modules/discord.js/src/structures/UserSelectMenuInteraction.js new file mode 100644 index 0000000..5e23239 --- /dev/null +++ b/node_modules/discord.js/src/structures/UserSelectMenuInteraction.js @@ -0,0 +1,51 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const MessageComponentInteraction = require('./MessageComponentInteraction'); +const Events = require('../util/Events'); + +/** + * Represents a {@link ComponentType.UserSelect} select menu interaction. + * @extends {MessageComponentInteraction} + */ +class UserSelectMenuInteraction extends MessageComponentInteraction { + constructor(client, data) { + super(client, data); + const { resolved, values } = data.data; + + /** + * An array of the selected user ids + * @type {Snowflake[]} + */ + this.values = values ?? []; + + /** + * Collection of the selected users + * @type {Collection<Snowflake, User>} + */ + this.users = new Collection(); + + /** + * Collection of the selected members + * @type {Collection<Snowflake, GuildMember|APIGuildMember>} + */ + this.members = new Collection(); + + for (const user of Object.values(resolved?.users ?? {})) { + this.users.set(user.id, this.client.users._add(user)); + } + + for (const [id, member] of Object.entries(resolved?.members ?? {})) { + const user = resolved.users[id]; + + if (!user) { + this.client.emit(Events.Debug, `[UserSelectMenuInteraction] Received a member without a user, skipping ${id}`); + continue; + } + + this.members.set(id, this.guild?.members._add({ user, ...member }) ?? { user, ...member }); + } + } +} + +module.exports = UserSelectMenuInteraction; diff --git a/node_modules/discord.js/src/structures/VoiceChannel.js b/node_modules/discord.js/src/structures/VoiceChannel.js new file mode 100644 index 0000000..d4f33ca --- /dev/null +++ b/node_modules/discord.js/src/structures/VoiceChannel.js @@ -0,0 +1,96 @@ +'use strict'; + +const { PermissionFlagsBits } = require('discord-api-types/v10'); +const BaseGuildVoiceChannel = require('./BaseGuildVoiceChannel'); + +/** + * Represents a guild voice channel on Discord. + * @extends {BaseGuildVoiceChannel} + */ +class VoiceChannel extends BaseGuildVoiceChannel { + /** + * Whether the channel is joinable by the client user + * @type {boolean} + * @readonly + */ + get joinable() { + if (!super.joinable) return false; + if (this.full && !this.permissionsFor(this.client.user).has(PermissionFlagsBits.MoveMembers, false)) return false; + return true; + } + + /** + * Checks if the client has permission to send audio to the voice channel + * @type {boolean} + * @readonly + */ + get speakable() { + const permissions = this.permissionsFor(this.client.user); + if (!permissions) return false; + // This flag allows speaking even if timed out + if (permissions.has(PermissionFlagsBits.Administrator, false)) return true; + + return ( + this.guild.members.me.communicationDisabledUntilTimestamp < Date.now() && + permissions.has(PermissionFlagsBits.Speak, false) + ); + } +} + +/** + * Sets the bitrate of the channel. + * @method setBitrate + * @memberof VoiceChannel + * @instance + * @param {number} bitrate The new bitrate + * @param {string} [reason] Reason for changing the channel's bitrate + * @returns {Promise<VoiceChannel>} + * @example + * // Set the bitrate of a voice channel + * voiceChannel.setBitrate(48_000) + * .then(channel => console.log(`Set bitrate to ${channel.bitrate}bps for ${channel.name}`)) + * .catch(console.error); + */ + +/** + * Sets the RTC region of the channel. + * @method setRTCRegion + * @memberof VoiceChannel + * @instance + * @param {?string} rtcRegion The new region of the channel. Set to `null` to remove a specific region for the channel + * @param {string} [reason] The reason for modifying this region. + * @returns {Promise<VoiceChannel>} + * @example + * // Set the RTC region to sydney + * voiceChannel.setRTCRegion('sydney'); + * @example + * // Remove a fixed region for this channel - let Discord decide automatically + * voiceChannel.setRTCRegion(null, 'We want to let Discord decide.'); + */ + +/** + * Sets the user limit of the channel. + * @method setUserLimit + * @memberof VoiceChannel + * @instance + * @param {number} userLimit The new user limit + * @param {string} [reason] Reason for changing the user limit + * @returns {Promise<VoiceChannel>} + * @example + * // Set the user limit of a voice channel + * voiceChannel.setUserLimit(42) + * .then(channel => console.log(`Set user limit to ${channel.userLimit} for ${channel.name}`)) + * .catch(console.error); + */ + +/** + * Sets the camera video quality mode of the channel. + * @method setVideoQualityMode + * @memberof VoiceChannel + * @instance + * @param {VideoQualityMode} videoQualityMode The new camera video quality mode. + * @param {string} [reason] Reason for changing the camera video quality mode. + * @returns {Promise<VoiceChannel>} + */ + +module.exports = VoiceChannel; diff --git a/node_modules/discord.js/src/structures/VoiceRegion.js b/node_modules/discord.js/src/structures/VoiceRegion.js new file mode 100644 index 0000000..1f5652a --- /dev/null +++ b/node_modules/discord.js/src/structures/VoiceRegion.js @@ -0,0 +1,46 @@ +'use strict'; + +const { flatten } = require('../util/Util'); + +/** + * Represents a Discord voice region for guilds. + */ +class VoiceRegion { + constructor(data) { + /** + * The region's id + * @type {string} + */ + this.id = data.id; + + /** + * Name of the region + * @type {string} + */ + this.name = data.name; + + /** + * Whether the region is deprecated + * @type {boolean} + */ + this.deprecated = data.deprecated; + + /** + * Whether the region is optimal + * @type {boolean} + */ + this.optimal = data.optimal; + + /** + * Whether the region is custom + * @type {boolean} + */ + this.custom = data.custom; + } + + toJSON() { + return flatten(this); + } +} + +module.exports = VoiceRegion; diff --git a/node_modules/discord.js/src/structures/VoiceState.js b/node_modules/discord.js/src/structures/VoiceState.js new file mode 100644 index 0000000..ae510f2 --- /dev/null +++ b/node_modules/discord.js/src/structures/VoiceState.js @@ -0,0 +1,303 @@ +'use strict'; + +const { ChannelType, Routes } = require('discord-api-types/v10'); +const Base = require('./Base'); +const { DiscordjsError, DiscordjsTypeError, ErrorCodes } = require('../errors'); + +/** + * Represents the voice state for a Guild Member. + * @extends {Base} + */ +class VoiceState extends Base { + constructor(guild, data) { + super(guild.client); + /** + * The guild of this voice state + * @type {Guild} + */ + this.guild = guild; + /** + * The id of the member of this voice state + * @type {Snowflake} + */ + this.id = data.user_id; + this._patch(data); + } + + _patch(data) { + if ('deaf' in data) { + /** + * Whether this member is deafened server-wide + * @type {?boolean} + */ + this.serverDeaf = data.deaf; + } else { + this.serverDeaf ??= null; + } + + if ('mute' in data) { + /** + * Whether this member is muted server-wide + * @type {?boolean} + */ + this.serverMute = data.mute; + } else { + this.serverMute ??= null; + } + + if ('self_deaf' in data) { + /** + * Whether this member is self-deafened + * @type {?boolean} + */ + this.selfDeaf = data.self_deaf; + } else { + this.selfDeaf ??= null; + } + + if ('self_mute' in data) { + /** + * Whether this member is self-muted + * @type {?boolean} + */ + this.selfMute = data.self_mute; + } else { + this.selfMute ??= null; + } + + if ('self_video' in data) { + /** + * Whether this member's camera is enabled + * @type {?boolean} + */ + this.selfVideo = data.self_video; + } else { + this.selfVideo ??= null; + } + + if ('session_id' in data) { + /** + * The session id for this member's connection + * @type {?string} + */ + this.sessionId = data.session_id; + } else { + this.sessionId ??= null; + } + + // The self_stream is property is omitted if false, check for another property + // here to avoid incorrectly clearing this when partial data is specified + if ('self_video' in data) { + /** + * Whether this member is streaming using "Screen Share" + * @type {?boolean} + */ + this.streaming = data.self_stream ?? false; + } else { + this.streaming ??= null; + } + + if ('channel_id' in data) { + /** + * The {@link VoiceChannel} or {@link StageChannel} id the member is in + * @type {?Snowflake} + */ + this.channelId = data.channel_id; + } else { + this.channelId ??= null; + } + + if ('suppress' in data) { + /** + * Whether this member is suppressed from speaking. This property is specific to stage channels only. + * @type {?boolean} + */ + this.suppress = data.suppress; + } else { + this.suppress ??= null; + } + + if ('request_to_speak_timestamp' in data) { + /** + * The time at which the member requested to speak. This property is specific to stage channels only. + * @type {?number} + */ + this.requestToSpeakTimestamp = data.request_to_speak_timestamp && Date.parse(data.request_to_speak_timestamp); + } else { + this.requestToSpeakTimestamp ??= null; + } + + return this; + } + + /** + * The member that this voice state belongs to + * @type {?GuildMember} + * @readonly + */ + get member() { + return this.guild.members.cache.get(this.id) ?? null; + } + + /** + * The channel that the member is connected to + * @type {?(VoiceChannel|StageChannel)} + * @readonly + */ + get channel() { + return this.guild.channels.cache.get(this.channelId) ?? null; + } + + /** + * Whether this member is either self-deafened or server-deafened + * @type {?boolean} + * @readonly + */ + get deaf() { + return this.serverDeaf || this.selfDeaf; + } + + /** + * Whether this member is either self-muted or server-muted + * @type {?boolean} + * @readonly + */ + get mute() { + return this.serverMute || this.selfMute; + } + + /** + * Mutes/unmutes the member of this voice state. + * @param {boolean} [mute=true] Whether or not the member should be muted + * @param {string} [reason] Reason for muting or unmuting + * @returns {Promise<GuildMember>} + */ + setMute(mute = true, reason) { + return this.guild.members.edit(this.id, { mute, reason }); + } + + /** + * Deafens/undeafens the member of this voice state. + * @param {boolean} [deaf=true] Whether or not the member should be deafened + * @param {string} [reason] Reason for deafening or undeafening + * @returns {Promise<GuildMember>} + */ + setDeaf(deaf = true, reason) { + return this.guild.members.edit(this.id, { deaf, reason }); + } + + /** + * Disconnects the member from the channel. + * @param {string} [reason] Reason for disconnecting the member from the channel + * @returns {Promise<GuildMember>} + */ + disconnect(reason) { + return this.setChannel(null, reason); + } + + /** + * Moves the member to a different channel, or disconnects them from the one they're in. + * @param {GuildVoiceChannelResolvable|null} channel Channel to move the member to, or `null` if you want to + * disconnect them from voice. + * @param {string} [reason] Reason for moving member to another channel or disconnecting + * @returns {Promise<GuildMember>} + */ + setChannel(channel, reason) { + return this.guild.members.edit(this.id, { channel, reason }); + } + + /** + * Data to edit the logged in user's own voice state with, when in a stage channel + * @typedef {Object} VoiceStateEditOptions + * @property {boolean} [requestToSpeak] Whether or not the client is requesting to become a speaker. + * <info>Only available to the logged in user's own voice state.</info> + * @property {boolean} [suppressed] Whether or not the user should be suppressed. + */ + + /** + * Edits this voice state. Currently only available when in a stage channel + * @param {VoiceStateEditOptions} options The options to provide + * @returns {Promise<VoiceState>} + */ + async edit(options) { + if (this.channel?.type !== ChannelType.GuildStageVoice) throw new DiscordjsError(ErrorCodes.VoiceNotStageChannel); + + const target = this.client.user.id === this.id ? '@me' : this.id; + + if (target !== '@me' && options.requestToSpeak !== undefined) { + throw new DiscordjsError(ErrorCodes.VoiceStateNotOwn); + } + + if (!['boolean', 'undefined'].includes(typeof options.requestToSpeak)) { + throw new DiscordjsTypeError(ErrorCodes.VoiceStateInvalidType, 'requestToSpeak'); + } + + if (!['boolean', 'undefined'].includes(typeof options.suppressed)) { + throw new DiscordjsTypeError(ErrorCodes.VoiceStateInvalidType, 'suppressed'); + } + + await this.client.rest.patch(Routes.guildVoiceState(this.guild.id, target), { + body: { + channel_id: this.channelId, + request_to_speak_timestamp: options.requestToSpeak + ? new Date().toISOString() + : options.requestToSpeak === false + ? null + : undefined, + suppress: options.suppressed, + }, + }); + return this; + } + + /** + * Toggles the request to speak in the channel. + * Only applicable for stage channels and for the client's own voice state. + * @param {boolean} [requestToSpeak=true] Whether or not the client is requesting to become a speaker. + * @example + * // Making the client request to speak in a stage channel (raise its hand) + * guild.members.me.voice.setRequestToSpeak(true); + * @example + * // Making the client cancel a request to speak + * guild.members.me.voice.setRequestToSpeak(false); + * @returns {Promise<VoiceState>} + */ + setRequestToSpeak(requestToSpeak = true) { + return this.edit({ requestToSpeak }); + } + + /** + * Suppress/unsuppress the user. Only applicable for stage channels. + * @param {boolean} [suppressed=true] Whether or not the user should be suppressed. + * @example + * // Making the client a speaker + * guild.members.me.voice.setSuppressed(false); + * @example + * // Making the client an audience member + * guild.members.me.voice.setSuppressed(true); + * @example + * // Inviting another user to speak + * voiceState.setSuppressed(false); + * @example + * // Moving another user to the audience, or cancelling their invite to speak + * voiceState.setSuppressed(true); + * @returns {Promise<VoiceState>} + */ + setSuppressed(suppressed = true) { + return this.edit({ suppressed }); + } + + toJSON() { + return super.toJSON({ + id: true, + serverDeaf: true, + serverMute: true, + selfDeaf: true, + selfMute: true, + sessionId: true, + channelId: 'channel', + }); + } +} + +module.exports = VoiceState; diff --git a/node_modules/discord.js/src/structures/Webhook.js b/node_modules/discord.js/src/structures/Webhook.js new file mode 100644 index 0000000..738d9e7 --- /dev/null +++ b/node_modules/discord.js/src/structures/Webhook.js @@ -0,0 +1,479 @@ +'use strict'; + +const { makeURLSearchParams } = require('@discordjs/rest'); +const { lazy } = require('@discordjs/util'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { Routes, WebhookType } = require('discord-api-types/v10'); +const MessagePayload = require('./MessagePayload'); +const { DiscordjsError, ErrorCodes } = require('../errors'); +const DataResolver = require('../util/DataResolver'); + +const getMessage = lazy(() => require('./Message').Message); + +/** + * Represents a webhook. + */ +class Webhook { + constructor(client, data) { + /** + * The client that instantiated the webhook + * @name Webhook#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + if (data) this._patch(data); + } + + _patch(data) { + if ('name' in data) { + /** + * The name of the webhook + * @type {string} + */ + this.name = data.name; + } + + /** + * The token for the webhook, unavailable for follower webhooks and webhooks owned by another application. + * @name Webhook#token + * @type {?string} + */ + Object.defineProperty(this, 'token', { + value: data.token ?? null, + writable: true, + configurable: true, + }); + + if ('avatar' in data) { + /** + * The avatar for the webhook + * @type {?string} + */ + this.avatar = data.avatar; + } + + /** + * The webhook's id + * @type {Snowflake} + */ + this.id = data.id; + + if ('type' in data) { + /** + * The type of the webhook + * @type {WebhookType} + */ + this.type = data.type; + } + + if ('guild_id' in data) { + /** + * The guild the webhook belongs to + * @type {Snowflake} + */ + this.guildId = data.guild_id; + } + + if ('channel_id' in data) { + /** + * The id of the channel the webhook belongs to + * @type {Snowflake} + */ + this.channelId = data.channel_id; + } + + if ('user' in data) { + /** + * The owner of the webhook + * @type {?(User|APIUser)} + */ + this.owner = this.client.users?._add(data.user) ?? data.user; + } else { + this.owner ??= null; + } + + if ('application_id' in data) { + /** + * The application that created this webhook + * @type {?Snowflake} + */ + this.applicationId = data.application_id; + } else { + this.applicationId ??= null; + } + + if ('source_guild' in data) { + /** + * The source guild of the webhook + * @type {?(Guild|APIGuild)} + */ + this.sourceGuild = this.client.guilds?.resolve(data.source_guild.id) ?? data.source_guild; + } else { + this.sourceGuild ??= null; + } + + if ('source_channel' in data) { + /** + * The source channel of the webhook + * @type {?(NewsChannel|APIChannel)} + */ + this.sourceChannel = this.client.channels?.resolve(data.source_channel?.id) ?? data.source_channel; + } else { + this.sourceChannel ??= null; + } + } + + /** + * Options that can be passed into send. + * @typedef {BaseMessageOptions} WebhookMessageCreateOptions + * @property {boolean} [tts=false] Whether the message should be spoken aloud + * @property {MessageFlags} [flags] Which flags to set for the message. + * <info>Only the {@link MessageFlags.SuppressEmbeds} flag can be set.</info> + * @property {string} [username=this.name] Username override for the message + * @property {string} [avatarURL] Avatar URL override for the message + * @property {Snowflake} [threadId] The id of the thread in the channel to send to. + * <info>For interaction webhooks, this property is ignored</info> + * @property {string} [threadName] Name of the thread to create (only available if webhook is in a forum channel) + */ + + /** + * Options that can be passed into editMessage. + * @typedef {BaseMessageOptions} WebhookMessageEditOptions + * @property {Attachment[]} [attachments] Attachments to send with the message + * @property {Snowflake} [threadId] The id of the thread this message belongs to + * <info>For interaction webhooks, this property is ignored</info> + */ + + /** + * The channel the webhook belongs to + * @type {?(TextChannel|VoiceChannel|StageChannel|NewsChannel|ForumChannel)} + * @readonly + */ + get channel() { + return this.client.channels.resolve(this.channelId); + } + + /** + * Sends a message with this webhook. + * @param {string|MessagePayload|WebhookMessageCreateOptions} options The options to provide + * @returns {Promise<Message>} + * @example + * // Send a basic message + * webhook.send('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a basic message in a thread + * webhook.send({ content: 'hello!', threadId: '836856309672348295' }) + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a remote file + * webhook.send({ + * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send a local file + * webhook.send({ + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send an embed with a local image inside + * webhook.send({ + * content: 'This is an embed', + * embeds: [{ + * thumbnail: { + * url: 'attachment://file.jpg' + * } + * }], + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg' + * }] + * }) + * .then(console.log) + * .catch(console.error); + */ + async send(options) { + if (!this.token) throw new DiscordjsError(ErrorCodes.WebhookTokenUnavailable); + + let messagePayload; + + if (options instanceof MessagePayload) { + messagePayload = options.resolveBody(); + } else { + messagePayload = MessagePayload.create(this, options).resolveBody(); + } + + const query = makeURLSearchParams({ + wait: true, + thread_id: messagePayload.options.threadId, + }); + + const { body, files } = await messagePayload.resolveFiles(); + const d = await this.client.rest.post(Routes.webhook(this.id, this.token), { + body, + files, + query, + auth: false, + }); + + if (!this.client.channels) return d; + return this.client.channels.cache.get(d.channel_id)?.messages._add(d, false) ?? new (getMessage())(this.client, d); + } + + /** + * Sends a raw slack message with this webhook. + * @param {Object} body The raw body to send + * @returns {Promise<boolean>} + * @example + * // Send a slack message + * webhook.sendSlackMessage({ + * 'username': 'Wumpus', + * 'attachments': [{ + * 'pretext': 'this looks pretty cool', + * 'color': '#F0F', + * 'footer_icon': 'http://snek.s3.amazonaws.com/topSnek.png', + * 'footer': 'Powered by sneks', + * 'ts': Date.now() / 1_000 + * }] + * }).catch(console.error); + * @see {@link https://api.slack.com/messaging/webhooks} + */ + async sendSlackMessage(body) { + if (!this.token) throw new DiscordjsError(ErrorCodes.WebhookTokenUnavailable); + + const data = await this.client.rest.post(Routes.webhookPlatform(this.id, this.token, 'slack'), { + query: makeURLSearchParams({ wait: true }), + auth: false, + body, + }); + return data.toString() === 'ok'; + } + + /** + * Options used to edit a {@link Webhook}. + * @typedef {Object} WebhookEditOptions + * @property {string} [name=this.name] The new name for the webhook + * @property {?(BufferResolvable)} [avatar] The new avatar for the webhook + * @property {GuildTextChannelResolvable|VoiceChannel|StageChannel|ForumChannel} [channel] + * The new channel for the webhook + * @property {string} [reason] Reason for editing the webhook + */ + + /** + * Edits this webhook. + * @param {WebhookEditOptions} options Options for editing the webhook + * @returns {Promise<Webhook>} + */ + async edit({ name = this.name, avatar, channel, reason }) { + if (avatar && !(typeof avatar === 'string' && avatar.startsWith('data:'))) { + avatar = await DataResolver.resolveImage(avatar); + } + channel &&= channel.id ?? channel; + const data = await this.client.rest.patch(Routes.webhook(this.id, channel ? undefined : this.token), { + body: { name, avatar, channel_id: channel }, + reason, + auth: !this.token || Boolean(channel), + }); + + this.name = data.name; + this.avatar = data.avatar; + this.channelId = data.channel_id; + return this; + } + + /** + * Options that can be passed into fetchMessage. + * @typedef {options} WebhookFetchMessageOptions + * @property {boolean} [cache=true] Whether to cache the message. + * @property {Snowflake} [threadId] The id of the thread this message belongs to. + * <info>For interaction webhooks, this property is ignored</info> + */ + + /** + * Gets a message that was sent by this webhook. + * @param {Snowflake|'@original'} message The id of the message to fetch + * @param {WebhookFetchMessageOptions} [options={}] The options to provide to fetch the message. + * @returns {Promise<Message>} Returns the message sent by this webhook + */ + async fetchMessage(message, { threadId } = {}) { + if (!this.token) throw new DiscordjsError(ErrorCodes.WebhookTokenUnavailable); + + const data = await this.client.rest.get(Routes.webhookMessage(this.id, this.token, message), { + query: threadId ? makeURLSearchParams({ thread_id: threadId }) : undefined, + auth: false, + }); + + if (!this.client.channels) return data; + return ( + this.client.channels.cache.get(data.channel_id)?.messages._add(data, false) ?? + new (getMessage())(this.client, data) + ); + } + + /** + * Edits a message that was sent by this webhook. + * @param {MessageResolvable|'@original'} message The message to edit + * @param {string|MessagePayload|WebhookMessageEditOptions} options The options to provide + * @returns {Promise<Message>} Returns the message edited by this webhook + */ + async editMessage(message, options) { + if (!this.token) throw new DiscordjsError(ErrorCodes.WebhookTokenUnavailable); + + let messagePayload; + + if (options instanceof MessagePayload) messagePayload = options; + else messagePayload = MessagePayload.create(this, options); + + const { body, files } = await messagePayload.resolveBody().resolveFiles(); + + const d = await this.client.rest.patch( + Routes.webhookMessage(this.id, this.token, typeof message === 'string' ? message : message.id), + { + body, + files, + query: messagePayload.options.threadId + ? makeURLSearchParams({ thread_id: messagePayload.options.threadId }) + : undefined, + auth: false, + }, + ); + + const channelManager = this.client.channels; + if (!channelManager) return d; + + const messageManager = channelManager.cache.get(d.channel_id)?.messages; + if (!messageManager) return new (getMessage())(this.client, d); + + const existing = messageManager.cache.get(d.id); + if (!existing) return messageManager._add(d); + + const clone = existing._clone(); + clone._patch(d); + return clone; + } + + /** + * Deletes the webhook. + * @param {string} [reason] Reason for deleting this webhook + * @returns {Promise<void>} + */ + delete(reason) { + return this.client.deleteWebhook(this.id, { token: this.token, reason }); + } + + /** + * Delete a message that was sent by this webhook. + * @param {MessageResolvable|'@original'} message The message to delete + * @param {Snowflake} [threadId] The id of the thread this message belongs to + * @returns {Promise<void>} + */ + async deleteMessage(message, threadId) { + if (!this.token) throw new DiscordjsError(ErrorCodes.WebhookTokenUnavailable); + + await this.client.rest.delete( + Routes.webhookMessage(this.id, this.token, typeof message === 'string' ? message : message.id), + { + query: threadId ? makeURLSearchParams({ thread_id: threadId }) : undefined, + auth: false, + }, + ); + } + + /** + * The timestamp the webhook was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the webhook was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * The URL of this webhook + * @type {string} + * @readonly + */ + get url() { + return this.client.options.rest.api + Routes.webhook(this.id, this.token); + } + + /** + * A link to the webhook's avatar. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + avatarURL(options = {}) { + return this.avatar && this.client.rest.cdn.avatar(this.id, this.avatar, options); + } + + /** + * Whether this webhook is created by a user. + * @returns {boolean} + */ + isUserCreated() { + return Boolean(this.type === WebhookType.Incoming && this.owner && !this.owner.bot); + } + + /** + * Whether this webhook is created by an application. + * @returns {boolean} + */ + isApplicationCreated() { + return this.type === WebhookType.Application; + } + + /** + * Whether or not this webhook is a channel follower webhook. + * @returns {boolean} + */ + isChannelFollower() { + return this.type === WebhookType.ChannelFollower; + } + + /** + * Whether or not this webhook is an incoming webhook. + * @returns {boolean} + */ + isIncoming() { + return this.type === WebhookType.Incoming; + } + + static applyToClass(structure, ignore = []) { + for (const prop of [ + 'send', + 'sendSlackMessage', + 'fetchMessage', + 'edit', + 'editMessage', + 'delete', + 'deleteMessage', + 'createdTimestamp', + 'createdAt', + 'url', + ]) { + if (ignore.includes(prop)) continue; + Object.defineProperty(structure.prototype, prop, Object.getOwnPropertyDescriptor(Webhook.prototype, prop)); + } + } +} + +module.exports = Webhook; diff --git a/node_modules/discord.js/src/structures/WelcomeChannel.js b/node_modules/discord.js/src/structures/WelcomeChannel.js new file mode 100644 index 0000000..d783e06 --- /dev/null +++ b/node_modules/discord.js/src/structures/WelcomeChannel.js @@ -0,0 +1,60 @@ +'use strict'; + +const Base = require('./Base'); +const { Emoji } = require('./Emoji'); + +/** + * Represents a channel link in a guild's welcome screen. + * @extends {Base} + */ +class WelcomeChannel extends Base { + constructor(guild, data) { + super(guild.client); + + /** + * The guild for this welcome channel + * @type {Guild|InviteGuild} + */ + this.guild = guild; + + /** + * The description of this welcome channel + * @type {string} + */ + this.description = data.description; + + /** + * The raw emoji data + * @type {Object} + * @private + */ + this._emoji = { + name: data.emoji_name, + id: data.emoji_id, + }; + + /** + * The id of this welcome channel + * @type {Snowflake} + */ + this.channelId = data.channel_id; + } + + /** + * The channel of this welcome channel + * @type {?(TextChannel|NewsChannel|ForumChannel)} + */ + get channel() { + return this.client.channels.resolve(this.channelId); + } + + /** + * The emoji of this welcome channel + * @type {GuildEmoji|Emoji} + */ + get emoji() { + return this.client.emojis.resolve(this._emoji.id) ?? new Emoji(this.client, this._emoji); + } +} + +module.exports = WelcomeChannel; diff --git a/node_modules/discord.js/src/structures/WelcomeScreen.js b/node_modules/discord.js/src/structures/WelcomeScreen.js new file mode 100644 index 0000000..9ff79bc --- /dev/null +++ b/node_modules/discord.js/src/structures/WelcomeScreen.js @@ -0,0 +1,49 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { GuildFeature } = require('discord-api-types/v10'); +const Base = require('./Base'); +const WelcomeChannel = require('./WelcomeChannel'); + +/** + * Represents a welcome screen. + * @extends {Base} + */ +class WelcomeScreen extends Base { + constructor(guild, data) { + super(guild.client); + + /** + * The guild for this welcome screen + * @type {Guild} + */ + this.guild = guild; + + /** + * The description of this welcome screen + * @type {?string} + */ + this.description = data.description ?? null; + + /** + * Collection of welcome channels belonging to this welcome screen + * @type {Collection<Snowflake, WelcomeChannel>} + */ + this.welcomeChannels = new Collection(); + + for (const channel of data.welcome_channels) { + const welcomeChannel = new WelcomeChannel(this.guild, channel); + this.welcomeChannels.set(welcomeChannel.channelId, welcomeChannel); + } + } + + /** + * Whether the welcome screen is enabled on the guild + * @type {boolean} + */ + get enabled() { + return this.guild.features.includes(GuildFeature.WelcomeScreenEnabled); + } +} + +module.exports = WelcomeScreen; diff --git a/node_modules/discord.js/src/structures/Widget.js b/node_modules/discord.js/src/structures/Widget.js new file mode 100644 index 0000000..344c81a --- /dev/null +++ b/node_modules/discord.js/src/structures/Widget.js @@ -0,0 +1,88 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { Routes } = require('discord-api-types/v10'); +const Base = require('./Base'); +const WidgetMember = require('./WidgetMember'); + +/** + * Represents a Widget. + * @extends {Base} + */ +class Widget extends Base { + constructor(client, data) { + super(client); + this._patch(data); + } + + /** + * Represents a channel in a Widget + * @typedef {Object} WidgetChannel + * @property {Snowflake} id Id of the channel + * @property {string} name Name of the channel + * @property {number} position Position of the channel + */ + + _patch(data) { + /** + * The id of the guild. + * @type {Snowflake} + */ + this.id = data.id; + + if ('name' in data) { + /** + * The name of the guild. + * @type {string} + */ + this.name = data.name; + } + + if ('instant_invite' in data) { + /** + * The invite of the guild. + * @type {?string} + */ + this.instantInvite = data.instant_invite; + } + + /** + * The list of channels in the guild. + * @type {Collection<Snowflake, WidgetChannel>} + */ + this.channels = new Collection(); + for (const channel of data.channels) { + this.channels.set(channel.id, channel); + } + + /** + * The list of members in the guild. + * These strings are just arbitrary numbers, they aren't Snowflakes. + * @type {Collection<string, WidgetMember>} + */ + this.members = new Collection(); + for (const member of data.members) { + this.members.set(member.id, new WidgetMember(this.client, member)); + } + + if ('presence_count' in data) { + /** + * The number of members online. + * @type {number} + */ + this.presenceCount = data.presence_count; + } + } + + /** + * Update the Widget. + * @returns {Promise<Widget>} + */ + async fetch() { + const data = await this.client.rest.get(Routes.guildWidgetJSON(this.id)); + this._patch(data); + return this; + } +} + +module.exports = Widget; diff --git a/node_modules/discord.js/src/structures/WidgetMember.js b/node_modules/discord.js/src/structures/WidgetMember.js new file mode 100644 index 0000000..d7aca21 --- /dev/null +++ b/node_modules/discord.js/src/structures/WidgetMember.js @@ -0,0 +1,99 @@ +'use strict'; + +const Base = require('./Base'); + +/** + * Represents a WidgetMember. + * @extends {Base} + */ +class WidgetMember extends Base { + /** + * Activity sent in a {@link WidgetMember}. + * @typedef {Object} WidgetActivity + * @property {string} name The name of the activity + */ + + constructor(client, data) { + super(client); + + /** + * The id of the user. It's an arbitrary number. + * @type {string} + */ + this.id = data.id; + + /** + * The username of the member. + * @type {string} + */ + this.username = data.username; + + /** + * The discriminator of the member. + * @type {string} + */ + this.discriminator = data.discriminator; + + /** + * The avatar of the member. + * @type {?string} + */ + this.avatar = data.avatar; + + /** + * The status of the member. + * @type {PresenceStatus} + */ + this.status = data.status; + + /** + * If the member is server deafened + * @type {?boolean} + */ + this.deaf = data.deaf ?? null; + + /** + * If the member is server muted + * @type {?boolean} + */ + this.mute = data.mute ?? null; + + /** + * If the member is self deafened + * @type {?boolean} + */ + this.selfDeaf = data.self_deaf ?? null; + + /** + * If the member is self muted + * @type {?boolean} + */ + this.selfMute = data.self_mute ?? null; + + /** + * If the member is suppressed + * @type {?boolean} + */ + this.suppress = data.suppress ?? null; + + /** + * The id of the voice channel the member is in, if any + * @type {?Snowflake} + */ + this.channelId = data.channel_id ?? null; + + /** + * The avatar URL of the member. + * @type {string} + */ + this.avatarURL = data.avatar_url; + + /** + * The activity of the member. + * @type {?WidgetActivity} + */ + this.activity = data.activity ?? null; + } +} + +module.exports = WidgetMember; diff --git a/node_modules/discord.js/src/structures/interfaces/Application.js b/node_modules/discord.js/src/structures/interfaces/Application.js new file mode 100644 index 0000000..5e81465 --- /dev/null +++ b/node_modules/discord.js/src/structures/interfaces/Application.js @@ -0,0 +1,108 @@ +'use strict'; + +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const Base = require('../Base'); + +/** + * Represents an OAuth2 Application. + * @extends {Base} + * @abstract + */ +class Application extends Base { + constructor(client, data) { + super(client); + this._patch(data); + } + + _patch(data) { + /** + * The application's id + * @type {Snowflake} + */ + this.id = data.id; + + if ('name' in data) { + /** + * The name of the application + * @type {?string} + */ + this.name = data.name; + } else { + this.name ??= null; + } + + if ('description' in data) { + /** + * The application's description + * @type {?string} + */ + this.description = data.description; + } else { + this.description ??= null; + } + + if ('icon' in data) { + /** + * The application's icon hash + * @type {?string} + */ + this.icon = data.icon; + } else { + this.icon ??= null; + } + } + + /** + * The timestamp the application was created at + * @type {number} + * @readonly + */ + get createdTimestamp() { + return DiscordSnowflake.timestampFrom(this.id); + } + + /** + * The time the application was created at + * @type {Date} + * @readonly + */ + get createdAt() { + return new Date(this.createdTimestamp); + } + + /** + * A link to the application's icon. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + iconURL(options = {}) { + return this.icon && this.client.rest.cdn.appIcon(this.id, this.icon, options); + } + + /** + * A link to this application's cover image. + * @param {ImageURLOptions} [options={}] Options for the image URL + * @returns {?string} + */ + coverURL(options = {}) { + return this.cover && this.client.rest.cdn.appIcon(this.id, this.cover, options); + } + + /** + * When concatenated with a string, this automatically returns the application's name instead of the + * Application object. + * @returns {?string} + * @example + * // Logs: Application name: My App + * console.log(`Application name: ${application}`); + */ + toString() { + return this.name; + } + + toJSON() { + return super.toJSON({ createdTimestamp: true }); + } +} + +module.exports = Application; diff --git a/node_modules/discord.js/src/structures/interfaces/Collector.js b/node_modules/discord.js/src/structures/interfaces/Collector.js new file mode 100644 index 0000000..65f4117 --- /dev/null +++ b/node_modules/discord.js/src/structures/interfaces/Collector.js @@ -0,0 +1,335 @@ +'use strict'; + +const EventEmitter = require('node:events'); +const { setTimeout, clearTimeout } = require('node:timers'); +const { Collection } = require('@discordjs/collection'); +const { DiscordjsTypeError, ErrorCodes } = require('../../errors'); +const { flatten } = require('../../util/Util'); + +/** + * Filter to be applied to the collector. + * @typedef {Function} CollectorFilter + * @param {...*} args Any arguments received by the listener + * @param {Collection} collection The items collected by this collector + * @returns {boolean|Promise<boolean>} + */ + +/** + * Options to be applied to the collector. + * @typedef {Object} CollectorOptions + * @property {CollectorFilter} [filter] The filter applied to this collector + * @property {number} [time] How long to run the collector for in milliseconds + * @property {number} [idle] How long to stop the collector after inactivity in milliseconds + * @property {boolean} [dispose=false] Whether to dispose data when it's deleted + */ + +/** + * Abstract class for defining a new Collector. + * @extends {EventEmitter} + * @abstract + */ +class Collector extends EventEmitter { + constructor(client, options = {}) { + super(); + + /** + * The client that instantiated this Collector + * @name Collector#client + * @type {Client} + * @readonly + */ + Object.defineProperty(this, 'client', { value: client }); + + /** + * The filter applied to this collector + * @type {CollectorFilter} + * @returns {boolean|Promise<boolean>} + */ + this.filter = options.filter ?? (() => true); + + /** + * The options of this collector + * @type {CollectorOptions} + */ + this.options = options; + + /** + * The items collected by this collector + * @type {Collection} + */ + this.collected = new Collection(); + + /** + * Whether this collector has finished collecting + * @type {boolean} + */ + this.ended = false; + + /** + * Timeout for cleanup + * @type {?Timeout} + * @private + */ + this._timeout = null; + + /** + * Timeout for cleanup due to inactivity + * @type {?Timeout} + * @private + */ + this._idletimeout = null; + + /** + * The reason the collector ended + * @type {string|null} + * @private + */ + this._endReason = null; + + if (typeof this.filter !== 'function') { + throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options.filter', 'function'); + } + + this.handleCollect = this.handleCollect.bind(this); + this.handleDispose = this.handleDispose.bind(this); + + if (options.time) this._timeout = setTimeout(() => this.stop('time'), options.time).unref(); + if (options.idle) this._idletimeout = setTimeout(() => this.stop('idle'), options.idle).unref(); + + /** + * The timestamp at which this collector last collected an item + * @type {?number} + */ + this.lastCollectedTimestamp = null; + } + + /** + * The Date at which this collector last collected an item + * @type {?Date} + */ + get lastCollectedAt() { + return this.lastCollectedTimestamp && new Date(this.lastCollectedTimestamp); + } + + /** + * Call this to handle an event as a collectable element. Accepts any event data as parameters. + * @param {...*} args The arguments emitted by the listener + * @returns {Promise<void>} + * @emits Collector#collect + */ + async handleCollect(...args) { + const collectedId = await this.collect(...args); + + if (collectedId) { + const filterResult = await this.filter(...args, this.collected); + if (filterResult) { + this.collected.set(collectedId, args[0]); + + /** + * Emitted whenever an element is collected. + * @event Collector#collect + * @param {...*} args The arguments emitted by the listener + */ + this.emit('collect', ...args); + + this.lastCollectedTimestamp = Date.now(); + if (this._idletimeout) { + clearTimeout(this._idletimeout); + this._idletimeout = setTimeout(() => this.stop('idle'), this.options.idle).unref(); + } + } else { + /** + * Emitted whenever an element is not collected by the collector. + * @event Collector#ignore + * @param {...*} args The arguments emitted by the listener + */ + this.emit('ignore', ...args); + } + } + this.checkEnd(); + } + + /** + * Call this to remove an element from the collection. Accepts any event data as parameters. + * @param {...*} args The arguments emitted by the listener + * @returns {Promise<void>} + * @emits Collector#dispose + */ + async handleDispose(...args) { + if (!this.options.dispose) return; + + const dispose = this.dispose(...args); + if (!dispose || !(await this.filter(...args)) || !this.collected.has(dispose)) return; + this.collected.delete(dispose); + + /** + * Emitted whenever an element is disposed of. + * @event Collector#dispose + * @param {...*} args The arguments emitted by the listener + */ + this.emit('dispose', ...args); + this.checkEnd(); + } + + /** + * Returns a promise that resolves with the next collected element; + * rejects with collected elements if the collector finishes without receiving a next element + * @type {Promise} + * @readonly + */ + get next() { + return new Promise((resolve, reject) => { + if (this.ended) { + reject(this.collected); + return; + } + + const cleanup = () => { + this.removeListener('collect', onCollect); + this.removeListener('end', onEnd); + }; + + const onCollect = item => { + cleanup(); + resolve(item); + }; + + const onEnd = () => { + cleanup(); + reject(this.collected); + }; + + this.on('collect', onCollect); + this.on('end', onEnd); + }); + } + + /** + * Stops this collector and emits the `end` event. + * @param {string} [reason='user'] The reason this collector is ending + * @emits Collector#end + */ + stop(reason = 'user') { + if (this.ended) return; + + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = null; + } + if (this._idletimeout) { + clearTimeout(this._idletimeout); + this._idletimeout = null; + } + + this._endReason = reason; + this.ended = true; + + /** + * Emitted when the collector is finished collecting. + * @event Collector#end + * @param {Collection} collected The elements collected by the collector + * @param {string} reason The reason the collector ended + */ + this.emit('end', this.collected, reason); + } + + /** + * Options used to reset the timeout and idle timer of a {@link Collector}. + * @typedef {Object} CollectorResetTimerOptions + * @property {number} [time] How long to run the collector for (in milliseconds) + * @property {number} [idle] How long to wait to stop the collector after inactivity (in milliseconds) + */ + + /** + * Resets the collector's timeout and idle timer. + * @param {CollectorResetTimerOptions} [options] Options for resetting + */ + resetTimer({ time, idle } = {}) { + if (this._timeout) { + clearTimeout(this._timeout); + this._timeout = setTimeout(() => this.stop('time'), time ?? this.options.time).unref(); + } + if (this._idletimeout) { + clearTimeout(this._idletimeout); + this._idletimeout = setTimeout(() => this.stop('idle'), idle ?? this.options.idle).unref(); + } + } + + /** + * Checks whether the collector should end, and if so, ends it. + * @returns {boolean} Whether the collector ended or not + */ + checkEnd() { + const reason = this.endReason; + if (reason) this.stop(reason); + return Boolean(reason); + } + + /** + * Allows collectors to be consumed with for-await-of loops + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of} + */ + async *[Symbol.asyncIterator]() { + const queue = []; + const onCollect = (...item) => queue.push(item); + this.on('collect', onCollect); + + try { + while (queue.length || !this.ended) { + if (queue.length) { + yield queue.shift(); + } else { + // eslint-disable-next-line no-await-in-loop + await new Promise(resolve => { + const tick = () => { + this.removeListener('collect', tick); + this.removeListener('end', tick); + return resolve(); + }; + this.on('collect', tick); + this.on('end', tick); + }); + } + } + } finally { + this.removeListener('collect', onCollect); + } + } + + toJSON() { + return flatten(this); + } + + /* eslint-disable no-empty-function */ + /** + * The reason this collector has ended with, or null if it hasn't ended yet + * @type {?string} + * @readonly + */ + get endReason() { + return this._endReason; + } + + /** + * Handles incoming events from the `handleCollect` function. Returns null if the event should not + * be collected, or returns an object describing the data that should be stored. + * @see Collector#handleCollect + * @param {...*} args Any args the event listener emits + * @returns {?(*|Promise<?*>)} Data to insert into collection, if any + * @abstract + */ + collect() {} + + /** + * Handles incoming events from the `handleDispose`. Returns null if the event should not + * be disposed, or returns the key that should be removed. + * @see Collector#handleDispose + * @param {...*} args Any args the event listener emits + * @returns {?*} Key to remove from the collection, if any + * @abstract + */ + dispose() {} + /* eslint-enable no-empty-function */ +} + +module.exports = Collector; diff --git a/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js b/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js new file mode 100644 index 0000000..15256e3 --- /dev/null +++ b/node_modules/discord.js/src/structures/interfaces/InteractionResponses.js @@ -0,0 +1,320 @@ +'use strict'; + +const { isJSONEncodable } = require('@discordjs/util'); +const { InteractionResponseType, MessageFlags, Routes, InteractionType } = require('discord-api-types/v10'); +const { DiscordjsError, ErrorCodes } = require('../../errors'); +const InteractionCollector = require('../InteractionCollector'); +const InteractionResponse = require('../InteractionResponse'); +const MessagePayload = require('../MessagePayload'); + +/** + * @typedef {Object} ModalComponentData + * @property {string} title The title of the modal + * @property {string} customId The custom id of the modal + * @property {ActionRow[]} components The components within this modal + */ + +/** + * Interface for classes that support shared interaction response types. + * @interface + */ +class InteractionResponses { + /** + * Options for deferring the reply to an {@link BaseInteraction}. + * @typedef {Object} InteractionDeferReplyOptions + * @property {boolean} [ephemeral] Whether the reply should be ephemeral + * @property {boolean} [fetchReply] Whether to fetch the reply + */ + + /** + * Options for deferring and updating the reply to a {@link MessageComponentInteraction}. + * @typedef {Object} InteractionDeferUpdateOptions + * @property {boolean} [fetchReply] Whether to fetch the reply + */ + + /** + * Options for a reply to a {@link BaseInteraction}. + * @typedef {BaseMessageOptions} InteractionReplyOptions + * @property {boolean} [tts=false] Whether the message should be spoken aloud + * @property {boolean} [ephemeral] Whether the reply should be ephemeral + * @property {boolean} [fetchReply] Whether to fetch the reply + * @property {MessageFlags} [flags] Which flags to set for the message. + * <info>Only `MessageFlags.SuppressEmbeds` and `MessageFlags.Ephemeral` can be set.</info> + */ + + /** + * Options for updating the message received from a {@link MessageComponentInteraction}. + * @typedef {MessageEditOptions} InteractionUpdateOptions + * @property {boolean} [fetchReply] Whether to fetch the reply + */ + + /** + * Defers the reply to this interaction. + * @param {InteractionDeferReplyOptions} [options] Options for deferring the reply to this interaction + * @returns {Promise<Message|InteractionResponse>} + * @example + * // Defer the reply to this interaction + * interaction.deferReply() + * .then(console.log) + * .catch(console.error) + * @example + * // Defer to send an ephemeral reply later + * interaction.deferReply({ ephemeral: true }) + * .then(console.log) + * .catch(console.error); + */ + async deferReply(options = {}) { + if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied); + this.ephemeral = options.ephemeral ?? false; + await this.client.rest.post(Routes.interactionCallback(this.id, this.token), { + body: { + type: InteractionResponseType.DeferredChannelMessageWithSource, + data: { + flags: options.ephemeral ? MessageFlags.Ephemeral : undefined, + }, + }, + auth: false, + }); + this.deferred = true; + + return options.fetchReply ? this.fetchReply() : new InteractionResponse(this); + } + + /** + * Creates a reply to this interaction. + * <info>Use the `fetchReply` option to get the bot's reply message.</info> + * @param {string|MessagePayload|InteractionReplyOptions} options The options for the reply + * @returns {Promise<Message|InteractionResponse>} + * @example + * // Reply to the interaction and fetch the response + * interaction.reply({ content: 'Pong!', fetchReply: true }) + * .then((message) => console.log(`Reply sent with content ${message.content}`)) + * .catch(console.error); + * @example + * // Create an ephemeral reply with an embed + * const embed = new EmbedBuilder().setDescription('Pong!'); + * + * interaction.reply({ embeds: [embed], ephemeral: true }) + * .then(() => console.log('Reply sent.')) + * .catch(console.error); + */ + async reply(options) { + if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied); + this.ephemeral = options.ephemeral ?? false; + + let messagePayload; + if (options instanceof MessagePayload) messagePayload = options; + else messagePayload = MessagePayload.create(this, options); + + const { body: data, files } = await messagePayload.resolveBody().resolveFiles(); + + await this.client.rest.post(Routes.interactionCallback(this.id, this.token), { + body: { + type: InteractionResponseType.ChannelMessageWithSource, + data, + }, + files, + auth: false, + }); + this.replied = true; + + return options.fetchReply ? this.fetchReply() : new InteractionResponse(this); + } + + /** + * Fetches a reply to this interaction. + * @see Webhook#fetchMessage + * @param {Snowflake|'@original'} [message='@original'] The response to fetch + * @returns {Promise<Message>} + * @example + * // Fetch the initial reply to this interaction + * interaction.fetchReply() + * .then(reply => console.log(`Replied with ${reply.content}`)) + * .catch(console.error); + */ + fetchReply(message = '@original') { + return this.webhook.fetchMessage(message); + } + + /** + * Options that can be passed into {@link InteractionResponses#editReply}. + * @typedef {WebhookMessageEditOptions} InteractionEditReplyOptions + * @property {MessageResolvable|'@original'} [message='@original'] The response to edit + */ + + /** + * Edits a reply to this interaction. + * @see Webhook#editMessage + * @param {string|MessagePayload|InteractionEditReplyOptions} options The new options for the message + * @returns {Promise<Message>} + * @example + * // Edit the initial reply to this interaction + * interaction.editReply('New content') + * .then(console.log) + * .catch(console.error); + */ + async editReply(options) { + if (!this.deferred && !this.replied) throw new DiscordjsError(ErrorCodes.InteractionNotReplied); + const msg = await this.webhook.editMessage(options.message ?? '@original', options); + this.replied = true; + return msg; + } + + /** + * Deletes a reply to this interaction. + * @see Webhook#deleteMessage + * @param {MessageResolvable|'@original'} [message='@original'] The response to delete + * @returns {Promise<void>} + * @example + * // Delete the initial reply to this interaction + * interaction.deleteReply() + * .then(console.log) + * .catch(console.error); + */ + async deleteReply(message = '@original') { + await this.webhook.deleteMessage(message); + } + + /** + * Send a follow-up message to this interaction. + * @param {string|MessagePayload|InteractionReplyOptions} options The options for the reply + * @returns {Promise<Message>} + */ + followUp(options) { + if (!this.deferred && !this.replied) return Promise.reject(new DiscordjsError(ErrorCodes.InteractionNotReplied)); + return this.webhook.send(options); + } + + /** + * Defers an update to the message to which the component was attached. + * @param {InteractionDeferUpdateOptions} [options] Options for deferring the update to this interaction + * @returns {Promise<Message|InteractionResponse>} + * @example + * // Defer updating and reset the component's loading state + * interaction.deferUpdate() + * .then(console.log) + * .catch(console.error); + */ + async deferUpdate(options = {}) { + if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied); + await this.client.rest.post(Routes.interactionCallback(this.id, this.token), { + body: { + type: InteractionResponseType.DeferredMessageUpdate, + }, + auth: false, + }); + this.deferred = true; + + return options.fetchReply ? this.fetchReply() : new InteractionResponse(this, this.message?.interaction?.id); + } + + /** + * Updates the original message of the component on which the interaction was received on. + * @param {string|MessagePayload|InteractionUpdateOptions} options The options for the updated message + * @returns {Promise<Message|void>} + * @example + * // Remove the components from the message + * interaction.update({ + * content: "A component interaction was received", + * components: [] + * }) + * .then(console.log) + * .catch(console.error); + */ + async update(options) { + if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied); + + let messagePayload; + if (options instanceof MessagePayload) messagePayload = options; + else messagePayload = MessagePayload.create(this, options); + + const { body: data, files } = await messagePayload.resolveBody().resolveFiles(); + + await this.client.rest.post(Routes.interactionCallback(this.id, this.token), { + body: { + type: InteractionResponseType.UpdateMessage, + data, + }, + files, + auth: false, + }); + this.replied = true; + + return options.fetchReply ? this.fetchReply() : new InteractionResponse(this, this.message.interaction?.id); + } + + /** + * Shows a modal component + * @param {ModalBuilder|ModalComponentData|APIModalInteractionResponseCallbackData} modal The modal to show + * @returns {Promise<void>} + */ + async showModal(modal) { + if (this.deferred || this.replied) throw new DiscordjsError(ErrorCodes.InteractionAlreadyReplied); + await this.client.rest.post(Routes.interactionCallback(this.id, this.token), { + body: { + type: InteractionResponseType.Modal, + data: isJSONEncodable(modal) ? modal.toJSON() : this.client.options.jsonTransformer(modal), + }, + auth: false, + }); + this.replied = true; + } + + /** + * An object containing the same properties as {@link CollectorOptions}, but a few less: + * @typedef {Object} AwaitModalSubmitOptions + * @property {CollectorFilter} [filter] The filter applied to this collector + * @property {number} time Time in milliseconds to wait for an interaction before rejecting + */ + + /** + * Collects a single modal submit interaction that passes the filter. + * The Promise will reject if the time expires. + * @param {AwaitModalSubmitOptions} options Options to pass to the internal collector + * @returns {Promise<ModalSubmitInteraction>} + * @example + * // Collect a modal submit interaction + * const filter = (interaction) => interaction.customId === 'modal'; + * interaction.awaitModalSubmit({ filter, time: 15_000 }) + * .then(interaction => console.log(`${interaction.customId} was submitted!`)) + * .catch(console.error); + */ + awaitModalSubmit(options) { + if (typeof options.time !== 'number') throw new DiscordjsError(ErrorCodes.InvalidType, 'time', 'number'); + const _options = { ...options, max: 1, interactionType: InteractionType.ModalSubmit }; + return new Promise((resolve, reject) => { + const collector = new InteractionCollector(this.client, _options); + collector.once('end', (interactions, reason) => { + const interaction = interactions.first(); + if (interaction) resolve(interaction); + else reject(new DiscordjsError(ErrorCodes.InteractionCollectorError, reason)); + }); + }); + } + + static applyToClass(structure, ignore = []) { + const props = [ + 'deferReply', + 'reply', + 'fetchReply', + 'editReply', + 'deleteReply', + 'followUp', + 'deferUpdate', + 'update', + 'showModal', + 'awaitModalSubmit', + ]; + + for (const prop of props) { + if (ignore.includes(prop)) continue; + Object.defineProperty( + structure.prototype, + prop, + Object.getOwnPropertyDescriptor(InteractionResponses.prototype, prop), + ); + } + } +} + +module.exports = InteractionResponses; diff --git a/node_modules/discord.js/src/structures/interfaces/TextBasedChannel.js b/node_modules/discord.js/src/structures/interfaces/TextBasedChannel.js new file mode 100644 index 0000000..cf455b9 --- /dev/null +++ b/node_modules/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -0,0 +1,413 @@ +'use strict'; + +const { Collection } = require('@discordjs/collection'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); +const { InteractionType, Routes } = require('discord-api-types/v10'); +const { DiscordjsTypeError, DiscordjsError, ErrorCodes } = require('../../errors'); +const { MaxBulkDeletableMessageAge } = require('../../util/Constants'); +const InteractionCollector = require('../InteractionCollector'); +const MessageCollector = require('../MessageCollector'); +const MessagePayload = require('../MessagePayload'); + +/** + * Interface for classes that have text-channel-like features. + * @interface + */ +class TextBasedChannel { + constructor() { + /** + * A manager of the messages sent to this channel + * @type {GuildMessageManager} + */ + this.messages = new GuildMessageManager(this); + + /** + * The channel's last message id, if one was sent + * @type {?Snowflake} + */ + this.lastMessageId = null; + + /** + * The timestamp when the last pinned message was pinned, if there was one + * @type {?number} + */ + this.lastPinTimestamp = null; + } + + /** + * The Message object of the last message in the channel, if one was sent + * @type {?Message} + * @readonly + */ + get lastMessage() { + return this.messages.resolve(this.lastMessageId); + } + + /** + * The date when the last pinned message was pinned, if there was one + * @type {?Date} + * @readonly + */ + get lastPinAt() { + return this.lastPinTimestamp && new Date(this.lastPinTimestamp); + } + + /** + * The base message options for messages. + * @typedef {Object} BaseMessageOptions + * @property {string|null} [content=''] The content for the message. This can only be `null` when editing a message. + * @property {Array<(EmbedBuilder|Embed|APIEmbed)>} [embeds] The embeds for the message + * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content + * (see [here](https://discord.com/developers/docs/resources/channel#allowed-mentions-object) for more details) + * @property {Array<(AttachmentBuilder|Attachment|AttachmentPayload|BufferResolvable)>} [files] + * The files to send with the message. + * @property {Array<(ActionRowBuilder|ActionRow|APIActionRowComponent)>} [components] + * Action rows containing interactive components for the message (buttons, select menus) + */ + + /** + * Options for sending a message with a reply. + * @typedef {Object} ReplyOptions + * @property {MessageResolvable} messageReference The message to reply to (must be in the same channel and not system) + * @property {boolean} [failIfNotExists=this.client.options.failIfNotExists] Whether to error if the referenced + * message does not exist (creates a standard message in this case when false) + */ + + /** + * The options for sending a message. + * @typedef {BaseMessageOptions} BaseMessageCreateOptions + * @property {boolean} [tts=false] Whether the message should be spoken aloud + * @property {string} [nonce=''] The nonce for the message + * @property {StickerResolvable[]} [stickers=[]] The stickers to send in the message + * @property {MessageFlags} [flags] Which flags to set for the message. + * <info>Only `MessageFlags.SuppressEmbeds` and `MessageFlags.SuppressNotifications` can be set.</info> + */ + + /** + * The options for sending a message. + * @typedef {BaseMessageCreateOptions} MessageCreateOptions + * @property {ReplyOptions} [reply] The options for replying to a message + */ + + /** + * Options provided to control parsing of mentions by Discord + * @typedef {Object} MessageMentionOptions + * @property {MessageMentionTypes[]} [parse] Types of mentions to be parsed + * @property {Snowflake[]} [users] Snowflakes of Users to be parsed as mentions + * @property {Snowflake[]} [roles] Snowflakes of Roles to be parsed as mentions + * @property {boolean} [repliedUser=true] Whether the author of the Message being replied to should be pinged + */ + + /** + * Types of mentions to enable in MessageMentionOptions. + * - `roles` + * - `users` + * - `everyone` + * @typedef {string} MessageMentionTypes + */ + + /** + * @typedef {Object} FileOptions + * @property {BufferResolvable} attachment File to attach + * @property {string} [name='file.jpg'] Filename of the attachment + * @property {string} description The description of the file + */ + + /** + * Sends a message to this channel. + * @param {string|MessagePayload|MessageCreateOptions} options The options to provide + * @returns {Promise<Message>} + * @example + * // Send a basic message + * channel.send('hello!') + * .then(message => console.log(`Sent message: ${message.content}`)) + * .catch(console.error); + * @example + * // Send a remote file + * channel.send({ + * files: ['https://cdn.discordapp.com/icons/222078108977594368/6e1019b3179d71046e463a75915e7244.png?size=2048'] + * }) + * .then(console.log) + * .catch(console.error); + * @example + * // Send a local file + * channel.send({ + * files: [{ + * attachment: 'entire/path/to/file.jpg', + * name: 'file.jpg', + * description: 'A description of the file' + * }] + * }) + * .then(console.log) + * .catch(console.error); + */ + async send(options) { + const User = require('../User'); + const { GuildMember } = require('../GuildMember'); + + if (this instanceof User || this instanceof GuildMember) { + const dm = await this.createDM(); + return dm.send(options); + } + + let messagePayload; + + if (options instanceof MessagePayload) { + messagePayload = options.resolveBody(); + } else { + messagePayload = MessagePayload.create(this, options).resolveBody(); + } + + const { body, files } = await messagePayload.resolveFiles(); + const d = await this.client.rest.post(Routes.channelMessages(this.id), { body, files }); + + return this.messages.cache.get(d.id) ?? this.messages._add(d); + } + + /** + * Sends a typing indicator in the channel. + * @returns {Promise<void>} Resolves upon the typing status being sent + * @example + * // Start typing in a channel + * channel.sendTyping(); + */ + async sendTyping() { + await this.client.rest.post(Routes.channelTyping(this.id)); + } + + /** + * Creates a Message Collector. + * @param {MessageCollectorOptions} [options={}] The options to pass to the collector + * @returns {MessageCollector} + * @example + * // Create a message collector + * const filter = m => m.content.includes('discord'); + * const collector = channel.createMessageCollector({ filter, time: 15_000 }); + * collector.on('collect', m => console.log(`Collected ${m.content}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createMessageCollector(options = {}) { + return new MessageCollector(this, options); + } + + /** + * An object containing the same properties as CollectorOptions, but a few more: + * @typedef {MessageCollectorOptions} AwaitMessagesOptions + * @property {string[]} [errors] Stop/end reasons that cause the promise to reject + */ + + /** + * Similar to createMessageCollector but in promise form. + * Resolves with a collection of messages that pass the specified filter. + * @param {AwaitMessagesOptions} [options={}] Optional options to pass to the internal collector + * @returns {Promise<Collection<Snowflake, Message>>} + * @example + * // Await !vote messages + * const filter = m => m.content.startsWith('!vote'); + * // Errors: ['time'] treats ending because of the time limit as an error + * channel.awaitMessages({ filter, max: 4, time: 60_000, errors: ['time'] }) + * .then(collected => console.log(collected.size)) + * .catch(collected => console.log(`After a minute, only ${collected.size} out of 4 voted.`)); + */ + awaitMessages(options = {}) { + return new Promise((resolve, reject) => { + const collector = this.createMessageCollector(options); + collector.once('end', (collection, reason) => { + if (options.errors?.includes(reason)) { + reject(collection); + } else { + resolve(collection); + } + }); + }); + } + + /** + * Creates a component interaction collector. + * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector + * @returns {InteractionCollector} + * @example + * // Create a button interaction collector + * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId'; + * const collector = channel.createMessageComponentCollector({ filter, time: 15_000 }); + * collector.on('collect', i => console.log(`Collected ${i.customId}`)); + * collector.on('end', collected => console.log(`Collected ${collected.size} items`)); + */ + createMessageComponentCollector(options = {}) { + return new InteractionCollector(this.client, { + ...options, + interactionType: InteractionType.MessageComponent, + channel: this, + }); + } + + /** + * Collects a single component interaction that passes the filter. + * The Promise will reject if the time expires. + * @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector + * @returns {Promise<MessageComponentInteraction>} + * @example + * // Collect a message component interaction + * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId'; + * channel.awaitMessageComponent({ filter, time: 15_000 }) + * .then(interaction => console.log(`${interaction.customId} was clicked!`)) + * .catch(console.error); + */ + awaitMessageComponent(options = {}) { + const _options = { ...options, max: 1 }; + return new Promise((resolve, reject) => { + const collector = this.createMessageComponentCollector(_options); + collector.once('end', (interactions, reason) => { + const interaction = interactions.first(); + if (interaction) resolve(interaction); + else reject(new DiscordjsError(ErrorCodes.InteractionCollectorError, reason)); + }); + }); + } + + /** + * Bulk deletes given messages that are newer than two weeks. + * @param {Collection<Snowflake, Message>|MessageResolvable[]|number} messages + * Messages or number of messages to delete + * @param {boolean} [filterOld=false] Filter messages to remove those which are older than two weeks automatically + * @returns {Promise<Collection<Snowflake, Message|undefined>>} Returns the deleted messages + * @example + * // Bulk delete messages + * channel.bulkDelete(5) + * .then(messages => console.log(`Bulk deleted ${messages.size} messages`)) + * .catch(console.error); + */ + async bulkDelete(messages, filterOld = false) { + if (Array.isArray(messages) || messages instanceof Collection) { + let messageIds = messages instanceof Collection ? [...messages.keys()] : messages.map(m => m.id ?? m); + if (filterOld) { + messageIds = messageIds.filter( + id => Date.now() - DiscordSnowflake.timestampFrom(id) < MaxBulkDeletableMessageAge, + ); + } + if (messageIds.length === 0) return new Collection(); + if (messageIds.length === 1) { + const message = this.client.actions.MessageDelete.getMessage( + { + message_id: messageIds[0], + }, + this, + ); + await this.client.rest.delete(Routes.channelMessage(this.id, messageIds[0])); + return message ? new Collection([[message.id, message]]) : new Collection(); + } + await this.client.rest.post(Routes.channelBulkDelete(this.id), { body: { messages: messageIds } }); + return messageIds.reduce( + (col, id) => + col.set( + id, + this.client.actions.MessageDeleteBulk.getMessage( + { + message_id: id, + }, + this, + ), + ), + new Collection(), + ); + } + if (!isNaN(messages)) { + const msgs = await this.messages.fetch({ limit: messages }); + return this.bulkDelete(msgs, filterOld); + } + throw new DiscordjsTypeError(ErrorCodes.MessageBulkDeleteType); + } + + /** + * Fetches all webhooks for the channel. + * @returns {Promise<Collection<Snowflake, Webhook>>} + * @example + * // Fetch webhooks + * channel.fetchWebhooks() + * .then(hooks => console.log(`This channel has ${hooks.size} hooks`)) + * .catch(console.error); + */ + fetchWebhooks() { + return this.guild.channels.fetchWebhooks(this.id); + } + + /** + * Options used to create a {@link Webhook}. + * @typedef {Object} ChannelWebhookCreateOptions + * @property {string} name The name of the webhook + * @property {?(BufferResolvable|Base64Resolvable)} [avatar] Avatar for the webhook + * @property {string} [reason] Reason for creating the webhook + */ + + /** + * Creates a webhook for the channel. + * @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook + * @returns {Promise<Webhook>} Returns the created Webhook + * @example + * // Create a webhook for the current channel + * channel.createWebhook({ + * name: 'Snek', + * avatar: 'https://i.imgur.com/mI8XcpG.jpg', + * reason: 'Needed a cool new Webhook' + * }) + * .then(console.log) + * .catch(console.error) + */ + createWebhook(options) { + return this.guild.channels.createWebhook({ channel: this.id, ...options }); + } + + /** + * Sets the rate limit per user (slowmode) for this channel. + * @param {number} rateLimitPerUser The new rate limit in seconds + * @param {string} [reason] Reason for changing the channel's rate limit + * @returns {Promise<this>} + */ + setRateLimitPerUser(rateLimitPerUser, reason) { + return this.edit({ rateLimitPerUser, reason }); + } + + /** + * Sets whether this channel is flagged as NSFW. + * @param {boolean} [nsfw=true] Whether the channel should be considered NSFW + * @param {string} [reason] Reason for changing the channel's NSFW flag + * @returns {Promise<this>} + */ + setNSFW(nsfw = true, reason) { + return this.edit({ nsfw, reason }); + } + + static applyToClass(structure, full = false, ignore = []) { + const props = ['send']; + if (full) { + props.push( + 'lastMessage', + 'lastPinAt', + 'bulkDelete', + 'sendTyping', + 'createMessageCollector', + 'awaitMessages', + 'createMessageComponentCollector', + 'awaitMessageComponent', + 'fetchWebhooks', + 'createWebhook', + 'setRateLimitPerUser', + 'setNSFW', + ); + } + for (const prop of props) { + if (ignore.includes(prop)) continue; + Object.defineProperty( + structure.prototype, + prop, + Object.getOwnPropertyDescriptor(TextBasedChannel.prototype, prop), + ); + } + } +} + +module.exports = TextBasedChannel; + +// Fixes Circular +// eslint-disable-next-line import/order +const GuildMessageManager = require('../../managers/GuildMessageManager'); |