From fba0c875e036f1733bf2d6dc9c49704710128a8a Mon Sep 17 00:00:00 2001
From: March 7th <71698422+aiko-chan-ai@users.noreply.github.com>
Date: Sun, 27 Mar 2022 19:05:43 +0700
Subject: [PATCH] new Method
- message.clickButton()
- message.selectMenu()
---
DOCUMENT.md | 14 +-
package.json | 2 +-
src/structures/Message.js | 1948 +++++++++++++++++++------------------
typings/index.d.ts | 3 +
4 files changed, 1040 insertions(+), 927 deletions(-)
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 {