diff --git a/DOCUMENT.md b/DOCUMENT.md index d04b5d0..c506018 100644 --- a/DOCUMENT.md +++ b/DOCUMENT.md @@ -230,18 +230,22 @@ And you can change the status 5 times every 20 seconds! ## Interaction
-Button Click (v1) +Button Click ```js -await Button.click(Message); // Message has button +await Button.click(Message); // Message has button (v1) +// +await message.clickButton(buttonID); // Message has button (v2) ```
-Message Select Menu (v1) +Message Select Menu ```js -await MessageSelectMenu.select(Message, value); // Message has menu +await MessageSelectMenu.select(Message, options); // Message has menu (v1) // value: ['value1', 'value2' , ...] +await message.selectMenu(menuID, options) // If message has >= 2 menu +await message.selectMenu(options) // If message has 1 menu ```
@@ -261,6 +265,7 @@ messageID: Message.id, await command.sendSlashCommand(Message, ['option1', 'option2']); // Eg: Slash /add role:123456789 user:987654321 // value: ['123456789', '987654321'] +// Channel.sendSlashCommand(botID, commandName, options): Comming soon ! ```
@@ -279,6 +284,7 @@ messageID: Message.id, author: Message.author, */ await command.sendContextMenu(Message); +// Channel.sendContextMenu(botID, commandName): Comming soon ! ```
diff --git a/package.json b/package.json index 480699c..3822d9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "discord.js-selfbot-v13", - "version": "1.1.5", + "version": "1.1.6", "description": "A unofficial discord.js fork for creating selfbots [Based on discord.js v13]", "main": "./src/index.js", "types": "./typings/index.d.ts", diff --git a/src/structures/Message.js b/src/structures/Message.js index 918edf5..8da1100 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -33,185 +33,197 @@ let deprecationEmittedForDeleted = false; * @extends {Base} */ class Message extends Base { - constructor(client, data) { - super(client); + 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 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; + /** + * 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); - } + this._patch(data); + } - _patch(data) { - /** - * The message's id - * @type {Snowflake} - */ - this.id = data.id; + _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); + /** + * 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]; + 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; - } + /** + * 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 ('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 ('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 ('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 ('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 ('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 ('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 ('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 ('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); - } + 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; - } + // 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 ('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} - */ - if (!data.mentions) + if (!this.mentions) { + /** + * All valid mentions that the message contains + * @type {MessageMentions} + */ + if (!data.mentions) this.mentions = new Mentions( this, data.mentions, @@ -220,768 +232,860 @@ class Message extends Base { data.mention_channels, data.referenced_message?.author, ); - else data.mentions instanceof Mentions ? this.mentions = data.mentions : this.mentions = null; - } 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, - }); - } + else + data.mentions instanceof Mentions + ? (this.mentions = data.mentions) + : (this.mentions = null); + } 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); + } } exports.Message = Message; diff --git a/typings/index.d.ts b/typings/index.d.ts index a4bfc3d..3d5aa0c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1578,6 +1578,9 @@ export class Message extends Base { public toString(): string; public unpin(): Promise; public inGuild(): this is Message & this; + // Added + public clickButton(buttonID: String): Promise + public selectMenu(menuID: String | Array, options: Array): Promise } export class MessageActionRow extends BaseMessageComponent {