diff --git a/src/structures/ApplicationCommand.js b/src/structures/ApplicationCommand.js
index c38eace..3a293d8 100644
--- a/src/structures/ApplicationCommand.js
+++ b/src/structures/ApplicationCommand.js
@@ -1,628 +1,628 @@
-'use strict';
-
-const Base = require('./Base');
-const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager');
-const {
- ApplicationCommandOptionTypes,
- ApplicationCommandTypes,
- ChannelTypes,
-} = require('../util/Constants');
-const SnowflakeUtil = require('../util/SnowflakeUtil');
-const { Message } = require('discord.js');
-
-/**
- * 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,
- this.applicationId,
- );
-
- /**
- * The type of this application command
- * @type {ApplicationCommandType}
- */
- this.type = ApplicationCommandTypes[data.type];
-
- this.user = client.users.cache.get(this.applicationId);
-
- this._patch(data);
- }
-
- _patch(data) {
- if ('name' in data) {
- /**
- * The name of this command
- * @type {string}
- */
- this.name = data.name;
- }
-
- if ('description' in data) {
- /**
- * The description of this command
- * @type {string}
- */
- this.description = data.description;
- }
-
- 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_permission' in data) {
- /**
- * Whether the command is enabled by default when the app is added to a guild
- * @type {boolean}
- */
- this.defaultPermission = data.default_permission;
- }
-
- 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 SnowflakeUtil.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
- * @property {string} description The description of the command
- * @property {ApplicationCommandType} [type] The type of the command
- * @property {ApplicationCommandOptionData[]} [options] Options for the command
- * @property {boolean} [defaultPermission] Whether the command is enabled by default when the app is added to a guild
- */
-
- /**
- * An option for an application command or subcommand.
- * 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`.
- * Note that providing a value for the `camelCase` counterpart for any `snake_case` property
- * will discard the provided `snake_case` property.
- * @typedef {Object} ApplicationCommandOptionData
- * @property {ApplicationCommandOptionType|number} type The type of the option
- * @property {string} name The name of the option
- * @property {string} description The description of the option
- * @property {boolean} [autocomplete] Whether the option is an autocomplete option
- * @property {boolean} [required] Whether the option is required
- * @property {ApplicationCommandOptionChoice[]} [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[]|number[]} [channelTypes] When the option type is channel,
- * the allowed types of channels that can be selected
- * @property {number} [minValue] The minimum value for an `INTEGER` or `NUMBER` option
- * @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option
- */
-
- /**
- * Edits this application command.
- * @param {ApplicationCommandData} data The data to update the command with
- * @returns {Promise}
- * @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}
- */
- setName(name) {
- return this.edit({ name });
- }
-
- /**
- * Edits the description of this ApplicationCommand
- * @param {string} description The new description of the command
- * @returns {Promise}
- */
- setDescription(description) {
- return this.edit({ description });
- }
-
- /**
- * Edits the default permission of this ApplicationCommand
- * @param {boolean} [defaultPermission=true] The default permission for this command
- * @returns {Promise}
- */
- setDefaultPermission(defaultPermission = true) {
- return this.edit({ defaultPermission });
- }
-
- /**
- * Edits the options of this ApplicationCommand
- * @param {ApplicationCommandOptionData[]} options The options to set for this command
- * @returns {Promise}
- */
- setOptions(options) {
- return this.edit({ options });
- }
-
- /**
- * Deletes this command.
- * @returns {Promise}
- * @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 The client may not always respect this ordering!
- * @returns {boolean}
- */
- equals(command, enforceOptionOrder = false) {
- // If given an id, check if the id matches
- if (command.id && this.id !== command.id) return false;
-
- // Check top level parameters
- const commandType =
- typeof command.type === 'string'
- ? command.type
- : ApplicationCommandTypes[command.type];
- if (
- command.name !== this.name ||
- ('description' in command && command.description !== this.description) ||
- ('version' in command && command.version !== this.version) ||
- ('autocomplete' in command &&
- command.autocomplete !== this.autocomplete) ||
- (commandType && commandType !== this.type) ||
- // Future proof for options being nullable
- // TODO: remove ?? 0 on each when nullable
- (command.options?.length ?? 0) !== (this.options?.length ?? 0) ||
- (command.defaultPermission ?? command.default_permission ?? true) !==
- this.defaultPermission
- ) {
- 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 The client may not always respect this ordering!
- * @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 The client may not always respect this ordering!
- * @returns {boolean}
- * @private
- */
- static _optionEquals(existing, option, enforceOptionOrder = false) {
- const optionType =
- typeof option.type === 'string'
- ? option.type
- : ApplicationCommandOptionTypes[option.type];
- if (
- option.name !== existing.name ||
- optionType !== existing.type ||
- option.description !== existing.description ||
- option.autocomplete !== existing.autocomplete ||
- (option.required ??
- (['SUB_COMMAND', 'SUB_COMMAND_GROUP'].includes(optionType)
- ? 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
- ) {
- return false;
- }
-
- if (existing.choices) {
- if (
- enforceOptionOrder &&
- !existing.choices.every(
- (choice, index) =>
- choice.name === option.choices[index].name &&
- choice.value === option.choices[index].value,
- )
- ) {
- 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).map(
- (type) => (typeof type === 'number' ? ChannelTypes[type] : type),
- );
- 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 {string} description The description of the option
- * @property {boolean} [required] Whether the option is required
- * @property {boolean} [autocomplete] Whether the option is an autocomplete 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 {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 `INTEGER` or `NUMBER` option
- * @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option
- */
-
- /**
- * A choice for an application command option.
- * @typedef {Object} ApplicationCommandOptionChoice
- * @property {string} name The name of the 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} option The option to transform
- * @param {boolean} [received] Whether this option has been received from Discord
- * @returns {APIApplicationCommandOption}
- * @private
- */
- static transformOption(option, received) {
- const stringType =
- typeof option.type === 'string'
- ? option.type
- : ApplicationCommandOptionTypes[option.type];
- const channelTypesKey = received ? 'channelTypes' : 'channel_types';
- const minValueKey = received ? 'minValue' : 'min_value';
- const maxValueKey = received ? 'maxValue' : 'max_value';
- return {
- type:
- typeof option.type === 'number' && !received
- ? option.type
- : ApplicationCommandOptionTypes[option.type],
- name: option.name,
- description: option.description,
- required:
- option.required ??
- (stringType === 'SUB_COMMAND' || stringType === 'SUB_COMMAND_GROUP'
- ? undefined
- : false),
- autocomplete: option.autocomplete,
- choices: option.choices,
- options: option.options?.map((o) => this.transformOption(o, received)),
- [channelTypesKey]: received
- ? option.channel_types?.map((type) => ChannelTypes[type])
- : option.channelTypes?.map((type) =>
- typeof type === 'string' ? ChannelTypes[type] : type,
- ) ??
- // When transforming to API data, accept API data
- option.channel_types,
- [minValueKey]: option.minValue ?? option.min_value,
- [maxValueKey]: option.maxValue ?? option.max_value,
- };
- }
- /**
- * Send Slash command to channel
- * @param {Message} message Discord Message
- * @param {Array} options The options to Slash Command
- * @returns {Promise}
- * @example
- * const botID = '12345678987654321'
- * const user = await client.users.fetch(botID);
- * const application = await user.applications.fetch();
- * const command = application.commands.first();
- * await command.sendSlashCommand(messsage, ['option1', 'option2']);
- */
- async sendSlashCommand(message, options = []) {
- // Check Options
- if (!message instanceof Message)
- throw new TypeError('The message must be a Discord.Message');
- if (!Array.isArray(options))
- throw new TypeError('The options must be an array of strings');
- if (this.type !== 'CHAT_INPUT') return false;
- const optionFormat = [];
- let option_ = [];
- let i = 0;
- // Check Command type is Sub group ?
- const subCommandCheck = this.options.some((option) =>
- ['SUB_COMMAND', 'SUB_COMMAND_GROUP'].includes(option.type),
- );
- let subCommand;
- if (subCommandCheck) {
- subCommand = this.options.find((option) => option.name == options[0]);
- options.shift();
- option_[0] = {
- type: ApplicationCommandOptionTypes[subCommand.type],
- name: subCommand.name,
- options: optionFormat,
- };
- } else {
- option_ = optionFormat;
- }
- for (i; i < options.length; i++) {
- const value = options[i];
- if (typeof value !== 'string') {
- throw new TypeError(
- `Expected option to be a String, got ${typeof value}. If you type Number, please convert to String.`,
- );
- }
- if (!subCommandCheck && !this?.options[i]) continue;
- if (subCommandCheck && subCommand?.options && !subCommand?.options[i]) continue;
- if (!subCommandCheck) {
- // Check value is invalid
- let choice;
- if (this.options[i].choices && this.options[i].choices.length > 0) {
- choice =
- this.options[i].choices.find((c) => c.name == value) ||
- this.options[i].choices.find((c) => c.value == value);
- if (!choice) {
- throw new Error(
- `Invalid option: ${value} is not a valid choice for this option\nList of choices: ${this.options[
- i
- ].choices
- .map((c, i) => `#${i + 1} Name: ${c.name} Value: ${c.value}\n`)
- .join('')}`,
- );
- }
- }
- const data = {
- type: ApplicationCommandOptionTypes[this.options[i].type],
- name: this.options[i].name,
- value: choice?.value || (this.options[i].type == 'INTEGER' ? Number(value) : (this.options[i].type == 'BOOLEAN' ? Boolean(value) : value)),
- };
- optionFormat.push(data);
- } else {
- // First element is sub command and removed
- if (!value) continue;
- // Check value is invalid
- let choice;
- if (
- subCommand?.options &&
- subCommand.options[i].choices &&
- subCommand.options[i].choices.length > 0
- ) {
- choice =
- subCommand.options[i].choices.find((c) => c.name == value) ||
- subCommand.options[i].choices.find((c) => c.value == value);
- if (!choice) {
- throw new Error(
- `Invalid option: ${value} is not a valid choice for this option\nList of choices: \n${subCommand.options[
- i
- ].choices.map(
- (c, i) => `#${i + 1} Name: ${c.name} Value: ${c.value}\n`,
- ).join('')}`,
- );
- }
- }
- const data = {
- type: ApplicationCommandOptionTypes[subCommand.options[i].type],
- name: subCommand.options[i].name,
- value:
- choice?.value ||
- (subCommand.options[i].type == 'INTEGER'
- ? Number(value)
- : subCommand.options[i].type == 'BOOLEAN'
- ? Boolean(value)
- : value),
- };
- optionFormat.push(data);
- }
- }
- if (!subCommandCheck && this.options[i]?.required)
- throw new Error('Value required missing');
- if (
- subCommandCheck &&
- subCommand?.options &&
- subCommand?.options[i]?.required
- )
- throw new Error('Value required missing');
- await this.client.api.interactions.post({
- body: {
- type: 2, // ???
- application_id: this.applicationId,
- guild_id: message.guildId,
- channel_id: message.channelId,
- session_id: this.client.session_id,
- data: {
- // ApplicationCommandData
- version: this.version,
- id: this.id,
- name: this.name,
- type: ApplicationCommandTypes[this.type],
- options: option_,
- },
- },
- });
- return true;
- }
- /**
- * Message Context Menu
- * @param {Message} message Discord Message
- * @returns {Promise}
- * @example
- * const botID = '12345678987654321'
- * const user = await client.users.fetch(botID);
- * const application = await user.applications.fetch();
- * const command = application.commands.first();
- * await command.sendContextMenu(messsage);
- */
- async sendContextMenu(message, sendFromMessage = false) {
- if (!message instanceof Message && !sendFromMessage)
- throw new TypeError('The message must be a Discord.Message');
- if (this.type == 'CHAT_INPUT') return false;
- await this.client.api.interactions.post({
- body: {
- type: 2, // ???
- application_id: this.applicationId,
- guild_id: message.guildId,
- channel_id: message.channelId,
- session_id: this.client.session_id,
- data: {
- // ApplicationCommandData
- version: this.version,
- id: this.id,
- name: this.name,
- type: ApplicationCommandTypes[this.type],
- target_id:
- ApplicationCommandTypes[this.type] == 1
- ? message.author.id
- : message.id,
- },
- },
- });
- return true;
- }
-}
-
-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}
- */
+'use strict';
+
+const Base = require('./Base');
+const ApplicationCommandPermissionsManager = require('../managers/ApplicationCommandPermissionsManager');
+const {
+ ApplicationCommandOptionTypes,
+ ApplicationCommandTypes,
+ ChannelTypes,
+} = require('../util/Constants');
+const SnowflakeUtil = require('../util/SnowflakeUtil');
+const { Message } = require('discord.js');
+
+/**
+ * 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,
+ this.applicationId,
+ );
+
+ /**
+ * The type of this application command
+ * @type {ApplicationCommandType}
+ */
+ this.type = ApplicationCommandTypes[data.type];
+
+ this.user = client.users.cache.get(this.applicationId);
+
+ this._patch(data);
+ }
+
+ _patch(data) {
+ if ('name' in data) {
+ /**
+ * The name of this command
+ * @type {string}
+ */
+ this.name = data.name;
+ }
+
+ if ('description' in data) {
+ /**
+ * The description of this command
+ * @type {string}
+ */
+ this.description = data.description;
+ }
+
+ 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_permission' in data) {
+ /**
+ * Whether the command is enabled by default when the app is added to a guild
+ * @type {boolean}
+ */
+ this.defaultPermission = data.default_permission;
+ }
+
+ 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 SnowflakeUtil.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
+ * @property {string} description The description of the command
+ * @property {ApplicationCommandType} [type] The type of the command
+ * @property {ApplicationCommandOptionData[]} [options] Options for the command
+ * @property {boolean} [defaultPermission] Whether the command is enabled by default when the app is added to a guild
+ */
+
+ /**
+ * An option for an application command or subcommand.
+ * 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`.
+ * Note that providing a value for the `camelCase` counterpart for any `snake_case` property
+ * will discard the provided `snake_case` property.
+ * @typedef {Object} ApplicationCommandOptionData
+ * @property {ApplicationCommandOptionType|number} type The type of the option
+ * @property {string} name The name of the option
+ * @property {string} description The description of the option
+ * @property {boolean} [autocomplete] Whether the option is an autocomplete option
+ * @property {boolean} [required] Whether the option is required
+ * @property {ApplicationCommandOptionChoice[]} [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[]|number[]} [channelTypes] When the option type is channel,
+ * the allowed types of channels that can be selected
+ * @property {number} [minValue] The minimum value for an `INTEGER` or `NUMBER` option
+ * @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option
+ */
+
+ /**
+ * Edits this application command.
+ * @param {ApplicationCommandData} data The data to update the command with
+ * @returns {Promise}
+ * @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}
+ */
+ setName(name) {
+ return this.edit({ name });
+ }
+
+ /**
+ * Edits the description of this ApplicationCommand
+ * @param {string} description The new description of the command
+ * @returns {Promise}
+ */
+ setDescription(description) {
+ return this.edit({ description });
+ }
+
+ /**
+ * Edits the default permission of this ApplicationCommand
+ * @param {boolean} [defaultPermission=true] The default permission for this command
+ * @returns {Promise}
+ */
+ setDefaultPermission(defaultPermission = true) {
+ return this.edit({ defaultPermission });
+ }
+
+ /**
+ * Edits the options of this ApplicationCommand
+ * @param {ApplicationCommandOptionData[]} options The options to set for this command
+ * @returns {Promise}
+ */
+ setOptions(options) {
+ return this.edit({ options });
+ }
+
+ /**
+ * Deletes this command.
+ * @returns {Promise}
+ * @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 The client may not always respect this ordering!
+ * @returns {boolean}
+ */
+ equals(command, enforceOptionOrder = false) {
+ // If given an id, check if the id matches
+ if (command.id && this.id !== command.id) return false;
+
+ // Check top level parameters
+ const commandType =
+ typeof command.type === 'string'
+ ? command.type
+ : ApplicationCommandTypes[command.type];
+ if (
+ command.name !== this.name ||
+ ('description' in command && command.description !== this.description) ||
+ ('version' in command && command.version !== this.version) ||
+ ('autocomplete' in command &&
+ command.autocomplete !== this.autocomplete) ||
+ (commandType && commandType !== this.type) ||
+ // Future proof for options being nullable
+ // TODO: remove ?? 0 on each when nullable
+ (command.options?.length ?? 0) !== (this.options?.length ?? 0) ||
+ (command.defaultPermission ?? command.default_permission ?? true) !==
+ this.defaultPermission
+ ) {
+ 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 The client may not always respect this ordering!
+ * @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 The client may not always respect this ordering!
+ * @returns {boolean}
+ * @private
+ */
+ static _optionEquals(existing, option, enforceOptionOrder = false) {
+ const optionType =
+ typeof option.type === 'string'
+ ? option.type
+ : ApplicationCommandOptionTypes[option.type];
+ if (
+ option.name !== existing.name ||
+ optionType !== existing.type ||
+ option.description !== existing.description ||
+ option.autocomplete !== existing.autocomplete ||
+ (option.required ??
+ (['SUB_COMMAND', 'SUB_COMMAND_GROUP'].includes(optionType)
+ ? 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
+ ) {
+ return false;
+ }
+
+ if (existing.choices) {
+ if (
+ enforceOptionOrder &&
+ !existing.choices.every(
+ (choice, index) =>
+ choice.name === option.choices[index].name &&
+ choice.value === option.choices[index].value,
+ )
+ ) {
+ 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).map(
+ (type) => (typeof type === 'number' ? ChannelTypes[type] : type),
+ );
+ 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 {string} description The description of the option
+ * @property {boolean} [required] Whether the option is required
+ * @property {boolean} [autocomplete] Whether the option is an autocomplete 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 {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 `INTEGER` or `NUMBER` option
+ * @property {number} [maxValue] The maximum value for an `INTEGER` or `NUMBER` option
+ */
+
+ /**
+ * A choice for an application command option.
+ * @typedef {Object} ApplicationCommandOptionChoice
+ * @property {string} name The name of the 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} option The option to transform
+ * @param {boolean} [received] Whether this option has been received from Discord
+ * @returns {APIApplicationCommandOption}
+ * @private
+ */
+ static transformOption(option, received) {
+ const stringType =
+ typeof option.type === 'string'
+ ? option.type
+ : ApplicationCommandOptionTypes[option.type];
+ const channelTypesKey = received ? 'channelTypes' : 'channel_types';
+ const minValueKey = received ? 'minValue' : 'min_value';
+ const maxValueKey = received ? 'maxValue' : 'max_value';
+ return {
+ type:
+ typeof option.type === 'number' && !received
+ ? option.type
+ : ApplicationCommandOptionTypes[option.type],
+ name: option.name,
+ description: option.description,
+ required:
+ option.required ??
+ (stringType === 'SUB_COMMAND' || stringType === 'SUB_COMMAND_GROUP'
+ ? undefined
+ : false),
+ autocomplete: option.autocomplete,
+ choices: option.choices,
+ options: option.options?.map((o) => this.transformOption(o, received)),
+ [channelTypesKey]: received
+ ? option.channel_types?.map((type) => ChannelTypes[type])
+ : option.channelTypes?.map((type) =>
+ typeof type === 'string' ? ChannelTypes[type] : type,
+ ) ??
+ // When transforming to API data, accept API data
+ option.channel_types,
+ [minValueKey]: option.minValue ?? option.min_value,
+ [maxValueKey]: option.maxValue ?? option.max_value,
+ };
+ }
+ /**
+ * Send Slash command to channel
+ * @param {Message} message Discord Message
+ * @param {Array} options The options to Slash Command
+ * @returns {Promise}
+ * @example
+ * const botID = '12345678987654321'
+ * const user = await client.users.fetch(botID);
+ * const application = await user.applications.fetch();
+ * const command = application.commands.first();
+ * await command.sendSlashCommand(messsage, ['option1', 'option2']);
+ */
+ async sendSlashCommand(message, options = []) {
+ // Check Options
+ if (!message instanceof Message)
+ throw new TypeError('The message must be a Discord.Message');
+ if (!Array.isArray(options))
+ throw new TypeError('The options must be an array of strings');
+ if (this.type !== 'CHAT_INPUT') return false;
+ const optionFormat = [];
+ let option_ = [];
+ let i = 0;
+ // Check Command type is Sub group ?
+ const subCommandCheck = this.options.some((option) =>
+ ['SUB_COMMAND', 'SUB_COMMAND_GROUP'].includes(option.type),
+ );
+ let subCommand;
+ if (subCommandCheck) {
+ subCommand = this.options.find((option) => option.name == options[0]);
+ options.shift();
+ option_[0] = {
+ type: ApplicationCommandOptionTypes[subCommand.type],
+ name: subCommand.name,
+ options: optionFormat,
+ };
+ } else {
+ option_ = optionFormat;
+ }
+ for (i; i < options.length; i++) {
+ const value = options[i];
+ if (typeof value !== 'string') {
+ throw new TypeError(
+ `Expected option to be a String, got ${typeof value}. If you type Number, please convert to String.`,
+ );
+ }
+ if (!subCommandCheck && !this?.options[i]) continue;
+ if (subCommandCheck && subCommand?.options && !subCommand?.options[i]) continue;
+ if (!subCommandCheck) {
+ // Check value is invalid
+ let choice;
+ if (this.options[i].choices && this.options[i].choices.length > 0) {
+ choice =
+ this.options[i].choices.find((c) => c.name == value) ||
+ this.options[i].choices.find((c) => c.value == value);
+ if (!choice) {
+ throw new Error(
+ `Invalid option: ${value} is not a valid choice for this option\nList of choices: ${this.options[
+ i
+ ].choices
+ .map((c, i) => `#${i + 1} Name: ${c.name} Value: ${c.value}\n`)
+ .join('')}`,
+ );
+ }
+ }
+ const data = {
+ type: ApplicationCommandOptionTypes[this.options[i].type],
+ name: this.options[i].name,
+ value: choice?.value || (this.options[i].type == 'INTEGER' ? Number(value) : (this.options[i].type == 'BOOLEAN' ? Boolean(value) : value)),
+ };
+ optionFormat.push(data);
+ } else {
+ // First element is sub command and removed
+ if (!value) continue;
+ // Check value is invalid
+ let choice;
+ if (
+ subCommand?.options &&
+ subCommand.options[i].choices &&
+ subCommand.options[i].choices.length > 0
+ ) {
+ choice =
+ subCommand.options[i].choices.find((c) => c.name == value) ||
+ subCommand.options[i].choices.find((c) => c.value == value);
+ if (!choice) {
+ throw new Error(
+ `Invalid option: ${value} is not a valid choice for this option\nList of choices: \n${subCommand.options[
+ i
+ ].choices.map(
+ (c, i) => `#${i + 1} Name: ${c.name} Value: ${c.value}\n`,
+ ).join('')}`,
+ );
+ }
+ }
+ const data = {
+ type: ApplicationCommandOptionTypes[subCommand.options[i].type],
+ name: subCommand.options[i].name,
+ value:
+ choice?.value ||
+ (subCommand.options[i].type == 'INTEGER'
+ ? Number(value)
+ : subCommand.options[i].type == 'BOOLEAN'
+ ? Boolean(value)
+ : value),
+ };
+ optionFormat.push(data);
+ }
+ }
+ if (!subCommandCheck && this.options[i]?.required)
+ throw new Error('Value required missing');
+ if (
+ subCommandCheck &&
+ subCommand?.options &&
+ subCommand?.options[i]?.required
+ )
+ throw new Error('Value required missing');
+ await this.client.api.interactions.post({
+ body: {
+ type: 2, // ???
+ application_id: this.applicationId,
+ guild_id: message.guildId,
+ channel_id: message.channelId,
+ session_id: this.client.session_id,
+ data: {
+ // ApplicationCommandData
+ version: this.version,
+ id: this.id,
+ name: this.name,
+ type: ApplicationCommandTypes[this.type],
+ options: option_,
+ },
+ },
+ });
+ return true;
+ }
+ /**
+ * Message Context Menu
+ * @param {Message} message Discord Message
+ * @returns {Promise}
+ * @example
+ * const botID = '12345678987654321'
+ * const user = await client.users.fetch(botID);
+ * const application = await user.applications.fetch();
+ * const command = application.commands.first();
+ * await command.sendContextMenu(messsage);
+ */
+ async sendContextMenu(message, sendFromMessage = false) {
+ if (!message instanceof Message && !sendFromMessage)
+ throw new TypeError('The message must be a Discord.Message');
+ if (this.type == 'CHAT_INPUT') return false;
+ await this.client.api.interactions.post({
+ body: {
+ type: 2, // ???
+ application_id: this.applicationId,
+ guild_id: message.guildId,
+ channel_id: message.channelId,
+ session_id: this.client.session_id,
+ data: {
+ // ApplicationCommandData
+ version: this.version,
+ id: this.id,
+ name: this.name,
+ type: ApplicationCommandTypes[this.type],
+ target_id:
+ ApplicationCommandTypes[this.type] == 1
+ ? message.author.id
+ : message.id,
+ },
+ },
+ });
+ return true;
+ }
+}
+
+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}
+ */
diff --git a/src/structures/BaseMessageComponent.js b/src/structures/BaseMessageComponent.js
index c2470e0..38c5ead 100644
--- a/src/structures/BaseMessageComponent.js
+++ b/src/structures/BaseMessageComponent.js
@@ -1,103 +1,103 @@
-'use strict';
-
-const { TypeError } = require('../errors');
-const { MessageComponentTypes, Events } = require('../util/Constants');
-
-/**
- * Represents an interactive component of a Message. It should not be necessary to construct this directly.
- * See {@link MessageComponent}
- */
-class BaseMessageComponent {
- /**
- * Options for a BaseMessageComponent
- * @typedef {Object} BaseMessageComponentOptions
- * @property {MessageComponentTypeResolvable} type The type of this component
- */
-
- /**
- * Data that can be resolved into options for a MessageComponent. This can be:
- * * MessageActionRowOptions
- * * MessageButtonOptions
- * * MessageSelectMenuOptions
- * @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions
- */
-
- /**
- * Components that can be sent in a message. These can be:
- * * MessageActionRow
- * * MessageButton
- * * MessageSelectMenu
- * @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent
- * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types}
- */
-
- /**
- * Data that can be resolved to a MessageComponentType. This can be:
- * * MessageComponentType
- * * string
- * * number
- * @typedef {string|number|MessageComponentType} MessageComponentTypeResolvable
- */
-
- /**
- * @param {BaseMessageComponent|BaseMessageComponentOptions} [data={}] The options for this component
- */
- constructor(data) {
- /**
- * The type of this component
- * @type {?MessageComponentType}
- */
- this.type = 'type' in data ? BaseMessageComponent.resolveType(data.type) : null;
- }
-
- /**
- * Constructs a MessageComponent based on the type of the incoming data
- * @param {MessageComponentOptions} data Data for a MessageComponent
- * @param {Client|WebhookClient} [client] Client constructing this component
- * @returns {?MessageComponent}
- * @private
- */
- static create(data, client) {
- let component;
- let type = data.type;
-
- if (typeof type === 'string') type = MessageComponentTypes[type];
-
- switch (type) {
- case MessageComponentTypes.ACTION_ROW: {
- const MessageActionRow = require('./MessageActionRow');
- component = data instanceof MessageActionRow ? data : new MessageActionRow(data, client);
- break;
- }
- case MessageComponentTypes.BUTTON: {
- const MessageButton = require('./MessageButton');
- component = data instanceof MessageButton ? data : new MessageButton(data);
- break;
- }
- case MessageComponentTypes.SELECT_MENU: {
- const MessageSelectMenu = require('./MessageSelectMenu');
- component = data instanceof MessageSelectMenu ? data : new MessageSelectMenu(data);
- break;
- }
- default:
- if (client) {
- client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`);
- } else {
- throw new TypeError('INVALID_TYPE', 'data.type', 'valid MessageComponentType');
- }
- }
- return component;
- }
-
- /**
- * Resolves the type of a MessageComponent
- * @param {MessageComponentTypeResolvable} type The type to resolve
- * @returns {MessageComponentType}
- * @private
- */
- static resolveType(type) {
- return typeof type === 'string' ? type : MessageComponentTypes[type];
- }
-}
-
-module.exports = BaseMessageComponent;
+'use strict';
+
+const { TypeError } = require('../errors');
+const { MessageComponentTypes, Events } = require('../util/Constants');
+
+/**
+ * Represents an interactive component of a Message. It should not be necessary to construct this directly.
+ * See {@link MessageComponent}
+ */
+class BaseMessageComponent {
+ /**
+ * Options for a BaseMessageComponent
+ * @typedef {Object} BaseMessageComponentOptions
+ * @property {MessageComponentTypeResolvable} type The type of this component
+ */
+
+ /**
+ * Data that can be resolved into options for a MessageComponent. This can be:
+ * * MessageActionRowOptions
+ * * MessageButtonOptions
+ * * MessageSelectMenuOptions
+ * @typedef {MessageActionRowOptions|MessageButtonOptions|MessageSelectMenuOptions} MessageComponentOptions
+ */
+
+ /**
+ * Components that can be sent in a message. These can be:
+ * * MessageActionRow
+ * * MessageButton
+ * * MessageSelectMenu
+ * @typedef {MessageActionRow|MessageButton|MessageSelectMenu} MessageComponent
+ * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object-component-types}
+ */
+
+ /**
+ * Data that can be resolved to a MessageComponentType. This can be:
+ * * MessageComponentType
+ * * string
+ * * number
+ * @typedef {string|number|MessageComponentType} MessageComponentTypeResolvable
+ */
+
+ /**
+ * @param {BaseMessageComponent|BaseMessageComponentOptions} [data={}] The options for this component
+ */
+ constructor(data) {
+ /**
+ * The type of this component
+ * @type {?MessageComponentType}
+ */
+ this.type = 'type' in data ? BaseMessageComponent.resolveType(data.type) : null;
+ }
+
+ /**
+ * Constructs a MessageComponent based on the type of the incoming data
+ * @param {MessageComponentOptions} data Data for a MessageComponent
+ * @param {Client|WebhookClient} [client] Client constructing this component
+ * @returns {?MessageComponent}
+ * @private
+ */
+ static create(data, client) {
+ let component;
+ let type = data.type;
+
+ if (typeof type === 'string') type = MessageComponentTypes[type];
+
+ switch (type) {
+ case MessageComponentTypes.ACTION_ROW: {
+ const MessageActionRow = require('./MessageActionRow');
+ component = data instanceof MessageActionRow ? data : new MessageActionRow(data, client);
+ break;
+ }
+ case MessageComponentTypes.BUTTON: {
+ const MessageButton = require('./MessageButton');
+ component = data instanceof MessageButton ? data : new MessageButton(data);
+ break;
+ }
+ case MessageComponentTypes.SELECT_MENU: {
+ const MessageSelectMenu = require('./MessageSelectMenu');
+ component = data instanceof MessageSelectMenu ? data : new MessageSelectMenu(data);
+ break;
+ }
+ default:
+ if (client) {
+ client.emit(Events.DEBUG, `[BaseMessageComponent] Received component with unknown type: ${data.type}`);
+ } else {
+ throw new TypeError('INVALID_TYPE', 'data.type', 'valid MessageComponentType');
+ }
+ }
+ return component;
+ }
+
+ /**
+ * Resolves the type of a MessageComponent
+ * @param {MessageComponentTypeResolvable} type The type to resolve
+ * @returns {MessageComponentType}
+ * @private
+ */
+ static resolveType(type) {
+ return typeof type === 'string' ? type : MessageComponentTypes[type];
+ }
+}
+
+module.exports = BaseMessageComponent;
diff --git a/src/structures/Channel.js b/src/structures/Channel.js
index 0448c25..f7e2ea1 100644
--- a/src/structures/Channel.js
+++ b/src/structures/Channel.js
@@ -1,275 +1,275 @@
-'use strict';
-
-const process = require('node:process');
-const Base = require('./Base');
-let CategoryChannel;
-let DMChannel;
-let NewsChannel;
-let StageChannel;
-let StoreChannel;
-let TextChannel;
-let ThreadChannel;
-let VoiceChannel;
-const { ChannelTypes, ThreadChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants');
-const SnowflakeUtil = require('../util/SnowflakeUtil');
-const { Message } = require('discord.js');
-const { ApplicationCommand } = require('discord.js-selfbot-v13');
-
-/**
- * @type {WeakSet}
- * @private
- * @internal
- */
-const deletedChannels = new WeakSet();
-let deprecationEmittedForDeleted = false;
-
-/**
- * Represents any channel on Discord.
- * @extends {Base}
- * @abstract
- */
-class Channel extends Base {
- constructor(client, data, immediatePatch = true) {
- super(client);
-
- const type = ChannelTypes[data?.type];
- /**
- * The type of the channel
- * @type {ChannelType}
- */
- this.type = type ?? 'UNKNOWN';
-
- if (data && immediatePatch) this._patch(data);
- }
-
- _patch(data) {
- /**
- * The channel's id
- * @type {Snowflake}
- */
- this.id = data.id;
- }
-
- /**
- * The timestamp the channel was created at
- * @type {number}
- * @readonly
- */
- get createdTimestamp() {
- return SnowflakeUtil.timestampFrom(this.id);
- }
-
- /**
- * The time the channel was created at
- * @type {Date}
- * @readonly
- */
- get createdAt() {
- return new Date(this.createdTimestamp);
- }
-
- /**
- * Whether or not the structure has been deleted
- * @type {boolean}
- * @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091
- */
- get deleted() {
- if (!deprecationEmittedForDeleted) {
- deprecationEmittedForDeleted = true;
- process.emitWarning(
- 'Channel#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
- 'DeprecationWarning',
- );
- }
-
- return deletedChannels.has(this);
- }
-
- set deleted(value) {
- if (!deprecationEmittedForDeleted) {
- deprecationEmittedForDeleted = true;
- process.emitWarning(
- 'Channel#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
- 'DeprecationWarning',
- );
- }
-
- if (value) deletedChannels.add(this);
- else deletedChannels.delete(this);
- }
-
- /**
- * Whether this Channel is a partial
- * This is always false outside of DM channels.
- * @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}
- * @example
- * // Delete the channel
- * channel.delete()
- * .then(console.log)
- * .catch(console.error);
- */
- async delete() {
- await this.client.api.channels(this.id).delete();
- return this;
- }
-
- /**
- * Fetches this channel.
- * @param {boolean} [force=true] Whether to skip the cache check and request the API
- * @returns {Promise}
- */
- fetch(force = true) {
- return this.client.channels.fetch(this.id, { force });
- }
-
- /**
- * Indicates whether this channel is {@link TextBasedChannels text-based}.
- * @returns {boolean}
- */
- isText() {
- return 'messages' in this;
- }
-
- /**
- * Indicates whether this channel is {@link BaseGuildVoiceChannel voice-based}.
- * @returns {boolean}
- */
- isVoice() {
- return VoiceBasedChannelTypes.includes(this.type);
- }
-
- /**
- * Indicates whether this channel is a {@link ThreadChannel}.
- * @returns {boolean}
- */
- isThread() {
- return ThreadChannelTypes.includes(this.type);
- }
-
- static create(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) {
- CategoryChannel ??= require('./CategoryChannel');
- DMChannel ??= require('./DMChannel');
- NewsChannel ??= require('./NewsChannel');
- StageChannel ??= require('./StageChannel');
- StoreChannel ??= require('./StoreChannel');
- TextChannel ??= require('./TextChannel');
- ThreadChannel ??= require('./ThreadChannel');
- VoiceChannel ??= require('./VoiceChannel');
-
- let channel;
- if (!data.guild_id && !guild) {
- if ((data.recipients && data.type !== ChannelTypes.GROUP_DM) || data.type === ChannelTypes.DM) {
- channel = new DMChannel(client, data);
- } else if (data.type === ChannelTypes.GROUP_DM) {
- const PartialGroupDMChannel = require('./PartialGroupDMChannel');
- channel = new PartialGroupDMChannel(client, data);
- }
- } else {
- guild ??= client.guilds.cache.get(data.guild_id);
-
- if (guild || allowUnknownGuild) {
- switch (data.type) {
- case ChannelTypes.GUILD_TEXT: {
- channel = new TextChannel(guild, data, client);
- break;
- }
- case ChannelTypes.GUILD_VOICE: {
- channel = new VoiceChannel(guild, data, client);
- break;
- }
- case ChannelTypes.GUILD_CATEGORY: {
- channel = new CategoryChannel(guild, data, client);
- break;
- }
- case ChannelTypes.GUILD_NEWS: {
- channel = new NewsChannel(guild, data, client);
- break;
- }
- case ChannelTypes.GUILD_STORE: {
- channel = new StoreChannel(guild, data, client);
- break;
- }
- case ChannelTypes.GUILD_STAGE_VOICE: {
- channel = new StageChannel(guild, data, client);
- break;
- }
- case ChannelTypes.GUILD_NEWS_THREAD:
- case ChannelTypes.GUILD_PUBLIC_THREAD:
- case ChannelTypes.GUILD_PRIVATE_THREAD: {
- channel = new ThreadChannel(guild, data, client, fromInteraction);
- if (!allowUnknownGuild) channel.parent?.threads.cache.set(channel.id, channel);
- break;
- }
- }
- if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel);
- }
- }
- return channel;
- }
-
- toJSON(...props) {
- return super.toJSON({ createdTimestamp: true }, ...props);
- }
-
- // Send Slash
- /**
- * Send Slash to this channel
- * @param {DiscordBot} botID Bot ID
- * @param {String} commandName Command name
- * @param {Array} args Command arguments
- * @returns {Promise}
- */
- async sendSlash(botID, commandName, args = []) {
- if (!this.isText()) throw new Error('This channel is not text-based.');
- if(!botID) throw new Error('Bot ID is required');
- const user = await this.client.users.fetch(botID).catch(() => {});
- if (!user || !user.bot || !user.applications) throw new Error('BotID is not a bot or does not have an application slash command');
- if (!commandName || typeof commandName !== 'string') throw new Error('Command name is required');
- const listApplication = user.applications.cache.size == 0 ? await user.applications.fetch() : user.applications.cache;
- let slashCommand;
- await Promise.all(listApplication.map(async application => {
- if (commandName == application.name && application.type == 'CHAT_INPUT') slashCommand = application;
- }));
- if (!slashCommand) throw new Error(
- `Command ${commandName} is not found\nList command avalible: ${listApplication.filter(a => a.type == 'CHAT_INPUT').map(a => a.name).join(', ')}`,
- );
- return slashCommand.sendSlashCommand(
- new Message(this.client, {
- channel_id: this.id,
- guild_id: this.guild?.id || null,
- author: this.client.user,
- content: '',
- id: this.client.user.id
- }),
- args
- );
- }
-}
-
-exports.Channel = Channel;
-exports.deletedChannels = deletedChannels;
-
-/**
- * @external APIChannel
- * @see {@link https://discord.com/developers/docs/resources/channel#channel-object}
- */
+'use strict';
+
+const process = require('node:process');
+const Base = require('./Base');
+let CategoryChannel;
+let DMChannel;
+let NewsChannel;
+let StageChannel;
+let StoreChannel;
+let TextChannel;
+let ThreadChannel;
+let VoiceChannel;
+const { ChannelTypes, ThreadChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants');
+const SnowflakeUtil = require('../util/SnowflakeUtil');
+const { Message } = require('discord.js');
+//const { ApplicationCommand } = require('discord.js-selfbot-v13'); - Not being used in this file, not necessary.
+
+/**
+ * @type {WeakSet}
+ * @private
+ * @internal
+ */
+const deletedChannels = new WeakSet();
+let deprecationEmittedForDeleted = false;
+
+/**
+ * Represents any channel on Discord.
+ * @extends {Base}
+ * @abstract
+ */
+class Channel extends Base {
+ constructor(client, data, immediatePatch = true) {
+ super(client);
+
+ const type = ChannelTypes[data?.type];
+ /**
+ * The type of the channel
+ * @type {ChannelType}
+ */
+ this.type = type ?? 'UNKNOWN';
+
+ if (data && immediatePatch) this._patch(data);
+ }
+
+ _patch(data) {
+ /**
+ * The channel's id
+ * @type {Snowflake}
+ */
+ this.id = data.id;
+ }
+
+ /**
+ * The timestamp the channel was created at
+ * @type {number}
+ * @readonly
+ */
+ get createdTimestamp() {
+ return SnowflakeUtil.timestampFrom(this.id);
+ }
+
+ /**
+ * The time the channel was created at
+ * @type {Date}
+ * @readonly
+ */
+ get createdAt() {
+ return new Date(this.createdTimestamp);
+ }
+
+ /**
+ * Whether or not the structure has been deleted
+ * @type {boolean}
+ * @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091
+ */
+ get deleted() {
+ if (!deprecationEmittedForDeleted) {
+ deprecationEmittedForDeleted = true;
+ process.emitWarning(
+ 'Channel#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
+ 'DeprecationWarning',
+ );
+ }
+
+ return deletedChannels.has(this);
+ }
+
+ set deleted(value) {
+ if (!deprecationEmittedForDeleted) {
+ deprecationEmittedForDeleted = true;
+ process.emitWarning(
+ 'Channel#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
+ 'DeprecationWarning',
+ );
+ }
+
+ if (value) deletedChannels.add(this);
+ else deletedChannels.delete(this);
+ }
+
+ /**
+ * Whether this Channel is a partial
+ * This is always false outside of DM channels.
+ * @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}
+ * @example
+ * // Delete the channel
+ * channel.delete()
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async delete() {
+ await this.client.api.channels(this.id).delete();
+ return this;
+ }
+
+ /**
+ * Fetches this channel.
+ * @param {boolean} [force=true] Whether to skip the cache check and request the API
+ * @returns {Promise}
+ */
+ fetch(force = true) {
+ return this.client.channels.fetch(this.id, { force });
+ }
+
+ /**
+ * Indicates whether this channel is {@link TextBasedChannels text-based}.
+ * @returns {boolean}
+ */
+ isText() {
+ return 'messages' in this;
+ }
+
+ /**
+ * Indicates whether this channel is {@link BaseGuildVoiceChannel voice-based}.
+ * @returns {boolean}
+ */
+ isVoice() {
+ return VoiceBasedChannelTypes.includes(this.type);
+ }
+
+ /**
+ * Indicates whether this channel is a {@link ThreadChannel}.
+ * @returns {boolean}
+ */
+ isThread() {
+ return ThreadChannelTypes.includes(this.type);
+ }
+
+ static create(client, data, guild, { allowUnknownGuild, fromInteraction } = {}) {
+ CategoryChannel ??= require('./CategoryChannel');
+ DMChannel ??= require('./DMChannel');
+ NewsChannel ??= require('./NewsChannel');
+ StageChannel ??= require('./StageChannel');
+ StoreChannel ??= require('./StoreChannel');
+ TextChannel ??= require('./TextChannel');
+ ThreadChannel ??= require('./ThreadChannel');
+ VoiceChannel ??= require('./VoiceChannel');
+
+ let channel;
+ if (!data.guild_id && !guild) {
+ if ((data.recipients && data.type !== ChannelTypes.GROUP_DM) || data.type === ChannelTypes.DM) {
+ channel = new DMChannel(client, data);
+ } else if (data.type === ChannelTypes.GROUP_DM) {
+ const PartialGroupDMChannel = require('./PartialGroupDMChannel');
+ channel = new PartialGroupDMChannel(client, data);
+ }
+ } else {
+ guild ??= client.guilds.cache.get(data.guild_id);
+
+ if (guild || allowUnknownGuild) {
+ switch (data.type) {
+ case ChannelTypes.GUILD_TEXT: {
+ channel = new TextChannel(guild, data, client);
+ break;
+ }
+ case ChannelTypes.GUILD_VOICE: {
+ channel = new VoiceChannel(guild, data, client);
+ break;
+ }
+ case ChannelTypes.GUILD_CATEGORY: {
+ channel = new CategoryChannel(guild, data, client);
+ break;
+ }
+ case ChannelTypes.GUILD_NEWS: {
+ channel = new NewsChannel(guild, data, client);
+ break;
+ }
+ case ChannelTypes.GUILD_STORE: {
+ channel = new StoreChannel(guild, data, client);
+ break;
+ }
+ case ChannelTypes.GUILD_STAGE_VOICE: {
+ channel = new StageChannel(guild, data, client);
+ break;
+ }
+ case ChannelTypes.GUILD_NEWS_THREAD:
+ case ChannelTypes.GUILD_PUBLIC_THREAD:
+ case ChannelTypes.GUILD_PRIVATE_THREAD: {
+ channel = new ThreadChannel(guild, data, client, fromInteraction);
+ if (!allowUnknownGuild) channel.parent?.threads.cache.set(channel.id, channel);
+ break;
+ }
+ }
+ if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel);
+ }
+ }
+ return channel;
+ }
+
+ toJSON(...props) {
+ return super.toJSON({ createdTimestamp: true }, ...props);
+ }
+
+ // Send Slash
+ /**
+ * Send Slash to this channel
+ * @param {DiscordBot} botID Bot ID
+ * @param {String} commandName Command name
+ * @param {Array} args Command arguments
+ * @returns {Promise}
+ */
+ async sendSlash(botID, commandName, args = []) {
+ if (!this.isText()) throw new Error('This channel is not text-based.');
+ if(!botID) throw new Error('Bot ID is required');
+ const user = await this.client.users.fetch(botID).catch(() => {});
+ if (!user || !user.bot || !user.applications) throw new Error('BotID is not a bot or does not have an application slash command');
+ if (!commandName || typeof commandName !== 'string') throw new Error('Command name is required');
+ const listApplication = user.applications.cache.size == 0 ? await user.applications.fetch() : user.applications.cache;
+ let slashCommand;
+ await Promise.all(listApplication.map(async application => {
+ if (commandName == application.name && application.type == 'CHAT_INPUT') slashCommand = application;
+ }));
+ if (!slashCommand) throw new Error(
+ `Command ${commandName} is not found\nList command avalible: ${listApplication.filter(a => a.type == 'CHAT_INPUT').map(a => a.name).join(', ')}`,
+ );
+ return slashCommand.sendSlashCommand(
+ new Message(this.client, {
+ channel_id: this.id,
+ guild_id: this.guild?.id || null,
+ author: this.client.user,
+ content: '',
+ id: this.client.user.id
+ }),
+ args
+ );
+ }
+}
+
+exports.Channel = Channel;
+exports.deletedChannels = deletedChannels;
+
+/**
+ * @external APIChannel
+ * @see {@link https://discord.com/developers/docs/resources/channel#channel-object}
+ */
diff --git a/src/structures/ClientUser.js b/src/structures/ClientUser.js
index 67dab7d..9eb195e 100644
--- a/src/structures/ClientUser.js
+++ b/src/structures/ClientUser.js
@@ -1,375 +1,357 @@
-'use strict';
-
-const User = require('./User');
-const DataResolver = require('../util/DataResolver');
-const { HypeSquadOptions } = require('../util/Constants');
-const { Util } = require('..');
-
-/**
- * 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;
-
- // Add (Selfbot)
- if ('premium' in data) this.nitro = data.premium;
- /**
- * Nitro Status
- * `0`: None
- * `1`: Classic
- * `2`: Boost
- * @external
- * https://discord.com/developers/docs/resources/user#user-object-premium-types
- * @type {Number}
- */
- if ('purchased_flags' in data) this.nitroType = data.purchased_flags;
- if ('phone' in data) this.phoneNumber = data.phone;
- if ('nsfw_allowed' in data) this.nsfwAllowed = data.nsfw_allowed;
- if ('email' in data) this.emailAddress = data.email;
- }
-
- /**
- * 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} ClientUserEditData
- * @property {string} [username] The new username
- * @property {?(BufferResolvable|Base64Resolvable)} [avatar] The new avatar
- */
-
- /**
- * Edits the logged in client.
- * @param {ClientUserEditData} data The new data
- * @returns {Promise}
- */
- async edit(data) {
- if (typeof data.avatar !== 'undefined')
- data.avatar = await DataResolver.resolveImage(data.avatar);
- if (typeof data.banner !== 'undefined')
- data.banner = await DataResolver.resolveImage(data.banner);
- const newData = await this.client.api.users('@me').patch({ data });
- this.client.token = newData.token;
- this.client.password = data?.password
- ? data?.password
- : this.client.password;
- const { updated } = this.client.actions.UserUpdate.handle(newData);
- return updated ?? this;
- }
-
- /**
- * Sets the username of the logged in client.
- * Changing usernames in Discord is heavily rate limited, with only 2 requests
- * every hour. Use this sparingly!
- * @param {string} username The new username
- * @param {string} password The password of the account
- * @returns {Promise}
- * @example
- * // Set username
- * client.user.setUsername('discordjs')
- * .then(user => console.log(`My new username is ${user.username}`))
- * .catch(console.error);
- */
- setUsername(username, password) {
- if (!password && !this.client.password)
- throw new Error('A password is required to change a username.');
- return this.edit({
- username,
- password: this.client.password ? this.client.password : password,
- });
- }
-
- /**
- * Sets the avatar of the logged in client.
- * @param {?(BufferResolvable|Base64Resolvable)} avatar The new avatar
- * @returns {Promise}
- * @example
- * // Set avatar
- * client.user.setAvatar('./avatar.png')
- * .then(user => console.log(`New avatar set!`))
- * .catch(console.error);
- */
- setAvatar(avatar) {
- return this.edit({ avatar });
- }
- /**
- * Sets the banner of the logged in client.
- * @param {?(BufferResolvable|Base64Resolvable)} banner The new banner
- * @returns {Promise}
- * @example
- * // Set banner
- * client.user.setBanner('./banner.png')
- * .then(user => console.log(`New banner set!`))
- * .catch(console.error);
- */
- setBanner(banner) {
- if (this.nitroType !== 2)
- throw new Error(
- 'You must be a Nitro Boosted User to change your banner.',
- );
- return this.edit({ banner });
- }
-
- /**
- * Set HyperSquad House
- * @param {HypeSquadOptions} type
- * `LEAVE`: 0
- * `HOUSE_BRAVERY`: 1
- * `HOUSE_BRILLIANCE`: 2
- * `HOUSE_BALANCE`: 3
- * @returns {Promise}
- * @example
- * // Set HyperSquad HOUSE_BRAVERY
- * client.user.setHypeSquad(1); || client.user.setHypeSquad('HOUSE_BRAVERY');
- * // Leave
- * client.user.setHypeSquad(0);
- */
- async setHypeSquad(type) {
- const id = typeof type === 'string' ? HypeSquadOptions[type] : type;
- if (!id && id !== 0) throw new Error('Invalid HypeSquad type.');
- if (id !== 0)
- return await this.client.api.hypesquad.online.post({
- data: { house_id: id },
- });
- else return await this.client.api.hypesquad.online.delete();
- }
-
- /**
- * Set Accent color
- * @param {ColorResolvable} color Color to set
- * @returns {Promise}
- */
- setAccentColor(color = null) {
- return this.edit({ accent_color: color ? Util.resolveColor(color) : null });
- }
-
- /**
- * Set discriminator
- * @param {User.discriminator} discriminator It is #1234
- * @param {string} password The password of the account
- * @returns {Promise}
- */
- setDiscriminator(discriminator, password) {
- if (!this.nitro)
- throw new Error('You must be a Nitro User to change your discriminator.');
- if (!password && !this.client.password)
- throw new Error('A password is required to change a discriminator.');
- return this.edit({
- discriminator,
- password: this.client.password ? this.client.password : password,
- });
- }
-
- /**
- * Set About me
- * @param {String} bio Bio to set
- * @returns {Promise}
- */
- setAboutMe(bio = null) {
- return this.edit({
- bio,
- });
- }
-
- /**
- * Change the email
- * @param {Email} email Email to change
- * @param {string} password Password of the account
- * @returns {Promise}
- */
- setEmail(email, password) {
- if (!password && !this.client.password)
- throw new Error('A password is required to change a email.');
- return this.edit({
- email,
- password: this.client.password ? this.client.password : password,
- });
- }
-
- /**
- * Set new password
- * @param {string} oldPassword Old password
- * @param {string} newPassword New password to set
- * @returns {Promise}
- */
- setPassword(oldPassword, newPassword) {
- if (!oldPassword && !this.client.password)
- throw new Error('A password is required to change a password.');
- if (!newPassword) throw new Error('New password is required.');
- return this.edit({
- password: this.client.password ? this.client.password : oldPassword,
- new_password: newPassword,
- });
- }
-
- /**
- * Disable account
- * @param {string} password Password of the account
- * @returns {Promise}
- */
- async disableAccount(password) {
- if (!password && !this.client.password)
- throw new Error('A password is required to disable an account.');
- return await this.client.api.users['@me'].disable.post({
- data: {
- password: this.client.password ? this.client.password : password,
- },
- });
- }
-
- /**
- * Delete account. Warning: Cannot be changed once used!
- * @param {string} password Password of the account
- * @returns {Promise}
- */
- async deleteAccount(password) {
- if (!password && !this.client.password)
- throw new Error('A password is required to delete an account.');
- return await this.client.api.users['@me'].delete.post({
- data: {
- password: this.client.password ? this.client.password : password,
- },
- });
- }
-
- /**
- * Options for setting activities
- * @typedef {Object} ActivitiesOptions
- * @property {string} [name] Name of the activity
- * @property {ActivityType|number} [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} [url] Twitch / YouTube stream URL
- * @property {ActivityType|number} [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: '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 });
- }
-
- /**
- * Send Friend Request to the user
- * @returns {Promise} the user object
- */
- async findFriend(username, discriminator) {
- return await this.client.api
- .users('@me')
- .relationships.post({
- data: {
- username: username,
- discriminator: parseInt(discriminator),
- },
- })
- .then((_) => _);
- }
-}
-
-module.exports = ClientUser;
+'use strict';
+
+const User = require('./User');
+const DataResolver = require('../util/DataResolver');
+const { HypeSquadOptions } = require('../util/Constants');
+const { Util } = require('..');
+
+/**
+ * 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;
+
+ // Add (Selfbot)
+ if ('premium' in data) this.nitro = data.premium;
+ /**
+ * Nitro Status
+ * `0`: None
+ * `1`: Classic
+ * `2`: Boost
+ * @external
+ * https://discord.com/developers/docs/resources/user#user-object-premium-types
+ * @type {Number}
+ */
+ if ('purchased_flags' in data) this.nitroType = data.purchased_flags;
+ if ('phone' in data) this.phoneNumber = data.phone;
+ if ('nsfw_allowed' in data) this.nsfwAllowed = data.nsfw_allowed;
+ if ('email' in data) this.emailAddress = data.email;
+ }
+
+ /**
+ * 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} ClientUserEditData
+ * @property {string} [username] The new username
+ * @property {?(BufferResolvable|Base64Resolvable)} [avatar] The new avatar
+ */
+
+ /**
+ * Edits the logged in client.
+ * @param {ClientUserEditData} data The new data
+ * @returns {Promise}
+ */
+ async edit(data) {
+ if (typeof data.avatar !== 'undefined')
+ data.avatar = await DataResolver.resolveImage(data.avatar);
+ if (typeof data.banner !== 'undefined')
+ data.banner = await DataResolver.resolveImage(data.banner);
+ const newData = await this.client.api.users('@me').patch({ data });
+ this.client.token = newData.token;
+ this.client.password = data?.password
+ ? data?.password
+ : this.client.password;
+ const { updated } = this.client.actions.UserUpdate.handle(newData);
+ return updated ?? this;
+ }
+
+ /**
+ * Sets the username of the logged in client.
+ * Changing usernames in Discord is heavily rate limited, with only 2 requests
+ * every hour. Use this sparingly!
+ * @param {string} username The new username
+ * @param {string} password The password of the account
+ * @returns {Promise}
+ * @example
+ * // Set username
+ * client.user.setUsername('discordjs')
+ * .then(user => console.log(`My new username is ${user.username}`))
+ * .catch(console.error);
+ */
+ setUsername(username, password) {
+ if (!password && !this.client.password)
+ throw new Error('A password is required to change a username.');
+ return this.edit({
+ username,
+ password: this.client.password ? this.client.password : password,
+ });
+ }
+
+ /**
+ * Sets the avatar of the logged in client.
+ * @param {?(BufferResolvable|Base64Resolvable)} avatar The new avatar
+ * @returns {Promise}
+ * @example
+ * // Set avatar
+ * client.user.setAvatar('./avatar.png')
+ * .then(user => console.log(`New avatar set!`))
+ * .catch(console.error);
+ */
+ setAvatar(avatar) {
+ return this.edit({ avatar });
+ }
+ /**
+ * Sets the banner of the logged in client.
+ * @param {?(BufferResolvable|Base64Resolvable)} banner The new banner
+ * @returns {Promise}
+ * @example
+ * // Set banner
+ * client.user.setBanner('./banner.png')
+ * .then(user => console.log(`New banner set!`))
+ * .catch(console.error);
+ */
+ setBanner(banner) {
+ if (this.nitroType !== 2)
+ throw new Error(
+ 'You must be a Nitro Boosted User to change your banner.',
+ );
+ return this.edit({ banner });
+ }
+
+ /**
+ * Set HyperSquad House
+ * @param {HypeSquadOptions} type
+ * `LEAVE`: 0
+ * `HOUSE_BRAVERY`: 1
+ * `HOUSE_BRILLIANCE`: 2
+ * `HOUSE_BALANCE`: 3
+ * @returns {Promise}
+ * @example
+ * // Set HyperSquad HOUSE_BRAVERY
+ * client.user.setHypeSquad(1); || client.user.setHypeSquad('HOUSE_BRAVERY');
+ * // Leave
+ * client.user.setHypeSquad(0);
+ */
+ async setHypeSquad(type) {
+ const id = typeof type === 'string' ? HypeSquadOptions[type] : type;
+ if (!id && id !== 0) throw new Error('Invalid HypeSquad type.');
+ if (id !== 0) return await this.client.api.hypesquad.online.post({
+ data: { house_id: id },
+ });
+ else return await this.client.api.hypesquad.online.delete();
+ }
+
+ /**
+ * Set Accent color
+ * @param {ColorResolvable} color Color to set
+ * @returns {Promise}
+ */
+ setAccentColor(color = null) {
+ return this.edit({ accent_color: color ? Util.resolveColor(color) : null });
+ }
+
+ /**
+ * Set discriminator
+ * @param {User.discriminator} discriminator It is #1234
+ * @param {string} password The password of the account
+ * @returns {Promise}
+ */
+ setDiscriminator(discriminator, password) {
+ if (!this.nitro) throw new Error('You must be a Nitro User to change your discriminator.');
+ if (!password && !this.client.password)
+ throw new Error('A password is required to change a discriminator.');
+ return this.edit({
+ discriminator,
+ password: this.client.password ? this.client.password : password,
+ });
+ }
+
+ /**
+ * Set About me
+ * @param {String} bio Bio to set
+ * @returns {Promise}
+ */
+ setAboutMe(bio = null) {
+ return this.edit({
+ bio,
+ });
+ }
+
+ /**
+ * Change the email
+ * @param {Email} email Email to change
+ * @param {string} password Password of the account
+ * @returns {Promise}
+ */
+ setEmail(email, password) {
+ if (!password && !this.client.password)
+ throw new Error('A password is required to change a email.');
+ return this.edit({
+ email,
+ password: this.client.password ? this.client.password : password,
+ });
+ }
+
+ /**
+ * Set new password
+ * @param {string} oldPassword Old password
+ * @param {string} newPassword New password to set
+ * @returns {Promise}
+ */
+ setPassword(oldPassword, newPassword) {
+ if (!oldPassword && !this.client.password)
+ throw new Error('A password is required to change a password.');
+ if (!newPassword) throw new Error('New password is required.');
+ return this.edit({
+ password: this.client.password ? this.client.password : oldPassword,
+ new_password: newPassword,
+ });
+ }
+
+ /**
+ * Disable account
+ * @param {string} password Password of the account
+ * @returns {Promise}
+ */
+ async disableAccount(password) {
+ if (!password && !this.client.password)
+ throw new Error('A password is required to disable an account.');
+ return await this.client.api.users['@me'].disable.post({
+ data: {
+ password: this.client.password ? this.client.password : password,
+ },
+ });
+ }
+
+ /**
+ * Delete account. Warning: Cannot be changed once used!
+ * @param {string} password Password of the account
+ * @returns {Promise}
+ */
+ async deleteAccount(password) {
+ if (!password && !this.client.password)
+ throw new Error('A password is required to delete an account.');
+ return await this.client.api.users['@me'].delete.post({
+ data: {
+ password: this.client.password ? this.client.password : password,
+ },
+ });
+ }
+
+ /**
+ * Options for setting activities
+ * @typedef {Object} ActivitiesOptions
+ * @property {string} [name] Name of the activity
+ * @property {ActivityType|number} [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} [url] Twitch / YouTube stream URL
+ * @property {ActivityType|number} [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: '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/src/structures/DMChannel.js b/src/structures/DMChannel.js
index 555de4f..5768496 100644
--- a/src/structures/DMChannel.js
+++ b/src/structures/DMChannel.js
@@ -1,101 +1,101 @@
-'use strict';
-
-const { Channel } = require('./Channel');
-const TextBasedChannel = require('./interfaces/TextBasedChannel');
-const MessageManager = require('../managers/MessageManager');
-
-/**
- * Represents a direct message channel between two users.
- * @extends {Channel}
- * @implements {TextBasedChannel}
- */
-class DMChannel extends Channel {
- constructor(client, data) {
- super(client, data);
-
- // Override the channel type so partials have a known type
- this.type = 'DM';
-
- /**
- * A manager of the messages belonging to this channel
- * @type {MessageManager}
- */
- this.messages = new MessageManager(this);
- }
-
- _patch(data) {
- super._patch(data);
-
- if (data.recipients) {
- /**
- * The recipient on the other end of the DM
- * @type {User}
- */
- this.recipient = this.client.users._add(data.recipients[0]);
- }
-
- 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 = new Date(data.last_pin_timestamp).getTime();
- } else {
- this.lastPinTimestamp ??= null;
- }
- }
-
- /**
- * Whether this DMChannel is a partial
- * @type {boolean}
- * @readonly
- */
- get partial() {
- return typeof this.lastMessageId === 'undefined';
- }
-
- /**
- * Fetch this DMChannel.
- * @param {boolean} [force=true] Whether to skip the cache check and request the API
- * @returns {Promise}
- */
- fetch(force = true) {
- return this.recipient.createDM(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 this.recipient.toString();
- }
-
- // 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() {}
-}
-
-TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']);
-
-module.exports = DMChannel;
+'use strict';
+
+const { Channel } = require('./Channel');
+const TextBasedChannel = require('./interfaces/TextBasedChannel');
+const MessageManager = require('../managers/MessageManager');
+
+/**
+ * Represents a direct message channel between two users.
+ * @extends {Channel}
+ * @implements {TextBasedChannel}
+ */
+class DMChannel extends Channel {
+ constructor(client, data) {
+ super(client, data);
+
+ // Override the channel type so partials have a known type
+ this.type = 'DM';
+
+ /**
+ * A manager of the messages belonging to this channel
+ * @type {MessageManager}
+ */
+ this.messages = new MessageManager(this);
+ }
+
+ _patch(data) {
+ super._patch(data);
+
+ if (data.recipients) {
+ /**
+ * The recipient on the other end of the DM
+ * @type {User}
+ */
+ this.recipient = this.client.users._add(data.recipients[0]);
+ }
+
+ 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 = new Date(data.last_pin_timestamp).getTime();
+ } else {
+ this.lastPinTimestamp ??= null;
+ }
+ }
+
+ /**
+ * Whether this DMChannel is a partial
+ * @type {boolean}
+ * @readonly
+ */
+ get partial() {
+ return typeof this.lastMessageId === 'undefined';
+ }
+
+ /**
+ * Fetch this DMChannel.
+ * @param {boolean} [force=true] Whether to skip the cache check and request the API
+ * @returns {Promise}
+ */
+ fetch(force = true) {
+ return this.recipient.createDM(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 this.recipient.toString();
+ }
+
+ // 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() {}
+}
+
+TextBasedChannel.applyToClass(DMChannel, true, ['bulkDelete']);
+
+module.exports = DMChannel;
diff --git a/src/structures/Message.js b/src/structures/Message.js
index 428bcce..8192c1a 100644
--- a/src/structures/Message.js
+++ b/src/structures/Message.js
@@ -1,1124 +1,1124 @@
-'use strict';
-
-const process = require('node:process');
-const { Collection } = require('@discordjs/collection');
-const Base = require('./Base');
-const BaseMessageComponent = require('./BaseMessageComponent');
-const ClientApplication = require('./ClientApplication');
-const InteractionCollector = require('./InteractionCollector');
-const MessageAttachment = require('./MessageAttachment');
-const Embed = require('./MessageEmbed');
-const Mentions = require('./MessageMentions');
-const MessagePayload = require('./MessagePayload');
-const ReactionCollector = require('./ReactionCollector');
-const { Sticker } = require('./Sticker');
-const { Error } = require('../errors');
-const ReactionManager = require('../managers/ReactionManager');
-const { InteractionTypes, MessageTypes, SystemMessageTypes } = require('../util/Constants');
-const MessageFlags = require('../util/MessageFlags');
-const Permissions = require('../util/Permissions');
-const SnowflakeUtil = require('../util/SnowflakeUtil');
-const Util = require('../util/Util');
-const { findBestMatch } = require('string-similarity'); // not check similarity
-const { ApplicationCommand } = require('discord.js-selfbot-v13');
-
-/**
- * @type {WeakSet}
- * @private
- * @internal
- */
-const deletedMessages = new WeakSet();
-let deprecationEmittedForDeleted = false;
-
-/**
- * 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 = SnowflakeUtil.timestampFrom(this.id);
-
- if ('type' in data) {
- /**
- * The type of the message
- * @type {?MessageType}
- */
- this.type = MessageTypes[data.type];
-
- /**
- * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
- * @type {?boolean}
- */
- this.system = SystemMessageTypes.includes(this.type);
- } else {
- this.system ??= null;
- this.type ??= null;
- }
-
- if ('content' in data) {
- /**
- * The content of the message
- * @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
- * This is only received after the message was sent successfully, and
- * lost if re-fetched
- * @type {?string}
- */
- this.nonce = data.nonce;
- } else {
- this.nonce ??= null;
- }
-
- if ('embeds' in data) {
- /**
- * A list of embeds in the message - e.g. YouTube Player
- * @type {MessageEmbed[]}
- */
- this.embeds = data.embeds.map((e) => new Embed(e, true));
- } else {
- this.embeds = this.embeds?.slice() ?? [];
- }
-
- if ('components' in data) {
- /**
- * A list of MessageActionRows in the message
- * @type {MessageActionRow[]}
- */
- this.components = data.components.map((c) =>
- BaseMessageComponent.create(c, this.client),
- );
- } else {
- this.components = this.components?.slice() ?? [];
- }
-
- if ('attachments' in data) {
- /**
- * A collection of attachments in the message - e.g. Pictures - mapped by their ids
- * @type {Collection}
- */
- this.attachments = new Collection();
- if (data.attachments) {
- for (const attachment of data.attachments) {
- this.attachments.set(
- attachment.id,
- new MessageAttachment(
- attachment.url,
- attachment.filename,
- 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}
- */
- 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);
- }
-
- // 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 = new Date(data.edited_timestamp).getTime();
- } 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}
- */
- this.flags = new MessageFlags(data.flags).freeze();
- } else {
- this.flags = new MessageFlags(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 (IS_CROSSPOST {@link MessageFlags.FLAGS message flag})
- * * CHANNEL_FOLLOW_ADD
- * * CHANNEL_PINNED_MESSAGE
- * * REPLY
- * * THREAD_STARTER_MESSAGE
- * @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
- * @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: InteractionTypes[data.interaction.type],
- commandName: data.interaction.name,
- user: this.client.users._add(data.interaction.user),
- };
- } else {
- this.interaction ??= null;
- }
- }
-
- /**
- * Whether or not the structure has been deleted
- * @type {boolean}
- * @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091
- */
- get deleted() {
- if (!deprecationEmittedForDeleted) {
- deprecationEmittedForDeleted = true;
- process.emitWarning(
- 'Message#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
- 'DeprecationWarning',
- );
- }
-
- return deletedMessages.has(this);
- }
-
- set deleted(value) {
- if (!deprecationEmittedForDeleted) {
- deprecationEmittedForDeleted = true;
- process.emitWarning(
- 'Message#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
- 'DeprecationWarning',
- );
- }
-
- if (value) deletedMessages.add(this);
- else deletedMessages.delete(this);
- }
-
- /**
- * The channel that the message was sent in
- * @type {TextChannel|DMChannel|NewsChannel|ThreadChannel}
- * @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) : null;
- }
-
- /**
- * 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.FLAGS.HAS_THREAD);
- }
-
- /**
- * The thread started by this message
- * This property is not suitable for checking whether a message has a thread,
- * use {@link Message#hasThread} instead.
- * @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 `https://discord.com/channels/${this.guildId ?? '@me'}/${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
- ? Util.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>}
- * @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 {MessageComponentType} [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: InteractionTypes.MESSAGE_COMPONENT,
- 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 {MessageComponentType} [componentType] The type of component interaction to collect
- */
-
- /**
- * 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}
- * @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 Error('INTERACTION_COLLECTOR_ERROR', 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 &&
- !deletedMessages.has(this) &&
- (!this.guild || this.channel?.viewable),
- );
- // Regardless of permissions thread messages cannot be edited if
- // the thread is locked.
- if (this.channel?.isThread()) {
- return precheck && !this.channel.locked;
- }
- return precheck;
- }
-
- /**
- * Whether the message is deletable by the client user
- * @type {boolean}
- * @readonly
- */
- get deletable() {
- if (deletedMessages.has(this)) {
- 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(Permissions.FLAGS.ADMINISTRATOR, false)) return true;
-
- return Boolean(
- this.author.id === this.client.user.id ||
- (permissions.has(Permissions.FLAGS.MANAGE_MESSAGES, false) &&
- this.guild.me.communicationDisabledUntilTimestamp < Date.now()),
- );
- }
-
- /**
- * Whether the message is pinnable by the client user
- * @type {boolean}
- * @readonly
- */
- get pinnable() {
- const { channel } = this;
- return Boolean(
- !this.system &&
- !deletedMessages.has(this) &&
- (!this.guild ||
- (channel?.viewable &&
- channel
- ?.permissionsFor(this.client.user)
- ?.has(Permissions.FLAGS.MANAGE_MESSAGES, false))),
- );
- }
-
- /**
- * Fetches the Message this crosspost/reply/pin-add references, if available to the client
- * @returns {Promise}
- */
- async fetchReference() {
- if (!this.reference) throw new Error('MESSAGE_REFERENCE_MISSING');
- const { channelId, messageId } = this.reference;
- const channel = this.client.channels.resolve(channelId);
- if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE');
- 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 =
- Permissions.FLAGS.SEND_MESSAGES |
- (this.author.id === this.client.user.id
- ? Permissions.defaultBit
- : Permissions.FLAGS.MANAGE_MESSAGES);
- const { channel } = this;
- return Boolean(
- channel?.type === 'GUILD_NEWS' &&
- !this.flags.has(MessageFlags.FLAGS.CROSSPOSTED) &&
- this.type === 'DEFAULT' &&
- channel.viewable &&
- channel.permissionsFor(this.client.user)?.has(bitfield, false) &&
- !deletedMessages.has(this),
- );
- }
-
- /**
- * Options that can be passed into {@link Message#edit}.
- * @typedef {Object} MessageEditOptions
- * @property {?string} [content] Content to be edited
- * @property {MessageEmbed[]|APIEmbed[]} [embeds] Embeds to be added/edited
- * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content
- * @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be edited.
- * @property {MessageAttachment[]} [attachments] An array of attachments to keep,
- * all attachments will be kept if omitted
- * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to add to the message
- * @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
- * Action rows containing interactive components for the message (buttons, select menus)
- */
-
- /**
- * Edits the content of the message.
- * @param {string|MessagePayload|MessageEditOptions} options The options to provide
- * @returns {Promise}
- * @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 Error('CHANNEL_NOT_CACHED'));
- return this.channel.messages.edit(this, options);
- }
-
- /**
- * Publishes a message in an announcement channel to all channels following it.
- * @returns {Promise}
- * @example
- * // Crosspost a message
- * if (message.channel.type === 'GUILD_NEWS') {
- * message.crosspost()
- * .then(() => console.log('Crossposted message'))
- * .catch(console.error);
- * }
- */
- crosspost() {
- if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
- return this.channel.messages.crosspost(this.id);
- }
-
- /**
- * Pins this message to the channel's pinned messages.
- * @returns {Promise}
- * @example
- * // Pin a message
- * message.pin()
- * .then(console.log)
- * .catch(console.error)
- */
- async pin() {
- if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
- await this.channel.messages.pin(this.id);
- return this;
- }
-
- /**
- * Unpins this message from the channel's pinned messages.
- * @returns {Promise}
- * @example
- * // Unpin a message
- * message.unpin()
- * .then(console.log)
- * .catch(console.error)
- */
- async unpin() {
- if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
- await this.channel.messages.unpin(this.id);
- return this;
- }
-
- /**
- * Adds a reaction to the message.
- * @param {EmojiIdentifierResolvable} emoji The emoji to react with
- * @returns {Promise}
- * @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 Error('CHANNEL_NOT_CACHED');
- await this.channel.messages.react(this.id, emoji);
-
- return this.client.actions.MessageReactionAdd.handle(
- {
- user: this.client.user,
- channel: this.channel,
- message: this,
- emoji: Util.resolvePartialEmoji(emoji),
- },
- true,
- ).reaction;
- }
-
- /**
- * Deletes the message.
- * @returns {Promise}
- * @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 Error('CHANNEL_NOT_CACHED');
- await this.channel.messages.delete(this.id);
- return this;
- }
-
- /**
- * Options provided when sending a message as an inline reply.
- * @typedef {BaseMessageOptions} ReplyMessageOptions
- * @property {boolean} [failIfNotExists=true] Whether to error if the referenced message
- * does not exist (creates a standard message in this case when false)
- * @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message
- */
-
- /**
- * Send an inline reply to this message.
- * @param {string|MessagePayload|ReplyMessageOptions} options The options to provide
- * @returns {Promise}
- * @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 Error('CHANNEL_NOT_CACHED'));
- 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);
- }
-
- /**
- * A number that is allowed to be the duration (in minutes) of inactivity after which a thread is automatically
- * archived. This can be:
- * * `60` (1 hour)
- * * `1440` (1 day)
- * * `4320` (3 days) This is only available when the guild has the `THREE_DAY_THREAD_ARCHIVE` feature.
- * * `10080` (7 days) This is only available when the guild has the `SEVEN_DAY_THREAD_ARCHIVE` feature.
- * * `'MAX'` Based on the guild's features
- * @typedef {number|string} ThreadAutoArchiveDuration
- */
-
- /**
- * 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 (in minutes) 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 ThreadManager#create
- * @param {StartThreadOptions} [options] Options for starting a thread on this message
- * @returns {Promise}
- */
- startThread(options = {}) {
- if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
- if (!['GUILD_TEXT', 'GUILD_NEWS'].includes(this.channel.type)) {
- return Promise.reject(new Error('MESSAGE_THREAD_PARENT'));
- }
- if (this.hasThread)
- return Promise.reject(new Error('MESSAGE_EXISTING_THREAD'));
- 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}
- */
- fetch(force = true) {
- if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
- return this.channel.messages.fetch(this.id, { force });
- }
-
- /**
- * Fetches the webhook used to create this message.
- * @returns {Promise}
- */
- fetchWebhook() {
- if (!this.webhookId) return Promise.reject(new Error('WEBHOOK_MESSAGE'));
- if (this.webhookId === this.applicationId)
- return Promise.reject(new Error('WEBHOOK_APPLICATION'));
- 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}
- */
- suppressEmbeds(suppress = true) {
- const flags = new MessageFlags(this.flags.bitfield);
-
- if (suppress) {
- flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS);
- } else {
- flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS);
- }
-
- return this.edit({ flags });
- }
-
- /**
- * Removes the attachments from this message.
- * @returns {Promise}
- */
- 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 === new Date(rawData.timestamp).getTime() &&
- this.editedTimestamp === new Date(rawData.edited_timestamp).getTime();
- }
-
- 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,
- });
- }
- // Added
- /**
- * Click specific button [Suggestion: Dux#2925]
- * @param {String} buttonID Button ID
- * @returns {Promise}
- */
- async clickButton(buttonID) {
- if (typeof buttonID !== 'string')
- throw new TypeError('BUTTON_ID_NOT_STRING');
- if (!this.components[0]) throw new TypeError('MESSAGE_NO_COMPONENTS');
- let button;
- await Promise.all(
- this.components.map(async (row) => {
- await Promise.all(
- row.components.map(async (interactionComponent) => {
- if (
- interactionComponent.type == 'BUTTON' &&
- interactionComponent.customId == buttonID
- ) {
- button = interactionComponent;
- }
- }),
- );
- }),
- );
- if (!button) throw new TypeError('BUTTON_NOT_FOUND');
- else button.click(this);
- }
- /**
- * Select specific menu or First Menu
- * @param {String|Array} menuID Select Menu specific id or auto select first Menu
- * @param {Array} options Menu Options
- */
- async selectMenu(menuID, options = []) {
- if (!this.components[0]) throw new TypeError('MESSAGE_NO_COMPONENTS');
- let menuFirst;
- let menuCorrect;
- let menuCount = 0;
- await Promise.all(
- this.components.map(async (row) => {
- const firstElement = row.components[0]; // Because 1 row has only 1 menu;
- if (firstElement.type == 'SELECT_MENU') {
- menuCount++;
- if (firstElement.customId == menuID) {
- menuCorrect = firstElement;
- } else if (!menuFirst) {
- menuFirst = firstElement;
- }
- }
- }),
- );
- if (menuCount == 0) throw new TypeError('MENU_NOT_FOUND');
- if (!menuCorrect) {
- if (menuCount == 1) menuCorrect = menuFirst;
- else if (typeof menuID !== 'string') throw new TypeError('MENU_ID_NOT_STRING');
- else throw new TypeError('MENU_ID_NOT_FOUND');
- }
- menuCorrect.select(this, Array.isArray(menuID) ? menuID : options);
- }
- //
- /**
- * Send context Menu v2
- * @param {DiscordBotID} botID Bot id
- * @param {String} commandName Command name in Context Menu
- * @returns {Promise}
- */
- async contextMenu(botID, commandName) {
- if (!botID) throw new Error('Bot ID is required');
- const user = await this.client.users.fetch(botID).catch(() => { });
- if (!user || !user.bot || !user.applications)
- throw new Error(
- 'BotID is not a bot or does not have an application slash command',
- );
- if (!commandName || typeof commandName !== 'string')
- throw new Error('Command name is required');
- const listApplication =
- user.applications.cache.size == 0
- ? await user.applications.fetch()
- : user.applications.cache;
- let contextCMD;
- await Promise.all(
- listApplication.map(async (application) => {
- if (commandName == application.name && application.type !== 'CHAT_INPUT')
- contextCMD = application;
- }),
- );
- if (!contextCMD)
- throw new Error(
- `Command ${commandName} is not found\nList command avalible: ${listApplication
- .filter((a) => a.type !== 'CHAT_INPUT')
- .map((a) => a.name)
- .join(', ')}`,
- );
- return contextCMD.sendContextMenu(this, true);
- }
-}
-
-exports.Message = Message;
-exports.deletedMessages = deletedMessages;
+'use strict';
+
+const process = require('node:process');
+const { Collection } = require('@discordjs/collection');
+const Base = require('./Base');
+const BaseMessageComponent = require('./BaseMessageComponent');
+const ClientApplication = require('./ClientApplication');
+const InteractionCollector = require('./InteractionCollector');
+const MessageAttachment = require('./MessageAttachment');
+const Embed = require('./MessageEmbed');
+const Mentions = require('./MessageMentions');
+const MessagePayload = require('./MessagePayload');
+const ReactionCollector = require('./ReactionCollector');
+const { Sticker } = require('./Sticker');
+const { Error } = require('../errors');
+const ReactionManager = require('../managers/ReactionManager');
+const { InteractionTypes, MessageTypes, SystemMessageTypes } = require('../util/Constants');
+const MessageFlags = require('../util/MessageFlags');
+const Permissions = require('../util/Permissions');
+const SnowflakeUtil = require('../util/SnowflakeUtil');
+const Util = require('../util/Util');
+const { findBestMatch } = require('string-similarity'); // not check similarity
+//const { ApplicationCommand } = require('discord.js-selfbot-v13'); - Not being used in this file, not necessary.
+
+/**
+ * @type {WeakSet}
+ * @private
+ * @internal
+ */
+const deletedMessages = new WeakSet();
+let deprecationEmittedForDeleted = false;
+
+/**
+ * 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 = SnowflakeUtil.timestampFrom(this.id);
+
+ if ('type' in data) {
+ /**
+ * The type of the message
+ * @type {?MessageType}
+ */
+ this.type = MessageTypes[data.type];
+
+ /**
+ * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
+ * @type {?boolean}
+ */
+ this.system = SystemMessageTypes.includes(this.type);
+ } else {
+ this.system ??= null;
+ this.type ??= null;
+ }
+
+ if ('content' in data) {
+ /**
+ * The content of the message
+ * @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
+ * This is only received after the message was sent successfully, and
+ * lost if re-fetched
+ * @type {?string}
+ */
+ this.nonce = data.nonce;
+ } else {
+ this.nonce ??= null;
+ }
+
+ if ('embeds' in data) {
+ /**
+ * A list of embeds in the message - e.g. YouTube Player
+ * @type {MessageEmbed[]}
+ */
+ this.embeds = data.embeds.map((e) => new Embed(e, true));
+ } else {
+ this.embeds = this.embeds?.slice() ?? [];
+ }
+
+ if ('components' in data) {
+ /**
+ * A list of MessageActionRows in the message
+ * @type {MessageActionRow[]}
+ */
+ this.components = data.components.map((c) =>
+ BaseMessageComponent.create(c, this.client),
+ );
+ } else {
+ this.components = this.components?.slice() ?? [];
+ }
+
+ if ('attachments' in data) {
+ /**
+ * A collection of attachments in the message - e.g. Pictures - mapped by their ids
+ * @type {Collection}
+ */
+ this.attachments = new Collection();
+ if (data.attachments) {
+ for (const attachment of data.attachments) {
+ this.attachments.set(
+ attachment.id,
+ new MessageAttachment(
+ attachment.url,
+ attachment.filename,
+ 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}
+ */
+ 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);
+ }
+
+ // 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 = new Date(data.edited_timestamp).getTime();
+ } 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}
+ */
+ this.flags = new MessageFlags(data.flags).freeze();
+ } else {
+ this.flags = new MessageFlags(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 (IS_CROSSPOST {@link MessageFlags.FLAGS message flag})
+ * * CHANNEL_FOLLOW_ADD
+ * * CHANNEL_PINNED_MESSAGE
+ * * REPLY
+ * * THREAD_STARTER_MESSAGE
+ * @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
+ * @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: InteractionTypes[data.interaction.type],
+ commandName: data.interaction.name,
+ user: this.client.users._add(data.interaction.user),
+ };
+ } else {
+ this.interaction ??= null;
+ }
+ }
+
+ /**
+ * Whether or not the structure has been deleted
+ * @type {boolean}
+ * @deprecated This will be removed in the next major version, see https://github.com/discordjs/discord.js/issues/7091
+ */
+ get deleted() {
+ if (!deprecationEmittedForDeleted) {
+ deprecationEmittedForDeleted = true;
+ process.emitWarning(
+ 'Message#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
+ 'DeprecationWarning',
+ );
+ }
+
+ return deletedMessages.has(this);
+ }
+
+ set deleted(value) {
+ if (!deprecationEmittedForDeleted) {
+ deprecationEmittedForDeleted = true;
+ process.emitWarning(
+ 'Message#deleted is deprecated, see https://github.com/discordjs/discord.js/issues/7091.',
+ 'DeprecationWarning',
+ );
+ }
+
+ if (value) deletedMessages.add(this);
+ else deletedMessages.delete(this);
+ }
+
+ /**
+ * The channel that the message was sent in
+ * @type {TextChannel|DMChannel|NewsChannel|ThreadChannel}
+ * @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) : null;
+ }
+
+ /**
+ * 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.FLAGS.HAS_THREAD);
+ }
+
+ /**
+ * The thread started by this message
+ * This property is not suitable for checking whether a message has a thread,
+ * use {@link Message#hasThread} instead.
+ * @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 `https://discord.com/channels/${this.guildId ?? '@me'}/${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
+ ? Util.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>}
+ * @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 {MessageComponentType} [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: InteractionTypes.MESSAGE_COMPONENT,
+ 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 {MessageComponentType} [componentType] The type of component interaction to collect
+ */
+
+ /**
+ * 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}
+ * @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 Error('INTERACTION_COLLECTOR_ERROR', 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 &&
+ !deletedMessages.has(this) &&
+ (!this.guild || this.channel?.viewable),
+ );
+ // Regardless of permissions thread messages cannot be edited if
+ // the thread is locked.
+ if (this.channel?.isThread()) {
+ return precheck && !this.channel.locked;
+ }
+ return precheck;
+ }
+
+ /**
+ * Whether the message is deletable by the client user
+ * @type {boolean}
+ * @readonly
+ */
+ get deletable() {
+ if (deletedMessages.has(this)) {
+ 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(Permissions.FLAGS.ADMINISTRATOR, false)) return true;
+
+ return Boolean(
+ this.author.id === this.client.user.id ||
+ (permissions.has(Permissions.FLAGS.MANAGE_MESSAGES, false) &&
+ this.guild.me.communicationDisabledUntilTimestamp < Date.now()),
+ );
+ }
+
+ /**
+ * Whether the message is pinnable by the client user
+ * @type {boolean}
+ * @readonly
+ */
+ get pinnable() {
+ const { channel } = this;
+ return Boolean(
+ !this.system &&
+ !deletedMessages.has(this) &&
+ (!this.guild ||
+ (channel?.viewable &&
+ channel
+ ?.permissionsFor(this.client.user)
+ ?.has(Permissions.FLAGS.MANAGE_MESSAGES, false))),
+ );
+ }
+
+ /**
+ * Fetches the Message this crosspost/reply/pin-add references, if available to the client
+ * @returns {Promise}
+ */
+ async fetchReference() {
+ if (!this.reference) throw new Error('MESSAGE_REFERENCE_MISSING');
+ const { channelId, messageId } = this.reference;
+ const channel = this.client.channels.resolve(channelId);
+ if (!channel) throw new Error('GUILD_CHANNEL_RESOLVE');
+ 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 =
+ Permissions.FLAGS.SEND_MESSAGES |
+ (this.author.id === this.client.user.id
+ ? Permissions.defaultBit
+ : Permissions.FLAGS.MANAGE_MESSAGES);
+ const { channel } = this;
+ return Boolean(
+ channel?.type === 'GUILD_NEWS' &&
+ !this.flags.has(MessageFlags.FLAGS.CROSSPOSTED) &&
+ this.type === 'DEFAULT' &&
+ channel.viewable &&
+ channel.permissionsFor(this.client.user)?.has(bitfield, false) &&
+ !deletedMessages.has(this),
+ );
+ }
+
+ /**
+ * Options that can be passed into {@link Message#edit}.
+ * @typedef {Object} MessageEditOptions
+ * @property {?string} [content] Content to be edited
+ * @property {MessageEmbed[]|APIEmbed[]} [embeds] Embeds to be added/edited
+ * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content
+ * @property {MessageFlags} [flags] Which flags to set for the message. Only `SUPPRESS_EMBEDS` can be edited.
+ * @property {MessageAttachment[]} [attachments] An array of attachments to keep,
+ * all attachments will be kept if omitted
+ * @property {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to add to the message
+ * @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
+ * Action rows containing interactive components for the message (buttons, select menus)
+ */
+
+ /**
+ * Edits the content of the message.
+ * @param {string|MessagePayload|MessageEditOptions} options The options to provide
+ * @returns {Promise}
+ * @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 Error('CHANNEL_NOT_CACHED'));
+ return this.channel.messages.edit(this, options);
+ }
+
+ /**
+ * Publishes a message in an announcement channel to all channels following it.
+ * @returns {Promise}
+ * @example
+ * // Crosspost a message
+ * if (message.channel.type === 'GUILD_NEWS') {
+ * message.crosspost()
+ * .then(() => console.log('Crossposted message'))
+ * .catch(console.error);
+ * }
+ */
+ crosspost() {
+ if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
+ return this.channel.messages.crosspost(this.id);
+ }
+
+ /**
+ * Pins this message to the channel's pinned messages.
+ * @returns {Promise}
+ * @example
+ * // Pin a message
+ * message.pin()
+ * .then(console.log)
+ * .catch(console.error)
+ */
+ async pin() {
+ if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
+ await this.channel.messages.pin(this.id);
+ return this;
+ }
+
+ /**
+ * Unpins this message from the channel's pinned messages.
+ * @returns {Promise}
+ * @example
+ * // Unpin a message
+ * message.unpin()
+ * .then(console.log)
+ * .catch(console.error)
+ */
+ async unpin() {
+ if (!this.channel) throw new Error('CHANNEL_NOT_CACHED');
+ await this.channel.messages.unpin(this.id);
+ return this;
+ }
+
+ /**
+ * Adds a reaction to the message.
+ * @param {EmojiIdentifierResolvable} emoji The emoji to react with
+ * @returns {Promise}
+ * @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 Error('CHANNEL_NOT_CACHED');
+ await this.channel.messages.react(this.id, emoji);
+
+ return this.client.actions.MessageReactionAdd.handle(
+ {
+ user: this.client.user,
+ channel: this.channel,
+ message: this,
+ emoji: Util.resolvePartialEmoji(emoji),
+ },
+ true,
+ ).reaction;
+ }
+
+ /**
+ * Deletes the message.
+ * @returns {Promise}
+ * @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 Error('CHANNEL_NOT_CACHED');
+ await this.channel.messages.delete(this.id);
+ return this;
+ }
+
+ /**
+ * Options provided when sending a message as an inline reply.
+ * @typedef {BaseMessageOptions} ReplyMessageOptions
+ * @property {boolean} [failIfNotExists=true] Whether to error if the referenced message
+ * does not exist (creates a standard message in this case when false)
+ * @property {StickerResolvable[]} [stickers=[]] Stickers to send in the message
+ */
+
+ /**
+ * Send an inline reply to this message.
+ * @param {string|MessagePayload|ReplyMessageOptions} options The options to provide
+ * @returns {Promise}
+ * @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 Error('CHANNEL_NOT_CACHED'));
+ 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);
+ }
+
+ /**
+ * A number that is allowed to be the duration (in minutes) of inactivity after which a thread is automatically
+ * archived. This can be:
+ * * `60` (1 hour)
+ * * `1440` (1 day)
+ * * `4320` (3 days) This is only available when the guild has the `THREE_DAY_THREAD_ARCHIVE` feature.
+ * * `10080` (7 days) This is only available when the guild has the `SEVEN_DAY_THREAD_ARCHIVE` feature.
+ * * `'MAX'` Based on the guild's features
+ * @typedef {number|string} ThreadAutoArchiveDuration
+ */
+
+ /**
+ * 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 (in minutes) 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 ThreadManager#create
+ * @param {StartThreadOptions} [options] Options for starting a thread on this message
+ * @returns {Promise}
+ */
+ startThread(options = {}) {
+ if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
+ if (!['GUILD_TEXT', 'GUILD_NEWS'].includes(this.channel.type)) {
+ return Promise.reject(new Error('MESSAGE_THREAD_PARENT'));
+ }
+ if (this.hasThread)
+ return Promise.reject(new Error('MESSAGE_EXISTING_THREAD'));
+ 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}
+ */
+ fetch(force = true) {
+ if (!this.channel) return Promise.reject(new Error('CHANNEL_NOT_CACHED'));
+ return this.channel.messages.fetch(this.id, { force });
+ }
+
+ /**
+ * Fetches the webhook used to create this message.
+ * @returns {Promise}
+ */
+ fetchWebhook() {
+ if (!this.webhookId) return Promise.reject(new Error('WEBHOOK_MESSAGE'));
+ if (this.webhookId === this.applicationId)
+ return Promise.reject(new Error('WEBHOOK_APPLICATION'));
+ 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}
+ */
+ suppressEmbeds(suppress = true) {
+ const flags = new MessageFlags(this.flags.bitfield);
+
+ if (suppress) {
+ flags.add(MessageFlags.FLAGS.SUPPRESS_EMBEDS);
+ } else {
+ flags.remove(MessageFlags.FLAGS.SUPPRESS_EMBEDS);
+ }
+
+ return this.edit({ flags });
+ }
+
+ /**
+ * Removes the attachments from this message.
+ * @returns {Promise}
+ */
+ 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 === new Date(rawData.timestamp).getTime() &&
+ this.editedTimestamp === new Date(rawData.edited_timestamp).getTime();
+ }
+
+ 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,
+ });
+ }
+ // Added
+ /**
+ * Click specific button [Suggestion: Dux#2925]
+ * @param {String} buttonID Button ID
+ * @returns {Promise}
+ */
+ async clickButton(buttonID) {
+ if (typeof buttonID !== 'string')
+ throw new TypeError('BUTTON_ID_NOT_STRING');
+ if (!this.components[0]) throw new TypeError('MESSAGE_NO_COMPONENTS');
+ let button;
+ await Promise.all(
+ this.components.map(async (row) => {
+ await Promise.all(
+ row.components.map(async (interactionComponent) => {
+ if (
+ interactionComponent.type == 'BUTTON' &&
+ interactionComponent.customId == buttonID
+ ) {
+ button = interactionComponent;
+ }
+ }),
+ );
+ }),
+ );
+ if (!button) throw new TypeError('BUTTON_NOT_FOUND');
+ else button.click(this);
+ }
+ /**
+ * Select specific menu or First Menu
+ * @param {String|Array} menuID Select Menu specific id or auto select first Menu
+ * @param {Array} options Menu Options
+ */
+ async selectMenu(menuID, options = []) {
+ if (!this.components[0]) throw new TypeError('MESSAGE_NO_COMPONENTS');
+ let menuFirst;
+ let menuCorrect;
+ let menuCount = 0;
+ await Promise.all(
+ this.components.map(async (row) => {
+ const firstElement = row.components[0]; // Because 1 row has only 1 menu;
+ if (firstElement.type == 'SELECT_MENU') {
+ menuCount++;
+ if (firstElement.customId == menuID) {
+ menuCorrect = firstElement;
+ } else if (!menuFirst) {
+ menuFirst = firstElement;
+ }
+ }
+ }),
+ );
+ if (menuCount == 0) throw new TypeError('MENU_NOT_FOUND');
+ if (!menuCorrect) {
+ if (menuCount == 1) menuCorrect = menuFirst;
+ else if (typeof menuID !== 'string') throw new TypeError('MENU_ID_NOT_STRING');
+ else throw new TypeError('MENU_ID_NOT_FOUND');
+ }
+ menuCorrect.select(this, Array.isArray(menuID) ? menuID : options);
+ }
+ //
+ /**
+ * Send context Menu v2
+ * @param {DiscordBotID} botID Bot id
+ * @param {String} commandName Command name in Context Menu
+ * @returns {Promise}
+ */
+ async contextMenu(botID, commandName) {
+ if (!botID) throw new Error('Bot ID is required');
+ const user = await this.client.users.fetch(botID).catch(() => { });
+ if (!user || !user.bot || !user.applications)
+ throw new Error(
+ 'BotID is not a bot or does not have an application slash command',
+ );
+ if (!commandName || typeof commandName !== 'string')
+ throw new Error('Command name is required');
+ const listApplication =
+ user.applications.cache.size == 0
+ ? await user.applications.fetch()
+ : user.applications.cache;
+ let contextCMD;
+ await Promise.all(
+ listApplication.map(async (application) => {
+ if (commandName == application.name && application.type !== 'CHAT_INPUT')
+ contextCMD = application;
+ }),
+ );
+ if (!contextCMD)
+ throw new Error(
+ `Command ${commandName} is not found\nList command avalible: ${listApplication
+ .filter((a) => a.type !== 'CHAT_INPUT')
+ .map((a) => a.name)
+ .join(', ')}`,
+ );
+ return contextCMD.sendContextMenu(this, true);
+ }
+}
+
+exports.Message = Message;
+exports.deletedMessages = deletedMessages;
diff --git a/src/structures/MessageActionRow.js b/src/structures/MessageActionRow.js
index c44b32a..6a95357 100644
--- a/src/structures/MessageActionRow.js
+++ b/src/structures/MessageActionRow.js
@@ -1,101 +1,101 @@
-'use strict';
-
-const BaseMessageComponent = require('./BaseMessageComponent');
-const { MessageComponentTypes } = require('../util/Constants');
-
-/**
- * Represents an action row containing message components.
- * @extends {BaseMessageComponent}
- */
-class MessageActionRow extends BaseMessageComponent {
- /**
- * Components that can be placed in an action row
- * * MessageButton
- * * MessageSelectMenu
- * @typedef {MessageButton|MessageSelectMenu} MessageActionRowComponent
- */
-
- /**
- * Options for components that can be placed in an action row
- * * MessageButtonOptions
- * * MessageSelectMenuOptions
- * @typedef {MessageButtonOptions|MessageSelectMenuOptions} MessageActionRowComponentOptions
- */
-
- /**
- * Data that can be resolved into components that can be placed in an action row
- * * MessageActionRowComponent
- * * MessageActionRowComponentOptions
- * @typedef {MessageActionRowComponent|MessageActionRowComponentOptions} MessageActionRowComponentResolvable
- */
-
- /**
- * @typedef {BaseMessageComponentOptions} MessageActionRowOptions
- * @property {MessageActionRowComponentResolvable[]} [components]
- * The components to place in this action row
- */
-
- /**
- * @param {MessageActionRow|MessageActionRowOptions} [data={}] MessageActionRow to clone or raw data
- * @param {Client} [client] The client constructing this MessageActionRow, if provided
- */
- constructor(data = {}, client = null) {
- super({ type: 'ACTION_ROW' });
-
- /**
- * The components in this action row
- * @type {MessageActionRowComponent[]}
- */
- this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? [];
- }
-
- /**
- * Adds components to the action row.
- * @param {...MessageActionRowComponentResolvable[]} components The components to add
- * @returns {MessageActionRow}
- */
- addComponents(...components) {
- this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
- return this;
- }
-
- /**
- * Sets the components of the action row.
- * @param {...MessageActionRowComponentResolvable[]} components The components to set
- * @returns {MessageActionRow}
- */
- setComponents(...components) {
- this.spliceComponents(0, this.components.length, components);
- return this;
- }
-
- /**
- * Removes, replaces, and inserts components in the action row.
- * @param {number} index The index to start at
- * @param {number} deleteCount The number of components to remove
- * @param {...MessageActionRowComponentResolvable[]} [components] The replacing components
- * @returns {MessageActionRow}
- */
- spliceComponents(index, deleteCount, ...components) {
- this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
- return this;
- }
-
- /**
- * Transforms the action row to a plain object.
- * @returns {APIMessageComponent} The raw data of this action row
- */
- toJSON() {
- return {
- components: this.components.map(c => c.toJSON()),
- type: MessageComponentTypes[this.type],
- };
- }
-}
-
-module.exports = MessageActionRow;
-
-/**
- * @external APIMessageComponent
- * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object}
- */
+'use strict';
+
+const BaseMessageComponent = require('./BaseMessageComponent');
+const { MessageComponentTypes } = require('../util/Constants');
+
+/**
+ * Represents an action row containing message components.
+ * @extends {BaseMessageComponent}
+ */
+class MessageActionRow extends BaseMessageComponent {
+ /**
+ * Components that can be placed in an action row
+ * * MessageButton
+ * * MessageSelectMenu
+ * @typedef {MessageButton|MessageSelectMenu} MessageActionRowComponent
+ */
+
+ /**
+ * Options for components that can be placed in an action row
+ * * MessageButtonOptions
+ * * MessageSelectMenuOptions
+ * @typedef {MessageButtonOptions|MessageSelectMenuOptions} MessageActionRowComponentOptions
+ */
+
+ /**
+ * Data that can be resolved into components that can be placed in an action row
+ * * MessageActionRowComponent
+ * * MessageActionRowComponentOptions
+ * @typedef {MessageActionRowComponent|MessageActionRowComponentOptions} MessageActionRowComponentResolvable
+ */
+
+ /**
+ * @typedef {BaseMessageComponentOptions} MessageActionRowOptions
+ * @property {MessageActionRowComponentResolvable[]} [components]
+ * The components to place in this action row
+ */
+
+ /**
+ * @param {MessageActionRow|MessageActionRowOptions} [data={}] MessageActionRow to clone or raw data
+ * @param {Client} [client] The client constructing this MessageActionRow, if provided
+ */
+ constructor(data = {}, client = null) {
+ super({ type: 'ACTION_ROW' });
+
+ /**
+ * The components in this action row
+ * @type {MessageActionRowComponent[]}
+ */
+ this.components = data.components?.map(c => BaseMessageComponent.create(c, client)) ?? [];
+ }
+
+ /**
+ * Adds components to the action row.
+ * @param {...MessageActionRowComponentResolvable[]} components The components to add
+ * @returns {MessageActionRow}
+ */
+ addComponents(...components) {
+ this.components.push(...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
+ return this;
+ }
+
+ /**
+ * Sets the components of the action row.
+ * @param {...MessageActionRowComponentResolvable[]} components The components to set
+ * @returns {MessageActionRow}
+ */
+ setComponents(...components) {
+ this.spliceComponents(0, this.components.length, components);
+ return this;
+ }
+
+ /**
+ * Removes, replaces, and inserts components in the action row.
+ * @param {number} index The index to start at
+ * @param {number} deleteCount The number of components to remove
+ * @param {...MessageActionRowComponentResolvable[]} [components] The replacing components
+ * @returns {MessageActionRow}
+ */
+ spliceComponents(index, deleteCount, ...components) {
+ this.components.splice(index, deleteCount, ...components.flat(Infinity).map(c => BaseMessageComponent.create(c)));
+ return this;
+ }
+
+ /**
+ * Transforms the action row to a plain object.
+ * @returns {APIMessageComponent} The raw data of this action row
+ */
+ toJSON() {
+ return {
+ components: this.components.map(c => c.toJSON()),
+ type: MessageComponentTypes[this.type],
+ };
+ }
+}
+
+module.exports = MessageActionRow;
+
+/**
+ * @external APIMessageComponent
+ * @see {@link https://discord.com/developers/docs/interactions/message-components#component-object}
+ */
diff --git a/src/structures/MessageButton.js b/src/structures/MessageButton.js
index 9adb88a..c21d98f 100644
--- a/src/structures/MessageButton.js
+++ b/src/structures/MessageButton.js
@@ -1,194 +1,193 @@
-'use strict';
-
-const BaseMessageComponent = require('./BaseMessageComponent');
-const { Message } = require('./Message');
-const { RangeError } = require('../errors');
-const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants');
-const Util = require('../util/Util');
-
-/**
- * Represents a button message component.
- * @extends {BaseMessageComponent}
- */
-class MessageButton extends BaseMessageComponent {
- /**
- * @typedef {BaseMessageComponentOptions} MessageButtonOptions
- * @property {string} [label] The text to be displayed on this button
- * @property {string} [customId] A unique string to be sent in the interaction when clicked
- * @property {MessageButtonStyleResolvable} [style] The style of this button
- * @property {EmojiIdentifierResolvable} [emoji] The emoji to be displayed to the left of the text
- * @property {string} [url] Optional URL for link-style buttons
- * @property {boolean} [disabled=false] Disables the button to prevent interactions
- */
-
- /**
- * @param {MessageButton|MessageButtonOptions} [data={}] MessageButton to clone or raw data
- */
- constructor(data = {}) {
- super({ type: 'BUTTON' });
-
- this.setup(data);
- }
-
- setup(data) {
- /**
- * The text to be displayed on this button
- * @type {?string}
- */
- this.label = data.label ?? null;
-
- /**
- * A unique string to be sent in the interaction when clicked
- * @type {?string}
- */
- this.customId = data.custom_id ?? data.customId ?? null;
-
- /**
- * The style of this button
- * @type {?MessageButtonStyle}
- */
- this.style = data.style ? MessageButton.resolveStyle(data.style) : null;
-
- /**
- * Emoji for this button
- * @type {?RawEmoji}
- */
- this.emoji = data.emoji ? Util.resolvePartialEmoji(data.emoji) : null;
-
- /**
- * The URL this button links to, if it is a Link style button
- * @type {?string}
- */
- this.url = data.url ?? null;
-
- /**
- * Whether this button is currently disabled
- * @type {boolean}
- */
- this.disabled = data.disabled ?? false;
- }
-
- /**
- * Sets the custom id for this button
- * @param {string} customId A unique string to be sent in the interaction when clicked
- * @returns {MessageButton}
- */
- setCustomId(customId) {
- this.customId = Util.verifyString(customId, RangeError, 'BUTTON_CUSTOM_ID');
- return this;
- }
-
- /**
- * Sets the interactive status of the button
- * @param {boolean} [disabled=true] Whether this button should be disabled
- * @returns {MessageButton}
- */
- setDisabled(disabled = true) {
- this.disabled = disabled;
- return this;
- }
-
- /**
- * Set the emoji of this button
- * @param {EmojiIdentifierResolvable} emoji The emoji to be displayed on this button
- * @returns {MessageButton}
- */
- setEmoji(emoji) {
- this.emoji = Util.resolvePartialEmoji(emoji);
- return this;
- }
-
- /**
- * Sets the label of this button
- * @param {string} label The text to be displayed on this button
- * @returns {MessageButton}
- */
- setLabel(label) {
- this.label = Util.verifyString(label, RangeError, 'BUTTON_LABEL');
- return this;
- }
-
- /**
- * Sets the style of this button
- * @param {MessageButtonStyleResolvable} style The style of this button
- * @returns {MessageButton}
- */
- setStyle(style) {
- this.style = MessageButton.resolveStyle(style);
- return this;
- }
-
- /**
- * Sets the URL of this button.
- * MessageButton#style must be LINK when setting a URL
- * @param {string} url The URL of this button
- * @returns {MessageButton}
- */
- setURL(url) {
- this.url = Util.verifyString(url, RangeError, 'BUTTON_URL');
- return this;
- }
-
- /**
- * Transforms the button to a plain object.
- * @returns {APIMessageButton} The raw data of this button
- */
- toJSON() {
- return {
- custom_id: this.customId,
- disabled: this.disabled,
- emoji: this.emoji,
- label: this.label,
- style: MessageButtonStyles[this.style],
- type: MessageComponentTypes[this.type],
- url: this.url,
- };
- }
-
- /**
- * Data that can be resolved to a MessageButtonStyle. This can be
- * * MessageButtonStyle
- * * number
- * @typedef {number|MessageButtonStyle} MessageButtonStyleResolvable
- */
-
- /**
- * Resolves the style of a button
- * @param {MessageButtonStyleResolvable} style The style to resolve
- * @returns {MessageButtonStyle}
- * @private
- */
- static resolveStyle(style) {
- return typeof style === 'string' ? style : MessageButtonStyles[style];
- }
- // Patch Click
- /**
- * Click the button
- * @param {Message} message Discord Message
- * @returns {boolean}
- */
- async click(message) {
- if (!message instanceof Message) throw new Error("[UNKNOWN_MESSAGE] Please pass a valid Message");
- if (!this.customId || this.style == 5 || this.disabled) return false; // Button URL, Disabled
- await message.client.api.interactions.post(
- {
- data: {
- type: 3, // ?
- guild_id: message.guild?.id ?? null, // In DMs
- channel_id: message.channel.id,
- message_id: message.id,
- application_id: message.author.id,
- session_id: message.client.session_id,
- data: {
- component_type: 2, // Button
- custom_id: this.customId
- },
- message_flags: message.flags.bitfield,
- }
- }
- )
- return true;
- }
-}
-
-module.exports = MessageButton;
+'use strict';
+
+const BaseMessageComponent = require('./BaseMessageComponent');
+const { Message } = require('./Message');
+const { RangeError } = require('../errors');
+const { MessageButtonStyles, MessageComponentTypes } = require('../util/Constants');
+const Util = require('../util/Util');
+
+/**
+ * Represents a button message component.
+ * @extends {BaseMessageComponent}
+ */
+class MessageButton extends BaseMessageComponent {
+ /**
+ * @typedef {BaseMessageComponentOptions} MessageButtonOptions
+ * @property {string} [label] The text to be displayed on this button
+ * @property {string} [customId] A unique string to be sent in the interaction when clicked
+ * @property {MessageButtonStyleResolvable} [style] The style of this button
+ * @property {EmojiIdentifierResolvable} [emoji] The emoji to be displayed to the left of the text
+ * @property {string} [url] Optional URL for link-style buttons
+ * @property {boolean} [disabled=false] Disables the button to prevent interactions
+ */
+
+ /**
+ * @param {MessageButton|MessageButtonOptions} [data={}] MessageButton to clone or raw data
+ */
+ constructor(data = {}) {
+ super({ type: 'BUTTON' });
+
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * The text to be displayed on this button
+ * @type {?string}
+ */
+ this.label = data.label ?? null;
+
+ /**
+ * A unique string to be sent in the interaction when clicked
+ * @type {?string}
+ */
+ this.customId = data.custom_id ?? data.customId ?? null;
+
+ /**
+ * The style of this button
+ * @type {?MessageButtonStyle}
+ */
+ this.style = data.style ? MessageButton.resolveStyle(data.style) : null;
+
+ /**
+ * Emoji for this button
+ * @type {?RawEmoji}
+ */
+ this.emoji = data.emoji ? Util.resolvePartialEmoji(data.emoji) : null;
+
+ /**
+ * The URL this button links to, if it is a Link style button
+ * @type {?string}
+ */
+ this.url = data.url ?? null;
+
+ /**
+ * Whether this button is currently disabled
+ * @type {boolean}
+ */
+ this.disabled = data.disabled ?? false;
+ }
+
+ /**
+ * Sets the custom id for this button
+ * @param {string} customId A unique string to be sent in the interaction when clicked
+ * @returns {MessageButton}
+ */
+ setCustomId(customId) {
+ this.customId = Util.verifyString(customId, RangeError, 'BUTTON_CUSTOM_ID');
+ return this;
+ }
+
+ /**
+ * Sets the interactive status of the button
+ * @param {boolean} [disabled=true] Whether this button should be disabled
+ * @returns {MessageButton}
+ */
+ setDisabled(disabled = true) {
+ this.disabled = disabled;
+ return this;
+ }
+
+ /**
+ * Set the emoji of this button
+ * @param {EmojiIdentifierResolvable} emoji The emoji to be displayed on this button
+ * @returns {MessageButton}
+ */
+ setEmoji(emoji) {
+ this.emoji = Util.resolvePartialEmoji(emoji);
+ return this;
+ }
+
+ /**
+ * Sets the label of this button
+ * @param {string} label The text to be displayed on this button
+ * @returns {MessageButton}
+ */
+ setLabel(label) {
+ this.label = Util.verifyString(label, RangeError, 'BUTTON_LABEL');
+ return this;
+ }
+
+ /**
+ * Sets the style of this button
+ * @param {MessageButtonStyleResolvable} style The style of this button
+ * @returns {MessageButton}
+ */
+ setStyle(style) {
+ this.style = MessageButton.resolveStyle(style);
+ return this;
+ }
+
+ /**
+ * Sets the URL of this button.
+ * MessageButton#style must be LINK when setting a URL
+ * @param {string} url The URL of this button
+ * @returns {MessageButton}
+ */
+ setURL(url) {
+ this.url = Util.verifyString(url, RangeError, 'BUTTON_URL');
+ return this;
+ }
+
+ /**
+ * Transforms the button to a plain object.
+ * @returns {APIMessageButton} The raw data of this button
+ */
+ toJSON() {
+ return {
+ custom_id: this.customId,
+ disabled: this.disabled,
+ emoji: this.emoji,
+ label: this.label,
+ style: MessageButtonStyles[this.style],
+ type: MessageComponentTypes[this.type],
+ url: this.url,
+ };
+ }
+
+ /**
+ * Data that can be resolved to a MessageButtonStyle. This can be
+ * * MessageButtonStyle
+ * * number
+ * @typedef {number|MessageButtonStyle} MessageButtonStyleResolvable
+ */
+
+ /**
+ * Resolves the style of a button
+ * @param {MessageButtonStyleResolvable} style The style to resolve
+ * @returns {MessageButtonStyle}
+ * @private
+ */
+ static resolveStyle(style) {
+ return typeof style === 'string' ? style : MessageButtonStyles[style];
+ }
+ // Patch Click
+ /**
+ * Click the button
+ * @param {Message} message Discord Message
+ * @returns {boolean}
+ */
+ async click(message) {
+ if (!message instanceof Message) throw new Error("[UNKNOWN_MESSAGE] Please pass a valid Message");
+ if (!this.customId || this.style == 5 || this.disabled) return false; // Button URL, Disabled
+ await message.client.api.interactions.post(
+ {
+ data: {
+ type: 3, // ?
+ guild_id: message.guild?.id ?? null, // In DMs
+ channel_id: message.channel.id,
+ message_id: message.id,
+ application_id: message.author.id,
+ session_id: message.client.session_id,
+ data: {
+ component_type: 2, // Button
+ custom_id: this.customId
+ },
+ }
+ }
+ )
+ return true;
+ }
+}
+
+module.exports = MessageButton;
diff --git a/src/structures/MessageMentions.js b/src/structures/MessageMentions.js
index 9b935f9..4b75efb 100644
--- a/src/structures/MessageMentions.js
+++ b/src/structures/MessageMentions.js
@@ -1,235 +1,235 @@
-'use strict';
-
-const { Collection } = require('@discordjs/collection');
-const { ChannelTypes } = require('../util/Constants');
-const Util = require('../util/Util');
-
-/**
- * Keeps track of mentions in a {@link Message}.
- */
-class MessageMentions {
- 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
- * Order as received from the API, not as they appear in the message content
- * @type {Collection}
- */
- 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
- * Order as received from the API, not as they appear in the message content
- * @type {Collection}
- */
- 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}
- * @private
- */
- this._members = null;
-
- /**
- * Cached channels for {@link MessageMentions#channels}
- * @type {?Collection}
- * @private
- */
- this._channels = 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
- * Order as received from the API, not as they appear in the message content
- * @type {Collection}
- */
- this.crosspostedChannels = new Collection(crosspostedChannels);
- } else {
- this.crosspostedChannels = new Collection();
- const channelTypes = Object.keys(ChannelTypes);
- for (const d of crosspostedChannels) {
- const type = channelTypes[d.type];
- this.crosspostedChannels.set(d.id, {
- channelId: d.id,
- guildId: d.guild_id,
- type: type ?? 'UNKNOWN',
- 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)
- * Order as received from the API, not as they appear in the message content
- * @type {?Collection}
- * @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
- * Order as they appear first in the message content
- * @type {Collection}
- * @readonly
- */
- get channels() {
- if (this._channels) return this._channels;
- this._channels = new Collection();
- let matches;
- while ((matches = this.constructor.CHANNELS_PATTERN.exec(this._content)) !== null) {
- const chan = this.client.channels.cache.get(matches[1]);
- if (chan) this._channels.set(chan.id, chan);
- }
- return this._channels;
- }
-
- /**
- * 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} [ignoreEveryone=false] Whether to ignore everyone/here mentions
- */
-
- /**
- * Checks if a user, guild member, role, or channel is mentioned.
- * Takes into account user mentions, role mentions, 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, ignoreEveryone = false } = {}) {
- if (!ignoreEveryone && this.everyone) return true;
- const { GuildMember } = require('./GuildMember');
- if (!ignoreRoles && data instanceof GuildMember) {
- for (const role of this.roles.values()) if (data.roles.cache.has(role.id)) return true;
- }
-
- if (!ignoreDirect) {
- const id =
- this.guild?.roles.resolveId(data) ?? this.client.channels.resolveId(data) ?? this.client.users.resolveId(data);
-
- return typeof id === 'string' && (this.users.has(id) || this.channels.has(id) || this.roles.has(id));
- }
-
- return false;
- }
-
- toJSON() {
- return Util.flatten(this, {
- members: true,
- channels: true,
- });
- }
-}
-
-/**
- * Regular expression that globally matches `@everyone` and `@here`
- * @type {RegExp}
- */
-MessageMentions.EVERYONE_PATTERN = /@(everyone|here)/g;
-
-/**
- * Regular expression that globally matches user mentions like `<@81440962496172032>`
- * @type {RegExp}
- */
-MessageMentions.USERS_PATTERN = /<@!?(\d{17,19})>/g;
-
-/**
- * Regular expression that globally matches role mentions like `<@&297577916114403338>`
- * @type {RegExp}
- */
-MessageMentions.ROLES_PATTERN = /<@&(\d{17,19})>/g;
-
-/**
- * Regular expression that globally matches channel mentions like `<#222079895583457280>`
- * @type {RegExp}
- */
-MessageMentions.CHANNELS_PATTERN = /<#(\d{17,19})>/g;
-
-module.exports = MessageMentions;
+'use strict';
+
+const { Collection } = require('@discordjs/collection');
+const { ChannelTypes } = require('../util/Constants');
+const Util = require('../util/Util');
+
+/**
+ * Keeps track of mentions in a {@link Message}.
+ */
+class MessageMentions {
+ 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
+ * Order as received from the API, not as they appear in the message content
+ * @type {Collection}
+ */
+ 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
+ * Order as received from the API, not as they appear in the message content
+ * @type {Collection}
+ */
+ 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}
+ * @private
+ */
+ this._members = null;
+
+ /**
+ * Cached channels for {@link MessageMentions#channels}
+ * @type {?Collection}
+ * @private
+ */
+ this._channels = 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
+ * Order as received from the API, not as they appear in the message content
+ * @type {Collection}
+ */
+ this.crosspostedChannels = new Collection(crosspostedChannels);
+ } else {
+ this.crosspostedChannels = new Collection();
+ const channelTypes = Object.keys(ChannelTypes);
+ for (const d of crosspostedChannels) {
+ const type = channelTypes[d.type];
+ this.crosspostedChannels.set(d.id, {
+ channelId: d.id,
+ guildId: d.guild_id,
+ type: type ?? 'UNKNOWN',
+ 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)
+ * Order as received from the API, not as they appear in the message content
+ * @type {?Collection}
+ * @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
+ * Order as they appear first in the message content
+ * @type {Collection}
+ * @readonly
+ */
+ get channels() {
+ if (this._channels) return this._channels;
+ this._channels = new Collection();
+ let matches;
+ while ((matches = this.constructor.CHANNELS_PATTERN.exec(this._content)) !== null) {
+ const chan = this.client.channels.cache.get(matches[1]);
+ if (chan) this._channels.set(chan.id, chan);
+ }
+ return this._channels;
+ }
+
+ /**
+ * 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} [ignoreEveryone=false] Whether to ignore everyone/here mentions
+ */
+
+ /**
+ * Checks if a user, guild member, role, or channel is mentioned.
+ * Takes into account user mentions, role mentions, 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, ignoreEveryone = false } = {}) {
+ if (!ignoreEveryone && this.everyone) return true;
+ const { GuildMember } = require('./GuildMember');
+ if (!ignoreRoles && data instanceof GuildMember) {
+ for (const role of this.roles.values()) if (data.roles.cache.has(role.id)) return true;
+ }
+
+ if (!ignoreDirect) {
+ const id =
+ this.guild?.roles.resolveId(data) ?? this.client.channels.resolveId(data) ?? this.client.users.resolveId(data);
+
+ return typeof id === 'string' && (this.users.has(id) || this.channels.has(id) || this.roles.has(id));
+ }
+
+ return false;
+ }
+
+ toJSON() {
+ return Util.flatten(this, {
+ members: true,
+ channels: true,
+ });
+ }
+}
+
+/**
+ * Regular expression that globally matches `@everyone` and `@here`
+ * @type {RegExp}
+ */
+MessageMentions.EVERYONE_PATTERN = /@(everyone|here)/g;
+
+/**
+ * Regular expression that globally matches user mentions like `<@81440962496172032>`
+ * @type {RegExp}
+ */
+MessageMentions.USERS_PATTERN = /<@!?(\d{17,19})>/g;
+
+/**
+ * Regular expression that globally matches role mentions like `<@&297577916114403338>`
+ * @type {RegExp}
+ */
+MessageMentions.ROLES_PATTERN = /<@&(\d{17,19})>/g;
+
+/**
+ * Regular expression that globally matches channel mentions like `<#222079895583457280>`
+ * @type {RegExp}
+ */
+MessageMentions.CHANNELS_PATTERN = /<#(\d{17,19})>/g;
+
+module.exports = MessageMentions;
diff --git a/src/structures/MessagePayload.js b/src/structures/MessagePayload.js
index c685b9e..2a0596a 100644
--- a/src/structures/MessagePayload.js
+++ b/src/structures/MessagePayload.js
@@ -1,313 +1,313 @@
-'use strict';
-
-const { Buffer } = require('node:buffer');
-const BaseMessageComponent = require('./BaseMessageComponent');
-const MessageEmbed = require('./MessageEmbed');
-const WebEmbed = require('./WebEmbed');
-const { RangeError } = require('../errors');
-const DataResolver = require('../util/DataResolver');
-const MessageFlags = require('../util/MessageFlags');
-const Util = require('../util/Util');
-
-/**
- * Represents a message to be sent to the API.
- */
-class MessagePayload {
- /**
- * @param {MessageTarget} target The target for this message to be sent to
- * @param {MessageOptions|WebhookMessageOptions} options Options passed in from send
- */
- constructor(target, options) {
- /**
- * The target for this message to be sent to
- * @type {MessageTarget}
- */
- this.target = target;
-
- /**
- * Options passed in from send
- * @type {MessageOptions|WebhookMessageOptions}
- */
- this.options = options;
-
- /**
- * Data sendable to the API
- * @type {?APIMessage}
- */
- this.data = null;
-
- /**
- * @typedef {Object} MessageFile
- * @property {Buffer|string|Stream} attachment The original attachment that generated this file
- * @property {string} name The name of this file
- * @property {Buffer|Stream} file The file to be sent to the API
- */
-
- /**
- * Files sendable to the API
- * @type {?MessageFile[]}
- */
- 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 Interaction} or an {@link InteractionWebhook}
- * @type {boolean}
- * @readonly
- */
- get isInteraction() {
- const Interaction = require('./Interaction');
- const InteractionWebhook = require('./InteractionWebhook');
- return this.target instanceof Interaction || this.target instanceof InteractionWebhook;
- }
-
- /**
- * Makes the content of this message.
- * @returns {?string}
- */
- makeContent() {
- let content;
- if (this.options.content === null) {
- content = '';
- } else if (typeof this.options.content !== 'undefined') {
- content = Util.verifyString(this.options.content, RangeError, 'MESSAGE_CONTENT_TYPE', false);
- }
-
- return content;
- }
-
- /**
- * Resolves data.
- * @returns {MessagePayload}
- */
- async resolveData() {
- if (this.data) return this;
- const isInteraction = this.isInteraction;
- const isWebhook = this.isWebhook;
-
- let content = this.makeContent();
- const tts = Boolean(this.options.tts);
-
- let nonce;
- if (typeof this.options.nonce !== 'undefined') {
- nonce = this.options.nonce;
- // eslint-disable-next-line max-len
- if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') {
- throw new RangeError('MESSAGE_NONCE_TYPE');
- }
- }
-
- const components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON());
-
- let username;
- let avatarURL;
- if (isWebhook) {
- username = this.options.username ?? this.target.name;
- if (this.options.avatarURL) avatarURL = this.options.avatarURL;
- }
-
- let flags;
- if (this.isMessage || this.isMessageManager) {
- // eslint-disable-next-line eqeqeq
- flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags?.bitfield;
- } else if (isInteraction && this.options.ephemeral) {
- flags = MessageFlags.FLAGS.EPHEMERAL;
- }
-
- let allowedMentions =
- typeof this.options.allowedMentions === 'undefined'
- ? this.target.client.options.allowedMentions
- : this.options.allowedMentions;
-
- if (allowedMentions) {
- allowedMentions = Util.cloneObject(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;
- }
-
- if (this.options.embeds) {
- if (!Array.isArray(this.options.embeds)) {
- this.options.embeds = [this.options.embeds];
- }
-
- const webembeds = this.options.embeds.filter(
- (e) => !(e instanceof MessageEmbed),
- );
- this.options.embeds = this.options.embeds.filter(e => e instanceof MessageEmbed);
-
- if (webembeds.length > 0) {
- // add hidden embed link
- content += `\n${WebEmbed.hiddenEmbed} \n`;
- if (webembeds.length > 1) {
- console.warn('Multiple webembeds are not supported, this will be ignored.');
- }
- // const embed = webembeds[0];
- for (const webE of webembeds) {
- const data = await webE.toMessage();
- content += `\n${data}`;
- }
- }
- // Check content
- if (content.length > 2000) {
- console.warn(`[WARN] Content is longer than 2000 characters.`);
- }
- if (content.length > 4000) { // Max length if user has nitro boost
- throw new RangeError('MESSAGE_EMBED_LINK_LENGTH');
- }
- }
-
- this.data = {
- content,
- tts,
- nonce,
- embeds: this.options.embeds?.map(embed => new MessageEmbed(embed).toJSON()),
- components,
- username,
- avatar_url: avatarURL,
- allowed_mentions:
- typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions,
- flags,
- message_reference,
- attachments: this.options.attachments,
- sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker),
- };
- return this;
- }
-
- /**
- * Resolves files.
- * @returns {Promise}
- */
- 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 {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file
- * @returns {Promise}
- */
- static async resolveFile(fileLike) {
- let attachment;
- let name;
-
- const findName = thing => {
- if (typeof thing === 'string') {
- return Util.basename(thing);
- }
-
- if (thing.path) {
- return Util.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 resource = await DataResolver.resolveFile(attachment);
- return { attachment, name, file: resource };
- }
-
- /**
- * Creates a {@link MessagePayload} from user-level arguments.
- * @param {MessageTarget} target Target to send to
- * @param {string|MessageOptions|WebhookMessageOptions} options Options or content to use
- * @param {MessageOptions|WebhookMessageOptions} [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 {TextChannel|DMChannel|User|GuildMember|Webhook|WebhookClient|Interaction|InteractionWebhook|
- * Message|MessageManager} MessageTarget
- */
-
-/**
- * @external APIMessage
- * @see {@link https://discord.com/developers/docs/resources/channel#message-object}
- */
+'use strict';
+
+const { Buffer } = require('node:buffer');
+const BaseMessageComponent = require('./BaseMessageComponent');
+const MessageEmbed = require('./MessageEmbed');
+const WebEmbed = require('./WebEmbed');
+const { RangeError } = require('../errors');
+const DataResolver = require('../util/DataResolver');
+const MessageFlags = require('../util/MessageFlags');
+const Util = require('../util/Util');
+
+/**
+ * Represents a message to be sent to the API.
+ */
+class MessagePayload {
+ /**
+ * @param {MessageTarget} target The target for this message to be sent to
+ * @param {MessageOptions|WebhookMessageOptions} options Options passed in from send
+ */
+ constructor(target, options) {
+ /**
+ * The target for this message to be sent to
+ * @type {MessageTarget}
+ */
+ this.target = target;
+
+ /**
+ * Options passed in from send
+ * @type {MessageOptions|WebhookMessageOptions}
+ */
+ this.options = options;
+
+ /**
+ * Data sendable to the API
+ * @type {?APIMessage}
+ */
+ this.data = null;
+
+ /**
+ * @typedef {Object} MessageFile
+ * @property {Buffer|string|Stream} attachment The original attachment that generated this file
+ * @property {string} name The name of this file
+ * @property {Buffer|Stream} file The file to be sent to the API
+ */
+
+ /**
+ * Files sendable to the API
+ * @type {?MessageFile[]}
+ */
+ 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 Interaction} or an {@link InteractionWebhook}
+ * @type {boolean}
+ * @readonly
+ */
+ get isInteraction() {
+ const Interaction = require('./Interaction');
+ const InteractionWebhook = require('./InteractionWebhook');
+ return this.target instanceof Interaction || this.target instanceof InteractionWebhook;
+ }
+
+ /**
+ * Makes the content of this message.
+ * @returns {?string}
+ */
+ makeContent() {
+ let content;
+ if (this.options.content === null) {
+ content = '';
+ } else if (typeof this.options.content !== 'undefined') {
+ content = Util.verifyString(this.options.content, RangeError, 'MESSAGE_CONTENT_TYPE', false);
+ }
+
+ return content;
+ }
+
+ /**
+ * Resolves data.
+ * @returns {MessagePayload}
+ */
+ async resolveData() {
+ if (this.data) return this;
+ const isInteraction = this.isInteraction;
+ const isWebhook = this.isWebhook;
+
+ let content = this.makeContent();
+ const tts = Boolean(this.options.tts);
+
+ let nonce;
+ if (typeof this.options.nonce !== 'undefined') {
+ nonce = this.options.nonce;
+ // eslint-disable-next-line max-len
+ if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') {
+ throw new RangeError('MESSAGE_NONCE_TYPE');
+ }
+ }
+
+ const components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON());
+
+ let username;
+ let avatarURL;
+ if (isWebhook) {
+ username = this.options.username ?? this.target.name;
+ if (this.options.avatarURL) avatarURL = this.options.avatarURL;
+ }
+
+ let flags;
+ if (this.isMessage || this.isMessageManager) {
+ // eslint-disable-next-line eqeqeq
+ flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags?.bitfield;
+ } else if (isInteraction && this.options.ephemeral) {
+ flags = MessageFlags.FLAGS.EPHEMERAL;
+ }
+
+ let allowedMentions =
+ typeof this.options.allowedMentions === 'undefined'
+ ? this.target.client.options.allowedMentions
+ : this.options.allowedMentions;
+
+ if (allowedMentions) {
+ allowedMentions = Util.cloneObject(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;
+ }
+
+ if (this.options.embeds) {
+ if (!Array.isArray(this.options.embeds)) {
+ this.options.embeds = [this.options.embeds];
+ }
+
+ const webembeds = this.options.embeds.filter(
+ (e) => !(e instanceof MessageEmbed),
+ );
+ this.options.embeds = this.options.embeds.filter(e => e instanceof MessageEmbed);
+
+ if (webembeds.length > 0) {
+ // add hidden embed link
+ content += `\n${WebEmbed.hiddenEmbed} \n`;
+ if (webembeds.length > 1) {
+ console.warn('Multiple webembeds are not supported, this will be ignored.');
+ }
+ // const embed = webembeds[0];
+ for (const webE of webembeds) {
+ const data = await webE.toMessage();
+ content += `\n${data}`;
+ }
+ }
+ // Check content
+ if (content.length > 2000) {
+ console.warn(`[WARN] Content is longer than 2000 characters.`);
+ }
+ if (content.length > 4000) { // Max length if user has nitro boost
+ throw new RangeError('MESSAGE_EMBED_LINK_LENGTH');
+ }
+ }
+
+ this.data = {
+ content,
+ tts,
+ nonce,
+ embeds: this.options.embeds?.map(embed => new MessageEmbed(embed).toJSON()),
+ components,
+ username,
+ avatar_url: avatarURL,
+ allowed_mentions:
+ typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions,
+ flags,
+ message_reference,
+ attachments: this.options.attachments,
+ sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker),
+ };
+ return this;
+ }
+
+ /**
+ * Resolves files.
+ * @returns {Promise}
+ */
+ 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 {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file
+ * @returns {Promise}
+ */
+ static async resolveFile(fileLike) {
+ let attachment;
+ let name;
+
+ const findName = thing => {
+ if (typeof thing === 'string') {
+ return Util.basename(thing);
+ }
+
+ if (thing.path) {
+ return Util.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 resource = await DataResolver.resolveFile(attachment);
+ return { attachment, name, file: resource };
+ }
+
+ /**
+ * Creates a {@link MessagePayload} from user-level arguments.
+ * @param {MessageTarget} target Target to send to
+ * @param {string|MessageOptions|WebhookMessageOptions} options Options or content to use
+ * @param {MessageOptions|WebhookMessageOptions} [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 {TextChannel|DMChannel|User|GuildMember|Webhook|WebhookClient|Interaction|InteractionWebhook|
+ * Message|MessageManager} MessageTarget
+ */
+
+/**
+ * @external APIMessage
+ * @see {@link https://discord.com/developers/docs/resources/channel#message-object}
+ */
diff --git a/src/structures/MessageSelectMenu.js b/src/structures/MessageSelectMenu.js
index ac75d58..f84e1c4 100644
--- a/src/structures/MessageSelectMenu.js
+++ b/src/structures/MessageSelectMenu.js
@@ -1,255 +1,256 @@
-'use strict';
-
-const BaseMessageComponent = require('./BaseMessageComponent');
-const { MessageComponentTypes } = require('../util/Constants');
-const Util = require('../util/Util');
-const { Message } = require('./Message');
-
-/**
- * Represents a select menu message component
- * @extends {BaseMessageComponent}
- */
-class MessageSelectMenu extends BaseMessageComponent {
- /**
- * @typedef {BaseMessageComponentOptions} MessageSelectMenuOptions
- * @property {string} [customId] A unique string to be sent in the interaction when clicked
- * @property {string} [placeholder] Custom placeholder text to display when nothing is selected
- * @property {number} [minValues] The minimum number of selections required
- * @property {number} [maxValues] The maximum number of selections allowed
- * @property {MessageSelectOption[]} [options] Options for the select menu
- * @property {boolean} [disabled=false] Disables the select menu to prevent interactions
- */
-
- /**
- * @typedef {Object} MessageSelectOption
- * @property {string} label The text to be displayed on this option
- * @property {string} value The value to be sent for this option
- * @property {?string} description Optional description to show for this option
- * @property {?RawEmoji} emoji Emoji to display for this option
- * @property {boolean} default Render this option as the default selection
- */
-
- /**
- * @typedef {Object} MessageSelectOptionData
- * @property {string} label The text to be displayed on this option
- * @property {string} value The value to be sent for this option
- * @property {string} [description] Optional description to show for this option
- * @property {EmojiIdentifierResolvable} [emoji] Emoji to display for this option
- * @property {boolean} [default] Render this option as the default selection
- */
-
- /**
- * @param {MessageSelectMenu|MessageSelectMenuOptions} [data={}] MessageSelectMenu to clone or raw data
- */
- constructor(data = {}) {
- super({ type: 'SELECT_MENU' });
-
- this.setup(data);
- }
-
- setup(data) {
- /**
- * A unique string to be sent in the interaction when clicked
- * @type {?string}
- */
- this.customId = data.custom_id ?? data.customId ?? null;
-
- /**
- * Custom placeholder text to display when nothing is selected
- * @type {?string}
- */
- this.placeholder = data.placeholder ?? null;
-
- /**
- * The minimum number of selections required
- * @type {?number}
- */
- this.minValues = data.min_values ?? data.minValues ?? null;
-
- /**
- * The maximum number of selections allowed
- * @type {?number}
- */
- this.maxValues = data.max_values ?? data.maxValues ?? null;
-
- /**
- * Options for the select menu
- * @type {MessageSelectOption[]}
- */
- this.options = this.constructor.normalizeOptions(data.options ?? []);
-
- /**
- * Whether this select menu is currently disabled
- * @type {boolean}
- */
- this.disabled = data.disabled ?? false;
- }
-
- /**
- * Sets the custom id of this select menu
- * @param {string} customId A unique string to be sent in the interaction when clicked
- * @returns {MessageSelectMenu}
- */
- setCustomId(customId) {
- this.customId = Util.verifyString(customId, RangeError, 'SELECT_MENU_CUSTOM_ID');
- return this;
- }
-
- /**
- * Sets the interactive status of the select menu
- * @param {boolean} [disabled=true] Whether this select menu should be disabled
- * @returns {MessageSelectMenu}
- */
- setDisabled(disabled = true) {
- this.disabled = disabled;
- return this;
- }
-
- /**
- * Sets the maximum number of selections allowed for this select menu
- * @param {number} maxValues Number of selections to be allowed
- * @returns {MessageSelectMenu}
- */
- setMaxValues(maxValues) {
- this.maxValues = maxValues;
- return this;
- }
-
- /**
- * Sets the minimum number of selections required for this select menu
- * This will default the maxValues to the number of options, unless manually set
- * @param {number} minValues Number of selections to be required
- * @returns {MessageSelectMenu}
- */
- setMinValues(minValues) {
- this.minValues = minValues;
- return this;
- }
-
- /**
- * Sets the placeholder of this select menu
- * @param {string} placeholder Custom placeholder text to display when nothing is selected
- * @returns {MessageSelectMenu}
- */
- setPlaceholder(placeholder) {
- this.placeholder = Util.verifyString(placeholder, RangeError, 'SELECT_MENU_PLACEHOLDER');
- return this;
- }
-
- /**
- * Adds options to the select menu.
- * @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The options to add
- * @returns {MessageSelectMenu}
- */
- addOptions(...options) {
- this.options.push(...this.constructor.normalizeOptions(options));
- return this;
- }
-
- /**
- * Sets the options of the select menu.
- * @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The options to set
- * @returns {MessageSelectMenu}
- */
- setOptions(...options) {
- this.spliceOptions(0, this.options.length, options);
- return this;
- }
-
- /**
- * Removes, replaces, and inserts options in the select menu.
- * @param {number} index The index to start at
- * @param {number} deleteCount The number of options to remove
- * @param {...MessageSelectOptionData|MessageSelectOptionData[]} [options] The replacing option objects
- * @returns {MessageSelectMenu}
- */
- spliceOptions(index, deleteCount, ...options) {
- this.options.splice(index, deleteCount, ...this.constructor.normalizeOptions(...options));
- return this;
- }
-
- /**
- * Transforms the select menu into a plain object
- * @returns {APIMessageSelectMenu} The raw data of this select menu
- */
- toJSON() {
- return {
- custom_id: this.customId,
- disabled: this.disabled,
- placeholder: this.placeholder,
- min_values: this.minValues,
- max_values: this.maxValues ?? (this.minValues ? this.options.length : undefined),
- options: this.options,
- type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type,
- };
- }
-
- /**
- * Normalizes option input and resolves strings and emojis.
- * @param {MessageSelectOptionData} option The select menu option to normalize
- * @returns {MessageSelectOption}
- */
- static normalizeOption(option) {
- let { label, value, description, emoji } = option;
-
- label = Util.verifyString(label, RangeError, 'SELECT_OPTION_LABEL');
- value = Util.verifyString(value, RangeError, 'SELECT_OPTION_VALUE');
- emoji = emoji ? Util.resolvePartialEmoji(emoji) : null;
- description = description ? Util.verifyString(description, RangeError, 'SELECT_OPTION_DESCRIPTION', true) : null;
-
- return { label, value, description, emoji, default: option.default ?? false };
- }
-
- /**
- * Normalizes option input and resolves strings and emojis.
- * @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The select menu options to normalize
- * @returns {MessageSelectOption[]}
- */
- static normalizeOptions(...options) {
- return options.flat(Infinity).map(option => this.normalizeOption(option));
- }
- // Add
- /**
- * Select in menu
- * @param {Message} message Discord Message
- * @param {Array} values Option values
- * @returns {Promise For loop
- if (values.length < this.minValues) throw new RangeError("[SELECT_MENU_MIN_VALUES] The minimum number of values is " + this.minValues);
- if (values.length > this.maxValues) throw new RangeError("[SELECT_MENU_MAX_VALUES] The maximum number of values is " + this.maxValues);
- const validValue = this.options.map(obj => obj.value);
- const check_ = await values.find(element => {
- if (typeof element !== 'string') return true;
- if (!validValue.includes(element)) return true;
- return false;
- })
- if (check_) throw new RangeError("[SELECT_MENU_INVALID_VALUE] The value " + check_ + " is invalid. Please use a valid value " + validValue.join(', '));
- await message.client.api.interactions.post({
- data: {
- type: 3, // ?
- guild_id: message.guild?.id ?? null, // In DMs
- channel_id: message.channel.id,
- message_id: message.id,
- application_id: message.author.id,
- session_id: message.client.session_id,
- data: {
- component_type: 3, // Select Menu
- custom_id: this.customId,
- type: 3, // Select Menu
- values,
- },
- message_flags: message.flags.bitfield,
- },
- });
- return true;
- }
-}
-
-module.exports = MessageSelectMenu;
+'use strict';
+
+const BaseMessageComponent = require('./BaseMessageComponent');
+const { MessageComponentTypes } = require('../util/Constants');
+const Util = require('../util/Util');
+const { Message } = require('./Message');
+
+/**
+ * Represents a select menu message component
+ * @extends {BaseMessageComponent}
+ */
+class MessageSelectMenu extends BaseMessageComponent {
+ /**
+ * @typedef {BaseMessageComponentOptions} MessageSelectMenuOptions
+ * @property {string} [customId] A unique string to be sent in the interaction when clicked
+ * @property {string} [placeholder] Custom placeholder text to display when nothing is selected
+ * @property {number} [minValues] The minimum number of selections required
+ * @property {number} [maxValues] The maximum number of selections allowed
+ * @property {MessageSelectOption[]} [options] Options for the select menu
+ * @property {boolean} [disabled=false] Disables the select menu to prevent interactions
+ */
+
+ /**
+ * @typedef {Object} MessageSelectOption
+ * @property {string} label The text to be displayed on this option
+ * @property {string} value The value to be sent for this option
+ * @property {?string} description Optional description to show for this option
+ * @property {?RawEmoji} emoji Emoji to display for this option
+ * @property {boolean} default Render this option as the default selection
+ */
+
+ /**
+ * @typedef {Object} MessageSelectOptionData
+ * @property {string} label The text to be displayed on this option
+ * @property {string} value The value to be sent for this option
+ * @property {string} [description] Optional description to show for this option
+ * @property {EmojiIdentifierResolvable} [emoji] Emoji to display for this option
+ * @property {boolean} [default] Render this option as the default selection
+ */
+
+ /**
+ * @param {MessageSelectMenu|MessageSelectMenuOptions} [data={}] MessageSelectMenu to clone or raw data
+ */
+ constructor(data = {}) {
+ super({ type: 'SELECT_MENU' });
+
+ this.setup(data);
+ }
+
+ setup(data) {
+ /**
+ * A unique string to be sent in the interaction when clicked
+ * @type {?string}
+ */
+ this.customId = data.custom_id ?? data.customId ?? null;
+
+ /**
+ * Custom placeholder text to display when nothing is selected
+ * @type {?string}
+ */
+ this.placeholder = data.placeholder ?? null;
+
+ /**
+ * The minimum number of selections required
+ * @type {?number}
+ */
+ this.minValues = data.min_values ?? data.minValues ?? null;
+
+ /**
+ * The maximum number of selections allowed
+ * @type {?number}
+ */
+ this.maxValues = data.max_values ?? data.maxValues ?? null;
+
+ /**
+ * Options for the select menu
+ * @type {MessageSelectOption[]}
+ */
+ this.options = this.constructor.normalizeOptions(data.options ?? []);
+
+ /**
+ * Whether this select menu is currently disabled
+ * @type {boolean}
+ */
+ this.disabled = data.disabled ?? false;
+ }
+
+ /**
+ * Sets the custom id of this select menu
+ * @param {string} customId A unique string to be sent in the interaction when clicked
+ * @returns {MessageSelectMenu}
+ */
+ setCustomId(customId) {
+ this.customId = Util.verifyString(customId, RangeError, 'SELECT_MENU_CUSTOM_ID');
+ return this;
+ }
+
+ /**
+ * Sets the interactive status of the select menu
+ * @param {boolean} [disabled=true] Whether this select menu should be disabled
+ * @returns {MessageSelectMenu}
+ */
+ setDisabled(disabled = true) {
+ this.disabled = disabled;
+ return this;
+ }
+
+ /**
+ * Sets the maximum number of selections allowed for this select menu
+ * @param {number} maxValues Number of selections to be allowed
+ * @returns {MessageSelectMenu}
+ */
+ setMaxValues(maxValues) {
+ this.maxValues = maxValues;
+ return this;
+ }
+
+ /**
+ * Sets the minimum number of selections required for this select menu
+ * This will default the maxValues to the number of options, unless manually set
+ * @param {number} minValues Number of selections to be required
+ * @returns {MessageSelectMenu}
+ */
+ setMinValues(minValues) {
+ this.minValues = minValues;
+ return this;
+ }
+
+ /**
+ * Sets the placeholder of this select menu
+ * @param {string} placeholder Custom placeholder text to display when nothing is selected
+ * @returns {MessageSelectMenu}
+ */
+ setPlaceholder(placeholder) {
+ this.placeholder = Util.verifyString(placeholder, RangeError, 'SELECT_MENU_PLACEHOLDER');
+ return this;
+ }
+
+ /**
+ * Adds options to the select menu.
+ * @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The options to add
+ * @returns {MessageSelectMenu}
+ */
+ addOptions(...options) {
+ this.options.push(...this.constructor.normalizeOptions(options));
+ return this;
+ }
+
+ /**
+ * Sets the options of the select menu.
+ * @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The options to set
+ * @returns {MessageSelectMenu}
+ */
+ setOptions(...options) {
+ this.spliceOptions(0, this.options.length, options);
+ return this;
+ }
+
+ /**
+ * Removes, replaces, and inserts options in the select menu.
+ * @param {number} index The index to start at
+ * @param {number} deleteCount The number of options to remove
+ * @param {...MessageSelectOptionData|MessageSelectOptionData[]} [options] The replacing option objects
+ * @returns {MessageSelectMenu}
+ */
+ spliceOptions(index, deleteCount, ...options) {
+ this.options.splice(index, deleteCount, ...this.constructor.normalizeOptions(...options));
+ return this;
+ }
+
+ /**
+ * Transforms the select menu into a plain object
+ * @returns {APIMessageSelectMenu} The raw data of this select menu
+ */
+ toJSON() {
+ return {
+ custom_id: this.customId,
+ disabled: this.disabled,
+ placeholder: this.placeholder,
+ min_values: this.minValues,
+ max_values: this.maxValues ?? (this.minValues ? this.options.length : undefined),
+ options: this.options,
+ type: typeof this.type === 'string' ? MessageComponentTypes[this.type] : this.type,
+ };
+ }
+
+ /**
+ * Normalizes option input and resolves strings and emojis.
+ * @param {MessageSelectOptionData} option The select menu option to normalize
+ * @returns {MessageSelectOption}
+ */
+ static normalizeOption(option) {
+ let { label, value, description, emoji } = option;
+
+ label = Util.verifyString(label, RangeError, 'SELECT_OPTION_LABEL');
+ value = Util.verifyString(value, RangeError, 'SELECT_OPTION_VALUE');
+ emoji = emoji ? Util.resolvePartialEmoji(emoji) : null;
+ description = description ? Util.verifyString(description, RangeError, 'SELECT_OPTION_DESCRIPTION', true) : null;
+
+ return { label, value, description, emoji, default: option.default ?? false };
+ }
+
+ /**
+ * Normalizes option input and resolves strings and emojis.
+ * @param {...MessageSelectOptionData|MessageSelectOptionData[]} options The select menu options to normalize
+ * @returns {MessageSelectOption[]}
+ */
+ static normalizeOptions(...options) {
+ return options.flat(Infinity).map(option => this.normalizeOption(option));
+ }
+ // Add
+ /**
+ * Select in menu
+ * @param {Message} message Discord Message
+ * @param {Array} values Option values
+ * @returns {Promise For loop
+ if (values.length < this.minValues) throw new RangeError("[SELECT_MENU_MIN_VALUES] The minimum number of values is " + this.minValues);
+ if (values.length > this.maxValues) throw new RangeError("[SELECT_MENU_MAX_VALUES] The maximum number of values is " + this.maxValues);
+ const validValue = this.options.map(obj => obj.value);
+ const check_ = await values.find(element => {
+ if (typeof element !== 'string') return true;
+ if (!validValue.includes(element)) return true;
+ return false;
+ })
+ if (check_) throw new RangeError("[SELECT_MENU_INVALID_VALUE] The value " + check_ + " is invalid. Please use a valid value " + validValue.join(', '));
+ await message.client.api.interactions.post(
+ {
+ data: {
+ type: 3, // ?
+ guild_id: message.guild?.id ?? null, // In DMs
+ channel_id: message.channel.id,
+ message_id: message.id,
+ application_id: message.author.id,
+ session_id: message.client.session_id,
+ data: {
+ component_type: 3, // Select Menu
+ custom_id: this.customId,
+ type: 3, // Select Menu
+ values,
+ },
+ }
+ }
+ )
+ return true;
+ }
+}
+
+module.exports = MessageSelectMenu;
diff --git a/src/structures/WebEmbed.js b/src/structures/WebEmbed.js
index 07d0627..ec9a9c6 100644
--- a/src/structures/WebEmbed.js
+++ b/src/structures/WebEmbed.js
@@ -1,328 +1,323 @@
-'use strict';
-const axios = require('axios');
-const baseURL = 'https://embed.benny.fun/?'; // error, not working .-. sad ...
-const hiddenCharter =
- '||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||';
-const { RangeError } = require('../errors');
-const Util = require('../util/Util');
-const process = require('node:process');
-const warn = true;
-
-class WebEmbed {
- constructor(data = {}) {
- if (warn) process.emitWarning(
- 'The WebEmbed constructor encountered a problem with the URL.',
- );
- this._setup(data);
- /**
- * Shorten the link
- * @type {?boolean}
- */
- this.shorten = data.shorten ?? true;
-
- /**
- * Hidden Embed link
- * @type {?boolean}
- */
- this.hidden = data.hidden ?? false;
- }
- _setup(data) {
- /**
- * The title of this embed
- * @type {?string}
- */
- this.title = data.title ?? null;
-
- /**
- * The description of this embed
- * @type {?string}
- */
- this.description = data.description ?? null;
-
- /**
- * The URL of this embed
- * @type {?string}
- */
- this.url = data.url ?? null;
-
- /**
- * The color of this embed
- * @type {?number}
- */
- this.color = 'color' in data ? Util.resolveColor(data.color) : null;
-
- /**
- * Represents the image of a MessageEmbed
- * @typedef {Object} MessageEmbedImage
- * @property {string} url URL for this image
- * @property {string} proxyURL ProxyURL for this image
- * @property {number} height Height of this image
- * @property {number} width Width of this image
- */
-
- /**
- * The image of this embed, if there is one
- * @type {?MessageEmbedImage}
- */
- this.image = data.image
- ? {
- url: data.image.url,
- proxyURL: data.image.proxyURL ?? data.image.proxy_url,
- height: data.image.height,
- width: data.image.width,
- }
- : null;
-
- /**
- * Represents the video of a MessageEmbed
- * @typedef {Object} MessageEmbedVideo
- * @property {string} url URL of this video
- * @property {string} proxyURL ProxyURL for this video
- * @property {number} height Height of this video
- * @property {number} width Width of this video
- */
-
- /**
- * The video of this embed (if there is one)
- * @type {?MessageEmbedVideo}
- * @readonly
- */
- this.video = data.video
- ? {
- url: data.video.url,
- proxyURL: data.video.proxyURL ?? data.video.proxy_url,
- height: data.video.height,
- width: data.video.width,
- }
- : null;
-
- /**
- * Represents the author field of a MessageEmbed
- * @typedef {Object} MessageEmbedAuthor
- * @property {string} name The name of this author
- * @property {string} url URL of this author
- * @property {string} iconURL URL of the icon for this author
- * @property {string} proxyIconURL Proxied URL of the icon for this author
- */
-
- /**
- * The author of this embed (if there is one)
- * @type {?MessageEmbedAuthor}
- */
- this.author = data.author
- ? {
- name: data.author.name,
- url: data.author.url,
- }
- : null;
-
- /**
- * Represents the provider of a MessageEmbed
- * @typedef {Object} MessageEmbedProvider
- * @property {string} name The name of this provider
- * @property {string} url URL of this provider
- */
-
- /**
- * The provider of this embed (if there is one)
- * @type {?MessageEmbedProvider}
- */
- this.provider = data.provider
- ? {
- name: data.provider.name,
- url: data.provider.name,
- }
- : null;
- }
- /**
- * The options to provide for setting an author for a {@link MessageEmbed}.
- * @typedef {Object} EmbedAuthorData
- * @property {string} name The name of this author.
- */
-
- /**
- * Sets the author of this embed.
- * @param {string|EmbedAuthorData|null} options The options to provide for the author.
- * Provide `null` to remove the author data.
- * @returns {MessageEmbed}
- */
- setAuthor(options) {
- if (options === null) {
- this.author = {};
- return this;
- }
- const { name, url } = options;
- this.author = {
- name: Util.verifyString(name, RangeError, 'EMBED_AUTHOR_NAME'),
- url,
- };
- return this;
- }
-
- /**
- * The options to provide for setting an provider for a {@link MessageEmbed}.
- * @typedef {Object} EmbedProviderData
- * @property {string} name The name of this provider.
- */
-
- /**
- * Sets the provider of this embed.
- * @param {string|EmbedProviderData|null} options The options to provide for the provider.
- * Provide `null` to remove the provider data.
- * @returns {MessageEmbed}
- */
- setProvider(options) {
- if (options === null) {
- this.provider = {};
- return this;
- }
- const { name, url } = options;
- this.provider = {
- name: Util.verifyString(name, RangeError, 'EMBED_PROVIDER_NAME'),
- url,
- };
- return this;
- }
-
- /**
- * Sets the color of this embed.
- * @param {ColorResolvable} color The color of the embed
- * @returns {MessageEmbed}
- */
- setColor(color) {
- this.color = Util.resolveColor(color);
- return this;
- }
-
- /**
- * Sets the description of this embed.
- * @param {string} description The description (Limit 350 characters)
- * @returns {MessageEmbed}
- */
- setDescription(description) {
- this.description = Util.verifyString(
- description,
- RangeError,
- 'EMBED_DESCRIPTION',
- );
- return this;
- }
-
- /**
- * Sets the image of this embed.
- * @param {string} url The URL of the image
- * @returns {MessageEmbed}
- */
- setImage(url) {
- this.image = { url };
- return this;
- }
-
- /**
- * Sets the video of this embed.
- * @param {string} url The URL of the video
- * @returns {MessageEmbed}
- */
- setVideo(url) {
- this.video = { url };
- return this;
- }
-
- /**
- * Sets the title of this embed.
- * @param {string} title The title
- * @returns {MessageEmbed}
- */
- setTitle(title) {
- this.title = Util.verifyString(title, RangeError, 'EMBED_TITLE');
- return this;
- }
-
- /**
- * Sets the URL of this embed.
- * @param {string} url The URL
- * @returns {MessageEmbed}
- */
- setURL(url) {
- this.url = url;
- return this;
- }
-
- /**
- * Return Message Content + Embed (if hidden, pls check content length because it has 1000+ length)
- * @returns {string} Message Content
- */
- async toMessage() {
- const arrayQuery = [];
- if (this.title) {
- arrayQuery.push(`title=${encodeURIComponent(this.title)}`);
- }
- if (this.description) {
- arrayQuery.push(
- `description=${encodeURIComponent(this.description)}`,
- );
- }
- if (this.url) {
- arrayQuery.push(`url=${encodeURIComponent(this.url)}`);
- }
- if (this.color) {
- arrayQuery.push(
- `colour=${encodeURIComponent('#' + this.color.toString(16))}`,
- );
- }
- if (this.image?.url) {
- arrayQuery.push(`image=${encodeURIComponent(this.image.url)}`);
- }
- if (this.video?.url) {
- arrayQuery.push(`video=${encodeURIComponent(this.video.url)}`);
- }
- if (this.author) {
- if (this.author.name) arrayQuery.push(
- `author_name=${encodeURIComponent(this.author.name)}`,
- );
- if (this.author.url) arrayQuery.push(
- `author_url=${encodeURIComponent(this.author.url)}`,
- );
- }
- if (this.provider) {
- if (this.provider.name) arrayQuery.push(
- `provider_name=${encodeURIComponent(this.provider.name)}`,
- );
- if (this.provider.url) arrayQuery.push(
- `provider_url=${encodeURIComponent(this.provider.url)}`,
- );
- }
- const fullURL = `${baseURL}${arrayQuery.join('&')}`;
- if (this.shorten) {
- const url = await getShorten(fullURL);
- if (!url) console.log('Cannot shorten URL in WebEmbed');
- return this.hidden ? `${hiddenCharter} ${url || fullURL}` : (url || fullURL);
- } else {
- return this.hidden ? `${hiddenCharter} ${fullURL}` : fullURL;
- }
- }
-}
-
-// Credit: https://www.npmjs.com/package/node-url-shortener + google :))
-const getShorten = async (url) => {
- const APIurl = [
- // 'https://is.gd/create.php?format=simple&url=', :(
- 'https://tinyurl.com/api-create.php?url=',
- 'https://sagiri-fansub.tk/api/v1/short?url=', // my api, pls don't ddos :(
- 'https://lazuee.ga/api/v1/shorten?url='
- // 'https://cdpt.in/shorten?url=', Redirects 5s :(
- ];
- try {
- const res = await axios.get(
- `${
- APIurl[Math.floor(Math.random() * APIurl.length)]
- }${encodeURIComponent(url)}`,
- );
- return `${res.data}`;
- } catch {
- return void 0;
- }
-}
-
-module.exports = WebEmbed;
-module.exports.hiddenEmbed = hiddenCharter;
+'use strict';
+const axios = require('axios');
+const baseURL = 'https://embed.benny.fun/?';
+const hiddenCharter =
+ '||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||';
+const { RangeError } = require('../errors');
+const Util = require('../util/Util');
+
+class WebEmbed {
+ constructor(data = {}) {
+ this._setup(data);
+ /**
+ * Shorten the link
+ * @type {?boolean}
+ */
+ this.shorten = data.shorten ?? true;
+
+ /**
+ * Hidden Embed link
+ * @type {?boolean}
+ */
+ this.hidden = data.hidden ?? false;
+ }
+ _setup(data) {
+ /**
+ * The title of this embed
+ * @type {?string}
+ */
+ this.title = data.title ?? null;
+
+ /**
+ * The description of this embed
+ * @type {?string}
+ */
+ this.description = data.description ?? null;
+
+ /**
+ * The URL of this embed
+ * @type {?string}
+ */
+ this.url = data.url ?? null;
+
+ /**
+ * The color of this embed
+ * @type {?number}
+ */
+ this.color = 'color' in data ? Util.resolveColor(data.color) : null;
+
+ /**
+ * Represents the image of a MessageEmbed
+ * @typedef {Object} MessageEmbedImage
+ * @property {string} url URL for this image
+ * @property {string} proxyURL ProxyURL for this image
+ * @property {number} height Height of this image
+ * @property {number} width Width of this image
+ */
+
+ /**
+ * The image of this embed, if there is one
+ * @type {?MessageEmbedImage}
+ */
+ this.image = data.image
+ ? {
+ url: data.image.url,
+ proxyURL: data.image.proxyURL ?? data.image.proxy_url,
+ height: data.image.height,
+ width: data.image.width,
+ }
+ : null;
+
+ /**
+ * Represents the video of a MessageEmbed
+ * @typedef {Object} MessageEmbedVideo
+ * @property {string} url URL of this video
+ * @property {string} proxyURL ProxyURL for this video
+ * @property {number} height Height of this video
+ * @property {number} width Width of this video
+ */
+
+ /**
+ * The video of this embed (if there is one)
+ * @type {?MessageEmbedVideo}
+ * @readonly
+ */
+ this.video = data.video
+ ? {
+ url: data.video.url,
+ proxyURL: data.video.proxyURL ?? data.video.proxy_url,
+ height: data.video.height,
+ width: data.video.width,
+ }
+ : null;
+
+ /**
+ * Represents the author field of a MessageEmbed
+ * @typedef {Object} MessageEmbedAuthor
+ * @property {string} name The name of this author
+ * @property {string} url URL of this author
+ * @property {string} iconURL URL of the icon for this author
+ * @property {string} proxyIconURL Proxied URL of the icon for this author
+ */
+
+ /**
+ * The author of this embed (if there is one)
+ * @type {?MessageEmbedAuthor}
+ */
+ this.author = data.author
+ ? {
+ name: data.author.name,
+ url: data.author.url,
+ }
+ : null;
+
+ /**
+ * Represents the provider of a MessageEmbed
+ * @typedef {Object} MessageEmbedProvider
+ * @property {string} name The name of this provider
+ * @property {string} url URL of this provider
+ */
+
+ /**
+ * The provider of this embed (if there is one)
+ * @type {?MessageEmbedProvider}
+ */
+ this.provider = data.provider
+ ? {
+ name: data.provider.name,
+ url: data.provider.name,
+ }
+ : null;
+ }
+ /**
+ * The options to provide for setting an author for a {@link MessageEmbed}.
+ * @typedef {Object} EmbedAuthorData
+ * @property {string} name The name of this author.
+ */
+
+ /**
+ * Sets the author of this embed.
+ * @param {string|EmbedAuthorData|null} options The options to provide for the author.
+ * Provide `null` to remove the author data.
+ * @returns {MessageEmbed}
+ */
+ setAuthor(options) {
+ if (options === null) {
+ this.author = {};
+ return this;
+ }
+ const { name, url } = options;
+ this.author = {
+ name: Util.verifyString(name, RangeError, 'EMBED_AUTHOR_NAME'),
+ url,
+ };
+ return this;
+ }
+
+ /**
+ * The options to provide for setting an provider for a {@link MessageEmbed}.
+ * @typedef {Object} EmbedProviderData
+ * @property {string} name The name of this provider.
+ */
+
+ /**
+ * Sets the provider of this embed.
+ * @param {string|EmbedProviderData|null} options The options to provide for the provider.
+ * Provide `null` to remove the provider data.
+ * @returns {MessageEmbed}
+ */
+ setProvider(options) {
+ if (options === null) {
+ this.provider = {};
+ return this;
+ }
+ const { name, url } = options;
+ this.provider = {
+ name: Util.verifyString(name, RangeError, 'EMBED_PROVIDER_NAME'),
+ url,
+ };
+ return this;
+ }
+
+ /**
+ * Sets the color of this embed.
+ * @param {ColorResolvable} color The color of the embed
+ * @returns {MessageEmbed}
+ */
+ setColor(color) {
+ this.color = Util.resolveColor(color);
+ return this;
+ }
+
+ /**
+ * Sets the description of this embed.
+ * @param {string} description The description (Limit 350 characters)
+ * @returns {MessageEmbed}
+ */
+ setDescription(description) {
+ this.description = Util.verifyString(
+ description,
+ RangeError,
+ 'EMBED_DESCRIPTION',
+ );
+ return this;
+ }
+
+ /**
+ * Sets the image of this embed.
+ * @param {string} url The URL of the image
+ * @returns {MessageEmbed}
+ */
+ setImage(url) {
+ this.image = { url };
+ return this;
+ }
+
+ /**
+ * Sets the video of this embed.
+ * @param {string} url The URL of the video
+ * @returns {MessageEmbed}
+ */
+ setVideo(url) {
+ this.video = { url };
+ return this;
+ }
+
+ /**
+ * Sets the title of this embed.
+ * @param {string} title The title
+ * @returns {MessageEmbed}
+ */
+ setTitle(title) {
+ this.title = Util.verifyString(title, RangeError, 'EMBED_TITLE');
+ return this;
+ }
+
+ /**
+ * Sets the URL of this embed.
+ * @param {string} url The URL
+ * @returns {MessageEmbed}
+ */
+ setURL(url) {
+ this.url = url;
+ return this;
+ }
+
+ /**
+ * Return Message Content + Embed (if hidden, pls check content length because it has 1000+ length)
+ * @returns {string} Message Content
+ */
+ async toMessage() {
+ const arrayQuery = [];
+ if (this.title) {
+ arrayQuery.push(`title=${encodeURIComponent(this.title)}`);
+ }
+ if (this.description) {
+ arrayQuery.push(
+ `description=${encodeURIComponent(this.description)}`,
+ );
+ }
+ if (this.url) {
+ arrayQuery.push(`url=${encodeURIComponent(this.url)}`);
+ }
+ if (this.color) {
+ arrayQuery.push(
+ `colour=${encodeURIComponent('#' + this.color.toString(16))}`,
+ );
+ }
+ if (this.image?.url) {
+ arrayQuery.push(`image=${encodeURIComponent(this.image.url)}`);
+ }
+ if (this.video?.url) {
+ arrayQuery.push(`video=${encodeURIComponent(this.video.url)}`);
+ }
+ if (this.author) {
+ if (this.author.name) arrayQuery.push(
+ `author_name=${encodeURIComponent(this.author.name)}`,
+ );
+ if (this.author.url) arrayQuery.push(
+ `author_url=${encodeURIComponent(this.author.url)}`,
+ );
+ }
+ if (this.provider) {
+ if (this.provider.name) arrayQuery.push(
+ `provider_name=${encodeURIComponent(this.provider.name)}`,
+ );
+ if (this.provider.url) arrayQuery.push(
+ `provider_url=${encodeURIComponent(this.provider.url)}`,
+ );
+ }
+ const fullURL = `${baseURL}${arrayQuery.join('&')}`;
+ if (this.shorten) {
+ const url = await getShorten(fullURL);
+ if (!url) console.log('Cannot shorten URL in WebEmbed');
+ return this.hidden ? `${hiddenCharter} ${url || fullURL}` : (url || fullURL);
+ } else {
+ return this.hidden ? `${hiddenCharter} ${fullURL}` : fullURL;
+ }
+ }
+}
+
+// Credit: https://www.npmjs.com/package/node-url-shortener + google :))
+const getShorten = async (url) => {
+ const APIurl = [
+ // 'https://is.gd/create.php?format=simple&url=', :(
+ 'https://tinyurl.com/api-create.php?url=',
+ 'https://sagiri-fansub.tk/api/v1/short?url=', // my api, pls don't ddos :(
+ 'https://lazuee.ga/api/v1/shorten?url='
+ // 'https://cdpt.in/shorten?url=', Redirects 5s :(
+ ];
+ try {
+ const res = await axios.get(
+ `${
+ APIurl[Math.floor(Math.random() * APIurl.length)]
+ }${encodeURIComponent(url)}`,
+ );
+ return `${res.data}`;
+ } catch {
+ return void 0;
+ }
+}
+
+module.exports = WebEmbed;
+module.exports.hiddenEmbed = hiddenCharter;
diff --git a/src/structures/interfaces/Application.js b/src/structures/interfaces/Application.js
index f1c02e0..035a016 100644
--- a/src/structures/interfaces/Application.js
+++ b/src/structures/interfaces/Application.js
@@ -1,136 +1,136 @@
-'use strict';
-
-const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants');
-const SnowflakeUtil = require('../../util/SnowflakeUtil');
-const Base = require('../Base');
-
-const AssetTypes = Object.keys(ClientApplicationAssetTypes);
-
-/**
- * Represents an OAuth2 Application.
- * @abstract
- */
-class Application extends Base {
- constructor(client, data) {
- super(client);
-
- if (data) {
- 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 SnowflakeUtil.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 {StaticImageURLOptions} [options={}] Options for the Image URL
- * @returns {?string}
- */
- iconURL({ format, size } = {}) {
- if (!this.icon) return null;
- return this.client.rest.cdn.AppIcon(this.id, this.icon, { format, size });
- }
-
- /**
- * A link to this application's cover image.
- * @param {StaticImageURLOptions} [options={}] Options for the Image URL
- * @returns {?string}
- */
- coverURL({ format, size } = {}) {
- if (!this.cover) return null;
- return Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.cover, { format, size });
- }
-
- /**
- * Asset data.
- * @typedef {Object} ApplicationAsset
- * @property {Snowflake} id The asset's id
- * @property {string} name The asset's name
- * @property {string} type The asset's type
- */
-
- /**
- * Gets the application's rich presence assets.
- * @returns {Promise>}
- */
- async fetchAssets() {
- const assets = await this.client.api.oauth2.applications(this.id).assets.get();
- return assets.map(a => ({
- id: a.id,
- name: a.name,
- type: AssetTypes[a.type - 1],
- }));
- }
-
- /**
- * 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;
+'use strict';
+
+const { ClientApplicationAssetTypes, Endpoints } = require('../../util/Constants');
+const SnowflakeUtil = require('../../util/SnowflakeUtil');
+const Base = require('../Base');
+
+const AssetTypes = Object.keys(ClientApplicationAssetTypes);
+
+/**
+ * Represents an OAuth2 Application.
+ * @abstract
+ */
+class Application extends Base {
+ constructor(client, data) {
+ super(client);
+
+ if (data) {
+ 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 SnowflakeUtil.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 {StaticImageURLOptions} [options={}] Options for the Image URL
+ * @returns {?string}
+ */
+ iconURL({ format, size } = {}) {
+ if (!this.icon) return null;
+ return this.client.rest.cdn.AppIcon(this.id, this.icon, { format, size });
+ }
+
+ /**
+ * A link to this application's cover image.
+ * @param {StaticImageURLOptions} [options={}] Options for the Image URL
+ * @returns {?string}
+ */
+ coverURL({ format, size } = {}) {
+ if (!this.cover) return null;
+ return Endpoints.CDN(this.client.options.http.cdn).AppIcon(this.id, this.cover, { format, size });
+ }
+
+ /**
+ * Asset data.
+ * @typedef {Object} ApplicationAsset
+ * @property {Snowflake} id The asset's id
+ * @property {string} name The asset's name
+ * @property {string} type The asset's type
+ */
+
+ /**
+ * Gets the application's rich presence assets.
+ * @returns {Promise>}
+ */
+ async fetchAssets() {
+ const assets = await this.client.api.oauth2.applications(this.id).assets.get();
+ return assets.map(a => ({
+ id: a.id,
+ name: a.name,
+ type: AssetTypes[a.type - 1],
+ }));
+ }
+
+ /**
+ * 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/src/structures/interfaces/TextBasedChannel.js b/src/structures/interfaces/TextBasedChannel.js
index e160826..f2fec0a 100644
--- a/src/structures/interfaces/TextBasedChannel.js
+++ b/src/structures/interfaces/TextBasedChannel.js
@@ -1,360 +1,360 @@
-'use strict';
-
-/* eslint-disable import/order */
-const MessageCollector = require('../MessageCollector');
-const MessagePayload = require('../MessagePayload');
-const SnowflakeUtil = require('../../util/SnowflakeUtil');
-const { Collection } = require('@discordjs/collection');
-const { InteractionTypes } = require('../../util/Constants');
-const { TypeError, Error } = require('../../errors');
-const InteractionCollector = require('../InteractionCollector');
-
-/**
- * Interface for classes that have text-channel-like features.
- * @interface
- */
-class TextBasedChannel {
- constructor() {
- /**
- * A manager of the messages sent to this channel
- * @type {MessageManager}
- */
- this.messages = new MessageManager(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) : null;
- }
-
- /**
- * Base options provided when sending.
- * @typedef {Object} BaseMessageOptions
- * @property {boolean} [tts=false] Whether or not the message should be spoken aloud
- * @property {string} [nonce=''] The nonce for the message
- * @property {string} [content=''] The content for the message
- * @property {WebEmbed[]|MessageEmbed[]|APIEmbed[]} [embeds] The embeds for the message
- * (see [here](https://discord.com/developers/docs/resources/channel#embed-object) for more details)
- * @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 {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to send with the message
- * @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
- * Action rows containing interactive components for the message (buttons, select menus)
- * @property {MessageAttachment[]} [attachments] Attachments to send in the message
- */
-
- /**
- * Options provided when sending or editing a message.
- * @typedef {BaseMessageOptions} MessageOptions
- * @property {ReplyOptions} [reply] The options for replying to a message
- * @property {StickerResolvable[]} [stickers=[]] Stickers to send in the 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
- */
-
- /**
- * 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=true] Whether to error if the referenced message
- * does not exist (creates a standard message in this case when false)
- */
-
- /**
- * Sends a message to this channel.
- * @param {string|MessagePayload|MessageOptions} options The options to provide
- * @returns {Promise}
- * @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);
- * @example
- * // Send an embed with a local image inside
- * channel.send({
- * content: 'This is an embed',
- * embeds: [
- * {
- * thumbnail: {
- * url: 'attachment://file.jpg'
- * }
- * }
- * ],
- * 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 = await options.resolveData();
- } else {
- messagePayload = await MessagePayload.create(this, options).resolveData();
- }
-
- const { data, files } = await messagePayload.resolveFiles();
- const d = await this.client.api.channels[this.id].messages.post({ data, files });
-
- return this.messages.cache.get(d.id) ?? this.messages._add(d);
- }
-
- /**
- * Sends a typing indicator in the channel.
- * @returns {Promise} Resolves upon the typing status being sent
- * @example
- * // Start typing in a channel
- * channel.sendTyping();
- */
- async sendTyping() {
- await this.client.api.channels(this.id).typing.post();
- }
-
- /**
- * 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>}
- * @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 button 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: InteractionTypes.MESSAGE_COMPONENT,
- 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}
- * @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 Error('INTERACTION_COLLECTOR_ERROR', reason));
- });
- });
- }
-
- /**
- * Bulk deletes given messages that are newer than two weeks.
- * @param {Collection|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>} 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() - SnowflakeUtil.timestampFrom(id) < 1_209_600_000);
- }
- if (messageIds.length === 0) return new Collection();
- if (messageIds.length === 1) {
- await this.client.api.channels(this.id).messages(messageIds[0]).delete();
- const message = this.client.actions.MessageDelete.getMessage(
- {
- message_id: messageIds[0],
- },
- this,
- );
- return message ? new Collection([[message.id, message]]) : new Collection();
- }
- await this.client.api.channels[this.id].messages['bulk-delete'].post({ data: { 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 TypeError('MESSAGE_BULK_DELETE_TYPE');
- }
-
- static applyToClass(structure, full = false, ignore = []) {
- const props = ['send'];
- if (full) {
- props.push(
- 'lastMessage',
- 'lastPinAt',
- 'bulkDelete',
- 'sendTyping',
- 'createMessageCollector',
- 'awaitMessages',
- 'createMessageComponentCollector',
- 'awaitMessageComponent',
- );
- }
- 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
-const MessageManager = require('../../managers/MessageManager');
+'use strict';
+
+/* eslint-disable import/order */
+const MessageCollector = require('../MessageCollector');
+const MessagePayload = require('../MessagePayload');
+const SnowflakeUtil = require('../../util/SnowflakeUtil');
+const { Collection } = require('@discordjs/collection');
+const { InteractionTypes } = require('../../util/Constants');
+const { TypeError, Error } = require('../../errors');
+const InteractionCollector = require('../InteractionCollector');
+
+/**
+ * Interface for classes that have text-channel-like features.
+ * @interface
+ */
+class TextBasedChannel {
+ constructor() {
+ /**
+ * A manager of the messages sent to this channel
+ * @type {MessageManager}
+ */
+ this.messages = new MessageManager(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) : null;
+ }
+
+ /**
+ * Base options provided when sending.
+ * @typedef {Object} BaseMessageOptions
+ * @property {boolean} [tts=false] Whether or not the message should be spoken aloud
+ * @property {string} [nonce=''] The nonce for the message
+ * @property {string} [content=''] The content for the message
+ * @property {WebEmbed[]|MessageEmbed[]|APIEmbed[]} [embeds] The embeds for the message
+ * (see [here](https://discord.com/developers/docs/resources/channel#embed-object) for more details)
+ * @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 {FileOptions[]|BufferResolvable[]|MessageAttachment[]} [files] Files to send with the message
+ * @property {MessageActionRow[]|MessageActionRowOptions[]} [components]
+ * Action rows containing interactive components for the message (buttons, select menus)
+ * @property {MessageAttachment[]} [attachments] Attachments to send in the message
+ */
+
+ /**
+ * Options provided when sending or editing a message.
+ * @typedef {BaseMessageOptions} MessageOptions
+ * @property {ReplyOptions} [reply] The options for replying to a message
+ * @property {StickerResolvable[]} [stickers=[]] Stickers to send in the 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
+ */
+
+ /**
+ * 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=true] Whether to error if the referenced message
+ * does not exist (creates a standard message in this case when false)
+ */
+
+ /**
+ * Sends a message to this channel.
+ * @param {string|MessagePayload|MessageOptions} options The options to provide
+ * @returns {Promise}
+ * @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);
+ * @example
+ * // Send an embed with a local image inside
+ * channel.send({
+ * content: 'This is an embed',
+ * embeds: [
+ * {
+ * thumbnail: {
+ * url: 'attachment://file.jpg'
+ * }
+ * }
+ * ],
+ * 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 = await options.resolveData();
+ } else {
+ messagePayload = await MessagePayload.create(this, options).resolveData();
+ }
+
+ const { data, files } = await messagePayload.resolveFiles();
+ const d = await this.client.api.channels[this.id].messages.post({ data, files });
+
+ return this.messages.cache.get(d.id) ?? this.messages._add(d);
+ }
+
+ /**
+ * Sends a typing indicator in the channel.
+ * @returns {Promise} Resolves upon the typing status being sent
+ * @example
+ * // Start typing in a channel
+ * channel.sendTyping();
+ */
+ async sendTyping() {
+ await this.client.api.channels(this.id).typing.post();
+ }
+
+ /**
+ * 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>}
+ * @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 button 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: InteractionTypes.MESSAGE_COMPONENT,
+ 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}
+ * @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 Error('INTERACTION_COLLECTOR_ERROR', reason));
+ });
+ });
+ }
+
+ /**
+ * Bulk deletes given messages that are newer than two weeks.
+ * @param {Collection|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>} 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() - SnowflakeUtil.timestampFrom(id) < 1_209_600_000);
+ }
+ if (messageIds.length === 0) return new Collection();
+ if (messageIds.length === 1) {
+ await this.client.api.channels(this.id).messages(messageIds[0]).delete();
+ const message = this.client.actions.MessageDelete.getMessage(
+ {
+ message_id: messageIds[0],
+ },
+ this,
+ );
+ return message ? new Collection([[message.id, message]]) : new Collection();
+ }
+ await this.client.api.channels[this.id].messages['bulk-delete'].post({ data: { 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 TypeError('MESSAGE_BULK_DELETE_TYPE');
+ }
+
+ static applyToClass(structure, full = false, ignore = []) {
+ const props = ['send'];
+ if (full) {
+ props.push(
+ 'lastMessage',
+ 'lastPinAt',
+ 'bulkDelete',
+ 'sendTyping',
+ 'createMessageCollector',
+ 'awaitMessages',
+ 'createMessageComponentCollector',
+ 'awaitMessageComponent',
+ );
+ }
+ 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
+const MessageManager = require('../../managers/MessageManager');