summaryrefslogtreecommitdiff
path: root/node_modules/discord.js/src/structures/Message.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/discord.js/src/structures/Message.js')
-rw-r--r--node_modules/discord.js/src/structures/Message.js997
1 files changed, 997 insertions, 0 deletions
diff --git a/node_modules/discord.js/src/structures/Message.js b/node_modules/discord.js/src/structures/Message.js
new file mode 100644
index 0000000..c82c177
--- /dev/null
+++ b/node_modules/discord.js/src/structures/Message.js
@@ -0,0 +1,997 @@
+'use strict';
+
+const { messageLink } = require('@discordjs/builders');
+const { Collection } = require('@discordjs/collection');
+const { DiscordSnowflake } = require('@sapphire/snowflake');
+const {
+ InteractionType,
+ ChannelType,
+ MessageType,
+ MessageFlags,
+ PermissionFlagsBits,
+} = require('discord-api-types/v10');
+const Attachment = require('./Attachment');
+const Base = require('./Base');
+const ClientApplication = require('./ClientApplication');
+const Embed = require('./Embed');
+const InteractionCollector = require('./InteractionCollector');
+const Mentions = require('./MessageMentions');
+const MessagePayload = require('./MessagePayload');
+const ReactionCollector = require('./ReactionCollector');
+const { Sticker } = require('./Sticker');
+const { DiscordjsError, ErrorCodes } = require('../errors');
+const ReactionManager = require('../managers/ReactionManager');
+const { createComponent } = require('../util/Components');
+const { NonSystemMessageTypes, MaxBulkDeletableMessageAge, DeletableMessageTypes } = require('../util/Constants');
+const MessageFlagsBitField = require('../util/MessageFlagsBitField');
+const PermissionsBitField = require('../util/PermissionsBitField');
+const { cleanContent, resolvePartialEmoji } = require('../util/Util');
+
+/**
+ * Represents a message on Discord.
+ * @extends {Base}
+ */
+class Message extends Base {
+ constructor(client, data) {
+ super(client);
+
+ /**
+ * The id of the channel the message was sent in
+ * @type {Snowflake}
+ */
+ this.channelId = data.channel_id;
+
+ /**
+ * The id of the guild the message was sent in, if any
+ * @type {?Snowflake}
+ */
+ this.guildId = data.guild_id ?? this.channel?.guild?.id ?? null;
+
+ this._patch(data);
+ }
+
+ _patch(data) {
+ /**
+ * The message's id
+ * @type {Snowflake}
+ */
+ this.id = data.id;
+
+ /**
+ * The timestamp the message was sent at
+ * @type {number}
+ */
+ this.createdTimestamp = DiscordSnowflake.timestampFrom(this.id);
+
+ if ('type' in data) {
+ /**
+ * The type of the message
+ * @type {?MessageType}
+ */
+ this.type = data.type;
+
+ /**
+ * Whether or not this message was sent by Discord, not actually a user (e.g. pin notifications)
+ * @type {?boolean}
+ */
+ this.system = !NonSystemMessageTypes.includes(this.type);
+ } else {
+ this.system ??= null;
+ this.type ??= null;
+ }
+
+ if ('content' in data) {
+ /**
+ * The content of the message.
+ * <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged intent
+ * in a guild for messages that do not mention the client.</info>
+ * @type {?string}
+ */
+ this.content = data.content;
+ } else {
+ this.content ??= null;
+ }
+
+ if ('author' in data) {
+ /**
+ * The author of the message
+ * @type {?User}
+ */
+ this.author = this.client.users._add(data.author, !data.webhook_id);
+ } else {
+ this.author ??= null;
+ }
+
+ if ('pinned' in data) {
+ /**
+ * Whether or not this message is pinned
+ * @type {?boolean}
+ */
+ this.pinned = Boolean(data.pinned);
+ } else {
+ this.pinned ??= null;
+ }
+
+ if ('tts' in data) {
+ /**
+ * Whether or not the message was Text-To-Speech
+ * @type {?boolean}
+ */
+ this.tts = data.tts;
+ } else {
+ this.tts ??= null;
+ }
+
+ if ('nonce' in data) {
+ /**
+ * A random number or string used for checking message delivery
+ * <warn>This is only received after the message was sent successfully, and
+ * lost if re-fetched</warn>
+ * @type {?string}
+ */
+ this.nonce = data.nonce;
+ } else {
+ this.nonce ??= null;
+ }
+
+ if ('embeds' in data) {
+ /**
+ * An array of embeds in the message - e.g. YouTube Player.
+ * <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged intent
+ * in a guild for messages that do not mention the client.</info>
+ * @type {Embed[]}
+ */
+ this.embeds = data.embeds.map(e => new Embed(e));
+ } else {
+ this.embeds = this.embeds?.slice() ?? [];
+ }
+
+ if ('components' in data) {
+ /**
+ * An array of of action rows in the message.
+ * <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged intent
+ * in a guild for messages that do not mention the client.</info>
+ * @type {ActionRow[]}
+ */
+ this.components = data.components.map(c => createComponent(c));
+ } else {
+ this.components = this.components?.slice() ?? [];
+ }
+
+ if ('attachments' in data) {
+ /**
+ * A collection of attachments in the message - e.g. Pictures - mapped by their ids.
+ * <info>This property requires the {@link GatewayIntentBits.MessageContent} privileged intent
+ * in a guild for messages that do not mention the client.</info>
+ * @type {Collection<Snowflake, Attachment>}
+ */
+ this.attachments = new Collection();
+ if (data.attachments) {
+ for (const attachment of data.attachments) {
+ this.attachments.set(attachment.id, new Attachment(attachment));
+ }
+ }
+ } else {
+ this.attachments = new Collection(this.attachments);
+ }
+
+ if ('sticker_items' in data || 'stickers' in data) {
+ /**
+ * A collection of stickers in the message
+ * @type {Collection<Snowflake, Sticker>}
+ */
+ this.stickers = new Collection(
+ (data.sticker_items ?? data.stickers)?.map(s => [s.id, new Sticker(this.client, s)]),
+ );
+ } else {
+ this.stickers = new Collection(this.stickers);
+ }
+
+ if ('position' in data) {
+ /**
+ * A generally increasing integer (there may be gaps or duplicates) that represents
+ * the approximate position of the message in a thread.
+ * @type {?number}
+ */
+ this.position = data.position;
+ } else {
+ this.position ??= null;
+ }
+
+ if ('role_subscription_data' in data) {
+ /**
+ * Role subscription data found on {@link MessageType.RoleSubscriptionPurchase} messages.
+ * @typedef {Object} RoleSubscriptionData
+ * @property {Snowflake} roleSubscriptionListingId The id of the SKU and listing the user is subscribed to
+ * @property {string} tierName The name of the tier the user is subscribed to
+ * @property {number} totalMonthsSubscribed The total number of months the user has been subscribed for
+ * @property {boolean} isRenewal Whether this notification is a renewal
+ */
+
+ /**
+ * The data of the role subscription purchase or renewal.
+ * <info>This is present on {@link MessageType.RoleSubscriptionPurchase} messages.</info>
+ * @type {?RoleSubscriptionData}
+ */
+ this.roleSubscriptionData = {
+ roleSubscriptionListingId: data.role_subscription_data.role_subscription_listing_id,
+ tierName: data.role_subscription_data.tier_name,
+ totalMonthsSubscribed: data.role_subscription_data.total_months_subscribed,
+ isRenewal: data.role_subscription_data.is_renewal,
+ };
+ } else {
+ this.roleSubscriptionData ??= null;
+ }
+
+ // Discord sends null if the message has not been edited
+ if (data.edited_timestamp) {
+ /**
+ * The timestamp the message was last edited at (if applicable)
+ * @type {?number}
+ */
+ this.editedTimestamp = Date.parse(data.edited_timestamp);
+ } else {
+ this.editedTimestamp ??= null;
+ }
+
+ if ('reactions' in data) {
+ /**
+ * A manager of the reactions belonging to this message
+ * @type {ReactionManager}
+ */
+ this.reactions = new ReactionManager(this);
+ if (data.reactions?.length > 0) {
+ for (const reaction of data.reactions) {
+ this.reactions._add(reaction);
+ }
+ }
+ } else {
+ this.reactions ??= new ReactionManager(this);
+ }
+
+ if (!this.mentions) {
+ /**
+ * All valid mentions that the message contains
+ * @type {MessageMentions}
+ */
+ this.mentions = new Mentions(
+ this,
+ data.mentions,
+ data.mention_roles,
+ data.mention_everyone,
+ data.mention_channels,
+ data.referenced_message?.author,
+ );
+ } else {
+ this.mentions = new Mentions(
+ this,
+ data.mentions ?? this.mentions.users,
+ data.mention_roles ?? this.mentions.roles,
+ data.mention_everyone ?? this.mentions.everyone,
+ data.mention_channels ?? this.mentions.crosspostedChannels,
+ data.referenced_message?.author ?? this.mentions.repliedUser,
+ );
+ }
+
+ if ('webhook_id' in data) {
+ /**
+ * The id of the webhook that sent the message, if applicable
+ * @type {?Snowflake}
+ */
+ this.webhookId = data.webhook_id;
+ } else {
+ this.webhookId ??= null;
+ }
+
+ if ('application' in data) {
+ /**
+ * Supplemental application information for group activities
+ * @type {?ClientApplication}
+ */
+ this.groupActivityApplication = new ClientApplication(this.client, data.application);
+ } else {
+ this.groupActivityApplication ??= null;
+ }
+
+ if ('application_id' in data) {
+ /**
+ * The id of the application of the interaction that sent this message, if any
+ * @type {?Snowflake}
+ */
+ this.applicationId = data.application_id;
+ } else {
+ this.applicationId ??= null;
+ }
+
+ if ('activity' in data) {
+ /**
+ * Group activity
+ * @type {?MessageActivity}
+ */
+ this.activity = {
+ partyId: data.activity.party_id,
+ type: data.activity.type,
+ };
+ } else {
+ this.activity ??= null;
+ }
+
+ if ('thread' in data) {
+ this.client.channels._add(data.thread, this.guild);
+ }
+
+ if (this.member && data.member) {
+ this.member._patch(data.member);
+ } else if (data.member && this.guild && this.author) {
+ this.guild.members._add(Object.assign(data.member, { user: this.author }));
+ }
+
+ if ('flags' in data) {
+ /**
+ * Flags that are applied to the message
+ * @type {Readonly<MessageFlagsBitField>}
+ */
+ this.flags = new MessageFlagsBitField(data.flags).freeze();
+ } else {
+ this.flags = new MessageFlagsBitField(this.flags).freeze();
+ }
+
+ /**
+ * Reference data sent in a message that contains ids identifying the referenced message.
+ * This can be present in the following types of message:
+ * * Crossposted messages (`MessageFlags.Crossposted`)
+ * * {@link MessageType.ChannelFollowAdd}
+ * * {@link MessageType.ChannelPinnedMessage}
+ * * {@link MessageType.Reply}
+ * * {@link MessageType.ThreadStarterMessage}
+ * @see {@link https://discord.com/developers/docs/resources/channel#message-types}
+ * @typedef {Object} MessageReference
+ * @property {Snowflake} channelId The channel's id the message was referenced
+ * @property {?Snowflake} guildId The guild's id the message was referenced
+ * @property {?Snowflake} messageId The message's id that was referenced
+ */
+
+ if ('message_reference' in data) {
+ /**
+ * Message reference data
+ * @type {?MessageReference}
+ */
+ this.reference = {
+ channelId: data.message_reference.channel_id,
+ guildId: data.message_reference.guild_id,
+ messageId: data.message_reference.message_id,
+ };
+ } else {
+ this.reference ??= null;
+ }
+
+ if (data.referenced_message) {
+ this.channel?.messages._add({ guild_id: data.message_reference?.guild_id, ...data.referenced_message });
+ }
+
+ /**
+ * Partial data of the interaction that a message is a reply to
+ * @typedef {Object} MessageInteraction
+ * @property {Snowflake} id The interaction's id
+ * @property {InteractionType} type The type of the interaction
+ * @property {string} commandName The name of the interaction's application command,
+ * as well as the subcommand and subcommand group, where applicable
+ * @property {User} user The user that invoked the interaction
+ */
+
+ if (data.interaction) {
+ /**
+ * Partial data of the interaction that this message is a reply to
+ * @type {?MessageInteraction}
+ */
+ this.interaction = {
+ id: data.interaction.id,
+ type: data.interaction.type,
+ commandName: data.interaction.name,
+ user: this.client.users._add(data.interaction.user),
+ };
+ } else {
+ this.interaction ??= null;
+ }
+ }
+
+ /**
+ * The channel that the message was sent in
+ * @type {TextBasedChannels}
+ * @readonly
+ */
+ get channel() {
+ return this.client.channels.resolve(this.channelId);
+ }
+
+ /**
+ * Whether or not this message is a partial
+ * @type {boolean}
+ * @readonly
+ */
+ get partial() {
+ return typeof this.content !== 'string' || !this.author;
+ }
+
+ /**
+ * Represents the author of the message as a guild member.
+ * Only available if the message comes from a guild where the author is still a member
+ * @type {?GuildMember}
+ * @readonly
+ */
+ get member() {
+ return this.guild?.members.resolve(this.author) ?? null;
+ }
+
+ /**
+ * The time the message was sent at
+ * @type {Date}
+ * @readonly
+ */
+ get createdAt() {
+ return new Date(this.createdTimestamp);
+ }
+
+ /**
+ * The time the message was last edited at (if applicable)
+ * @type {?Date}
+ * @readonly
+ */
+ get editedAt() {
+ return this.editedTimestamp && new Date(this.editedTimestamp);
+ }
+
+ /**
+ * The guild the message was sent in (if in a guild channel)
+ * @type {?Guild}
+ * @readonly
+ */
+ get guild() {
+ return this.client.guilds.resolve(this.guildId) ?? this.channel?.guild ?? null;
+ }
+
+ /**
+ * Whether this message has a thread associated with it
+ * @type {boolean}
+ * @readonly
+ */
+ get hasThread() {
+ return this.flags.has(MessageFlags.HasThread);
+ }
+
+ /**
+ * The thread started by this message
+ * <info>This property is not suitable for checking whether a message has a thread,
+ * use {@link Message#hasThread} instead.</info>
+ * @type {?ThreadChannel}
+ * @readonly
+ */
+ get thread() {
+ return this.channel?.threads?.resolve(this.id) ?? null;
+ }
+
+ /**
+ * The URL to jump to this message
+ * @type {string}
+ * @readonly
+ */
+ get url() {
+ return this.inGuild() ? messageLink(this.channelId, this.id, this.guildId) : messageLink(this.channelId, this.id);
+ }
+
+ /**
+ * The message contents with all mentions replaced by the equivalent text.
+ * If mentions cannot be resolved to a name, the relevant mention in the message content will not be converted.
+ * @type {?string}
+ * @readonly
+ */
+ get cleanContent() {
+ // eslint-disable-next-line eqeqeq
+ return this.content != null ? cleanContent(this.content, this.channel) : null;
+ }
+
+ /**
+ * Creates a reaction collector.
+ * @param {ReactionCollectorOptions} [options={}] Options to send to the collector
+ * @returns {ReactionCollector}
+ * @example
+ * // Create a reaction collector
+ * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId';
+ * const collector = message.createReactionCollector({ filter, time: 15_000 });
+ * collector.on('collect', r => console.log(`Collected ${r.emoji.name}`));
+ * collector.on('end', collected => console.log(`Collected ${collected.size} items`));
+ */
+ createReactionCollector(options = {}) {
+ return new ReactionCollector(this, options);
+ }
+
+ /**
+ * An object containing the same properties as CollectorOptions, but a few more:
+ * @typedef {ReactionCollectorOptions} AwaitReactionsOptions
+ * @property {string[]} [errors] Stop/end reasons that cause the promise to reject
+ */
+
+ /**
+ * Similar to createReactionCollector but in promise form.
+ * Resolves with a collection of reactions that pass the specified filter.
+ * @param {AwaitReactionsOptions} [options={}] Optional options to pass to the internal collector
+ * @returns {Promise<Collection<string | Snowflake, MessageReaction>>}
+ * @example
+ * // Create a reaction collector
+ * const filter = (reaction, user) => reaction.emoji.name === '👌' && user.id === 'someId'
+ * message.awaitReactions({ filter, time: 15_000 })
+ * .then(collected => console.log(`Collected ${collected.size} reactions`))
+ * .catch(console.error);
+ */
+ awaitReactions(options = {}) {
+ return new Promise((resolve, reject) => {
+ const collector = this.createReactionCollector(options);
+ collector.once('end', (reactions, reason) => {
+ if (options.errors?.includes(reason)) reject(reactions);
+ else resolve(reactions);
+ });
+ });
+ }
+
+ /**
+ * @typedef {CollectorOptions} MessageComponentCollectorOptions
+ * @property {ComponentType} [componentType] The type of component to listen for
+ * @property {number} [max] The maximum total amount of interactions to collect
+ * @property {number} [maxComponents] The maximum number of components to collect
+ * @property {number} [maxUsers] The maximum number of users to interact
+ */
+
+ /**
+ * Creates a message component interaction collector.
+ * @param {MessageComponentCollectorOptions} [options={}] Options to send to the collector
+ * @returns {InteractionCollector}
+ * @example
+ * // Create a message component interaction collector
+ * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId';
+ * const collector = message.createMessageComponentCollector({ filter, time: 15_000 });
+ * collector.on('collect', i => console.log(`Collected ${i.customId}`));
+ * collector.on('end', collected => console.log(`Collected ${collected.size} items`));
+ */
+ createMessageComponentCollector(options = {}) {
+ return new InteractionCollector(this.client, {
+ ...options,
+ interactionType: InteractionType.MessageComponent,
+ message: this,
+ });
+ }
+
+ /**
+ * An object containing the same properties as CollectorOptions, but a few more:
+ * @typedef {Object} AwaitMessageComponentOptions
+ * @property {CollectorFilter} [filter] The filter applied to this collector
+ * @property {number} [time] Time to wait for an interaction before rejecting
+ * @property {ComponentType} [componentType] The type of component interaction to collect
+ * @property {number} [idle] Time to wait without another message component interaction before ending the collector
+ * @property {boolean} [dispose] Whether to remove the message component interaction after collecting
+ * @property {InteractionResponse} [interactionResponse] The interaction response to collect interactions from
+ */
+
+ /**
+ * Collects a single component interaction that passes the filter.
+ * The Promise will reject if the time expires.
+ * @param {AwaitMessageComponentOptions} [options={}] Options to pass to the internal collector
+ * @returns {Promise<MessageComponentInteraction>}
+ * @example
+ * // Collect a message component interaction
+ * const filter = (interaction) => interaction.customId === 'button' && interaction.user.id === 'someId';
+ * message.awaitMessageComponent({ filter, time: 15_000 })
+ * .then(interaction => console.log(`${interaction.customId} was clicked!`))
+ * .catch(console.error);
+ */
+ awaitMessageComponent(options = {}) {
+ const _options = { ...options, max: 1 };
+ return new Promise((resolve, reject) => {
+ const collector = this.createMessageComponentCollector(_options);
+ collector.once('end', (interactions, reason) => {
+ const interaction = interactions.first();
+ if (interaction) resolve(interaction);
+ else reject(new DiscordjsError(ErrorCodes.InteractionCollectorError, reason));
+ });
+ });
+ }
+
+ /**
+ * Whether the message is editable by the client user
+ * @type {boolean}
+ * @readonly
+ */
+ get editable() {
+ const precheck = Boolean(this.author.id === this.client.user.id && (!this.guild || this.channel?.viewable));
+
+ // Regardless of permissions thread messages cannot be edited if
+ // the thread is archived or the thread is locked and the bot does not have permission to manage threads.
+ if (this.channel?.isThread()) {
+ if (this.channel.archived) return false;
+ if (this.channel.locked) {
+ const permissions = this.channel.permissionsFor(this.client.user);
+ if (!permissions?.has(PermissionFlagsBits.ManageThreads, true)) return false;
+ }
+ }
+
+ return precheck;
+ }
+
+ /**
+ * Whether the message is deletable by the client user
+ * @type {boolean}
+ * @readonly
+ */
+ get deletable() {
+ if (!DeletableMessageTypes.includes(this.type)) return false;
+
+ if (!this.guild) {
+ return this.author.id === this.client.user.id;
+ }
+ // DMChannel does not have viewable property, so check viewable after proved that message is on a guild.
+ if (!this.channel?.viewable) {
+ return false;
+ }
+
+ const permissions = this.channel?.permissionsFor(this.client.user);
+ if (!permissions) return false;
+ // This flag allows deleting even if timed out
+ if (permissions.has(PermissionFlagsBits.Administrator, false)) return true;
+
+ // The auto moderation action message author is the reference message author
+ return (
+ (this.type !== MessageType.AutoModerationAction && this.author.id === this.client.user.id) ||
+ (permissions.has(PermissionFlagsBits.ManageMessages, false) && !this.guild.members.me.isCommunicationDisabled())
+ );
+ }
+
+ /**
+ * Whether the message is bulk deletable by the client user
+ * @type {boolean}
+ * @readonly
+ * @example
+ * // Filter for bulk deletable messages
+ * channel.bulkDelete(messages.filter(message => message.bulkDeletable));
+ */
+ get bulkDeletable() {
+ return (
+ (this.inGuild() &&
+ Date.now() - this.createdTimestamp < MaxBulkDeletableMessageAge &&
+ this.deletable &&
+ this.channel?.permissionsFor(this.client.user).has(PermissionFlagsBits.ManageMessages, false)) ??
+ false
+ );
+ }
+
+ /**
+ * Whether the message is pinnable by the client user
+ * @type {boolean}
+ * @readonly
+ */
+ get pinnable() {
+ const { channel } = this;
+ return Boolean(
+ !this.system &&
+ (!this.guild ||
+ (channel?.viewable &&
+ channel?.permissionsFor(this.client.user)?.has(PermissionFlagsBits.ManageMessages, false))),
+ );
+ }
+
+ /**
+ * Fetches the Message this crosspost/reply/pin-add references, if available to the client
+ * @returns {Promise<Message>}
+ */
+ async fetchReference() {
+ if (!this.reference) throw new DiscordjsError(ErrorCodes.MessageReferenceMissing);
+ const { channelId, messageId } = this.reference;
+ const channel = this.client.channels.resolve(channelId);
+ if (!channel) throw new DiscordjsError(ErrorCodes.GuildChannelResolve);
+ const message = await channel.messages.fetch(messageId);
+ return message;
+ }
+
+ /**
+ * Whether the message is crosspostable by the client user
+ * @type {boolean}
+ * @readonly
+ */
+ get crosspostable() {
+ const bitfield =
+ PermissionFlagsBits.SendMessages |
+ (this.author.id === this.client.user.id ? PermissionsBitField.DefaultBit : PermissionFlagsBits.ManageMessages);
+ const { channel } = this;
+ return Boolean(
+ channel?.type === ChannelType.GuildAnnouncement &&
+ !this.flags.has(MessageFlags.Crossposted) &&
+ this.type === MessageType.Default &&
+ channel.viewable &&
+ channel.permissionsFor(this.client.user)?.has(bitfield, false),
+ );
+ }
+
+ /**
+ * Edits the content of the message.
+ * @param {string|MessagePayload|MessageEditOptions} options The options to provide
+ * @returns {Promise<Message>}
+ * @example
+ * // Update the content of a message
+ * message.edit('This is my new content!')
+ * .then(msg => console.log(`Updated the content of a message to ${msg.content}`))
+ * .catch(console.error);
+ */
+ edit(options) {
+ if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached));
+ return this.channel.messages.edit(this, options);
+ }
+
+ /**
+ * Publishes a message in an announcement channel to all channels following it.
+ * @returns {Promise<Message>}
+ * @example
+ * // Crosspost a message
+ * if (message.channel.type === ChannelType.GuildAnnouncement) {
+ * message.crosspost()
+ * .then(() => console.log('Crossposted message'))
+ * .catch(console.error);
+ * }
+ */
+ crosspost() {
+ if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached));
+ return this.channel.messages.crosspost(this.id);
+ }
+
+ /**
+ * Pins this message to the channel's pinned messages.
+ * @param {string} [reason] Reason for pinning
+ * @returns {Promise<Message>}
+ * @example
+ * // Pin a message
+ * message.pin()
+ * .then(console.log)
+ * .catch(console.error)
+ */
+ async pin(reason) {
+ if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached);
+ await this.channel.messages.pin(this.id, reason);
+ return this;
+ }
+
+ /**
+ * Unpins this message from the channel's pinned messages.
+ * @param {string} [reason] Reason for unpinning
+ * @returns {Promise<Message>}
+ * @example
+ * // Unpin a message
+ * message.unpin()
+ * .then(console.log)
+ * .catch(console.error)
+ */
+ async unpin(reason) {
+ if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached);
+ await this.channel.messages.unpin(this.id, reason);
+ return this;
+ }
+
+ /**
+ * Adds a reaction to the message.
+ * @param {EmojiIdentifierResolvable} emoji The emoji to react with
+ * @returns {Promise<MessageReaction>}
+ * @example
+ * // React to a message with a unicode emoji
+ * message.react('🤔')
+ * .then(console.log)
+ * .catch(console.error);
+ * @example
+ * // React to a message with a custom emoji
+ * message.react(message.guild.emojis.cache.get('123456789012345678'))
+ * .then(console.log)
+ * .catch(console.error);
+ */
+ async react(emoji) {
+ if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached);
+ await this.channel.messages.react(this.id, emoji);
+
+ return this.client.actions.MessageReactionAdd.handle(
+ {
+ [this.client.actions.injectedUser]: this.client.user,
+ [this.client.actions.injectedChannel]: this.channel,
+ [this.client.actions.injectedMessage]: this,
+ emoji: resolvePartialEmoji(emoji),
+ },
+ true,
+ ).reaction;
+ }
+
+ /**
+ * Deletes the message.
+ * @returns {Promise<Message>}
+ * @example
+ * // Delete a message
+ * message.delete()
+ * .then(msg => console.log(`Deleted message from ${msg.author.username}`))
+ * .catch(console.error);
+ */
+ async delete() {
+ if (!this.channel) throw new DiscordjsError(ErrorCodes.ChannelNotCached);
+ await this.channel.messages.delete(this.id);
+ return this;
+ }
+
+ /**
+ * Options provided when sending a message as an inline reply.
+ * @typedef {BaseMessageCreateOptions} MessageReplyOptions
+ * @property {boolean} [failIfNotExists=this.client.options.failIfNotExists] Whether to error if the referenced
+ * message does not exist (creates a standard message in this case when false)
+ */
+
+ /**
+ * Send an inline reply to this message.
+ * @param {string|MessagePayload|MessageReplyOptions} options The options to provide
+ * @returns {Promise<Message>}
+ * @example
+ * // Reply to a message
+ * message.reply('This is a reply!')
+ * .then(() => console.log(`Replied to message "${message.content}"`))
+ * .catch(console.error);
+ */
+ reply(options) {
+ if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached));
+ let data;
+
+ if (options instanceof MessagePayload) {
+ data = options;
+ } else {
+ data = MessagePayload.create(this, options, {
+ reply: {
+ messageReference: this,
+ failIfNotExists: options?.failIfNotExists ?? this.client.options.failIfNotExists,
+ },
+ });
+ }
+ return this.channel.send(data);
+ }
+
+ /**
+ * Options for starting a thread on a message.
+ * @typedef {Object} StartThreadOptions
+ * @property {string} name The name of the new thread
+ * @property {ThreadAutoArchiveDuration} [autoArchiveDuration=this.channel.defaultAutoArchiveDuration] The amount of
+ * time after which the thread should automatically archive in case of no recent activity
+ * @property {string} [reason] Reason for creating the thread
+ * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the thread in seconds
+ */
+
+ /**
+ * Create a new public thread from this message
+ * @see GuildTextThreadManager#create
+ * @param {StartThreadOptions} [options] Options for starting a thread on this message
+ * @returns {Promise<ThreadChannel>}
+ */
+ startThread(options = {}) {
+ if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached));
+ if (![ChannelType.GuildText, ChannelType.GuildAnnouncement].includes(this.channel.type)) {
+ return Promise.reject(new DiscordjsError(ErrorCodes.MessageThreadParent));
+ }
+ if (this.hasThread) return Promise.reject(new DiscordjsError(ErrorCodes.MessageExistingThread));
+ return this.channel.threads.create({ ...options, startMessage: this });
+ }
+
+ /**
+ * Fetch this message.
+ * @param {boolean} [force=true] Whether to skip the cache check and request the API
+ * @returns {Promise<Message>}
+ */
+ fetch(force = true) {
+ if (!this.channel) return Promise.reject(new DiscordjsError(ErrorCodes.ChannelNotCached));
+ return this.channel.messages.fetch({ message: this.id, force });
+ }
+
+ /**
+ * Fetches the webhook used to create this message.
+ * @returns {Promise<?Webhook>}
+ */
+ fetchWebhook() {
+ if (!this.webhookId) return Promise.reject(new DiscordjsError(ErrorCodes.WebhookMessage));
+ if (this.webhookId === this.applicationId) return Promise.reject(new DiscordjsError(ErrorCodes.WebhookApplication));
+ return this.client.fetchWebhook(this.webhookId);
+ }
+
+ /**
+ * Suppresses or unsuppresses embeds on a message.
+ * @param {boolean} [suppress=true] If the embeds should be suppressed or not
+ * @returns {Promise<Message>}
+ */
+ suppressEmbeds(suppress = true) {
+ const flags = new MessageFlagsBitField(this.flags.bitfield);
+
+ if (suppress) {
+ flags.add(MessageFlags.SuppressEmbeds);
+ } else {
+ flags.remove(MessageFlags.SuppressEmbeds);
+ }
+
+ return this.edit({ flags });
+ }
+
+ /**
+ * Removes the attachments from this message.
+ * @returns {Promise<Message>}
+ */
+ removeAttachments() {
+ return this.edit({ attachments: [] });
+ }
+
+ /**
+ * Resolves a component by a custom id.
+ * @param {string} customId The custom id to resolve against
+ * @returns {?MessageActionRowComponent}
+ */
+ resolveComponent(customId) {
+ return this.components.flatMap(row => row.components).find(component => component.customId === customId) ?? null;
+ }
+
+ /**
+ * Used mainly internally. Whether two messages are identical in properties. If you want to compare messages
+ * without checking all the properties, use `message.id === message2.id`, which is much more efficient. This
+ * method allows you to see if there are differences in content, embeds, attachments, nonce and tts properties.
+ * @param {Message} message The message to compare it to
+ * @param {APIMessage} rawData Raw data passed through the WebSocket about this message
+ * @returns {boolean}
+ */
+ equals(message, rawData) {
+ if (!message) return false;
+ const embedUpdate = !message.author && !message.attachments;
+ if (embedUpdate) return this.id === message.id && this.embeds.length === message.embeds.length;
+
+ let equal =
+ this.id === message.id &&
+ this.author.id === message.author.id &&
+ this.content === message.content &&
+ this.tts === message.tts &&
+ this.nonce === message.nonce &&
+ this.embeds.length === message.embeds.length &&
+ this.attachments.length === message.attachments.length;
+
+ if (equal && rawData) {
+ equal =
+ this.mentions.everyone === message.mentions.everyone &&
+ this.createdTimestamp === Date.parse(rawData.timestamp) &&
+ this.editedTimestamp === Date.parse(rawData.edited_timestamp);
+ }
+
+ return equal;
+ }
+
+ /**
+ * Whether this message is from a guild.
+ * @returns {boolean}
+ */
+ inGuild() {
+ return Boolean(this.guildId);
+ }
+
+ /**
+ * When concatenated with a string, this automatically concatenates the message's content instead of the object.
+ * @returns {string}
+ * @example
+ * // Logs: Message: This is a message!
+ * console.log(`Message: ${message}`);
+ */
+ toString() {
+ return this.content;
+ }
+
+ toJSON() {
+ return super.toJSON({
+ channel: 'channelId',
+ author: 'authorId',
+ groupActivityApplication: 'groupActivityApplicationId',
+ guild: 'guildId',
+ cleanContent: true,
+ member: false,
+ reactions: false,
+ });
+ }
+}
+
+exports.Message = Message;