"use strict";
const Base = require("./Base");
const Endpoints = require("../rest/Endpoints");
const { MessageFlags } = require("../Constants");
const User = require("./User");
/**
* Represents a message
* @prop {Object?} activity The activity specified in the message
* @prop {Object?} application The application of the activity in the message
* @prop {String?} applicationID The ID of the interaction's application
* @prop {Array<Object>} attachments Array of attachments
* @prop {User} author The message author
* @prop {Object} call The call associated with the message
* @prop {Number?} call.endedTimestamp The time when the call ended
* @prop {Array<String>} call.participants An array of user IDs that participated in the call
* @prop {DMChannel | TextChannel | NewsChannel} channel The channel the message is in. Can be partial with only the id if the channel is not cached.
* @prop {Array<String>} channelMentions Array of mentions channels' ids
* @prop {String?} cleanContent Message content with mentions replaced by names. Mentions are currently escaped, but this behavior is [DEPRECATED] and will be removed soon. Use allowed mentions, the official way of avoiding unintended mentions, when creating messages.
* @prop {Command?} command The Command used in the Message, if any (CommandClient only)
* @prop {Array<Object>} components An array of component objects
* @prop {String} content Message content
* @prop {Number} createdAt Timestamp of message creation
* @prop {Number?} editedTimestamp Timestamp of latest message edit
* @prop {Array<Object>} embeds Array of embeds
* @prop {Number} flags Message flags (see constants)
* @prop {String?} guildID The ID of the guild this message is in (undefined if in DMs)
* @prop {String} id The ID of the message
* @prop {Object?} interaction An object containing info about the interaction the message is responding to, if applicable
* @prop {String} interaction.id The ID of the interaction
* @prop {Member?} interaction.member The member who invoked the interaction
* @prop {String} interaction.name The name of the command
* @prop {Number} interaction.type The type of interaction
* @prop {User} interaction.user The user who invoked the interaction
* @prop {String} jumpLink The url used by Discord clients to jump to this message
* @prop {Member?} member The message author with server-specific data
* @prop {Boolean} mentionEveryone Whether the message mentions everyone/here or not
* @prop {Array<User>} mentions Array of mentioned users
* @prop {Object?} messageReference An object containing the reference to the original message if it is a crossposted message, reply or forwarded message
* @prop {String} messageReference.channelID The ID of the channel this message was crossposted from
* @prop {String?} messageReference.guildID The ID of the guild this message was crossposted from
* @prop {String?} messageReference.messageID The ID of the original message this message was crossposted from
* @prop {Number} messageReference.type The type of reference. Either `0` (REPLY) or `1` (FORWARDED)
* @prop {Array<Object>?} messageSnapshots The message associated with the messageReference
* @prop {String?} messageSnapshots.guildID The ID of the guild this message originated from
* @prop {Message} messageSnapshots.message Subset of message fields. The list of message fields subset consists of: `attachments`, `content`, `edited_timestamp`, `embeds`, `flags`, `id`, `mentions`, `roleMentions`, `timestamp` and `type`
* @prop {Boolean} pinned Whether the message is pinned or not
* @prop {Object?} poll A poll object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/poll#poll-object) for object structure. Note: `poll.expiry` is an int, not ISO8601 as mentioned in Discord docs
* @prop {String?} prefix The prefix used in the Message, if any (CommandClient only)
* @prop {Object} reactions An object containing the reactions on the message. Each key is a reaction emoji and each value is an object with properties `burst_colors` (Array<String>), `count` (Number), `count_details` (an object with `burst` and `normal` keys corresponding to the amount of reactions), `me` (Boolean) and `me_burst` for that specific reaction emoji.
* @prop {Message?} referencedMessage The message that was replied to. If undefined, message data was not received. If null, the message was deleted.
* @prop {Array<String>} roleMentions Array of mentioned roles' ids
* @prop {Object?} roleSubscriptionData An object containing the data of the role subscription purchase or renewal that prompted this `ROLE_SUBSCRIPTION_PURCHASE` message. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#role-subscription-data-object) for object structure
* @prop {Array<Object>?} stickers [DEPRECATED] The stickers sent with the message
* @prop {Array<Object>?} stickerItems The stickers sent with the message
* @prop {Number} timestamp Timestamp of message creation
* @prop {Boolean} tts Whether to play the message using TTS or not
* @prop {Number} type The type of the message
* @prop {String?} webhookID ID of the webhook that sent the message
*/
class Message extends Base {
constructor(data, client) {
super(data.id);
this._client = client;
this.type = data.type || 0;
this.timestamp = Date.parse(data.timestamp);
this.channel = this._client.getChannel(data.channel_id) || {
id: data.channel_id,
};
this.content = "";
this.reactions = {};
this.guildID = data.guild_id;
this.webhookID = data.webhook_id;
if (data.message_reference) {
this.messageReference = {
type: data.message_reference.type,
messageID: data.message_reference.message_id,
channelID: data.message_reference.channel_id,
guildID: data.message_reference.guild_id,
};
} else {
this.messageReference = null;
}
if (data.message_snapshots && this.messageReference) {
this.messageSnapshots = data.message_snapshots.map((snapshot) => {
const channel = this._client.getChannel(this.messageReference.channelID);
let message;
snapshot.message.id = this.messageReference.messageID;
if (channel) {
message = channel.messages.update(snapshot.message, this._client);
} else {
message = new Message(snapshot.message, this._client);
}
return {
guildID: this.messageReference.guildID,
message: message,
};
});
}
this.flags = data.flags || 0;
if (data.author) {
if (data.author.discriminator !== "0000") {
this.author = this._client.users.update(data.author, client);
} else {
this.author = new User(data.author, client);
}
} else {
this._client.emit("error", new Error("MESSAGE_CREATE but no message author:\n" + JSON.stringify(data, null, 2)));
}
if (data.referenced_message) {
const channel = this._client.getChannel(data.referenced_message.channel_id);
if (channel) {
this.referencedMessage = channel.messages.update(data.referenced_message, this._client);
} else {
this.referencedMessage = new Message(data.referenced_message, this._client);
}
} else {
this.referencedMessage = data.referenced_message;
}
if (data.interaction) {
this.interaction = data.interaction;
let interactionMember;
const interactionUser = this._client.users.update(data.interaction.user, client);
if (data.interaction.member) {
data.interaction.member.id = data.interaction.user.id;
if (this.channel.guild) {
interactionMember = this.channel.guild.members.update(data.interaction.member, this.channel.guild);
} else {
interactionMember = data.interaction.member;
}
} else if (this.channel.guild && this.channel.guild.members.has(data.interaction.user.id)) {
interactionMember = this.channel.guild.members.get(data.interaction.user.id);
} else {
interactionMember = null;
}
this.interaction.user = interactionUser;
this.interaction.member = interactionMember;
} else {
this.interaction = null;
}
if (this.channel.guild) {
if (data.member) {
data.member.id = this.author.id;
if (data.author) {
data.member.user = data.author;
}
this.member = this.channel.guild.members.update(data.member, this.channel.guild);
} else if (this.channel.guild.members.has(this.author.id)) {
this.member = this.channel.guild.members.get(this.author.id);
} else {
this.member = null;
}
if (!this.guildID) {
this.guildID = this.channel.guild.id;
}
} else {
this.member = null;
}
this.update(data, client);
}
update(data, client) {
if (data.content !== undefined) {
this.content = data.content || "";
this.mentionEveryone = !!data.mention_everyone;
this.mentions = data.mentions.map((mention) => {
const user = this._client.users.add(mention, client);
if (mention.member && this.channel.guild) {
mention.member.id = mention.id;
this.channel.guild.members.update(mention.member, this.channel.guild);
}
return user;
});
this.roleMentions = data.mention_roles;
}
if (data.poll !== undefined) {
this.poll = data.poll;
if (data.poll.expiry !== null) {
this.poll.expiry = Date.parse(data.poll.expiry);
}
}
if (data.pinned !== undefined) {
this.pinned = !!data.pinned;
}
if (data.edited_timestamp != undefined) {
this.editedTimestamp = Date.parse(data.edited_timestamp);
}
if (data.tts !== undefined) {
this.tts = data.tts;
}
if (data.attachments !== undefined) {
this.attachments = data.attachments;
}
if (data.embeds !== undefined) {
this.embeds = data.embeds;
}
if (data.flags !== undefined) {
this.flags = data.flags;
}
if (data.activity !== undefined) {
this.activity = data.activity;
}
if (data.application !== undefined) {
this.application = data.application;
}
if (data.application_id !== undefined) {
this.applicationID = data.application_id;
}
if (data.reactions) {
data.reactions.forEach((reaction) => {
this.reactions[reaction.emoji.id ? `${reaction.emoji.name}:${reaction.emoji.id}` : reaction.emoji.name] = {
burst_colors: reaction.burst_colors,
count: reaction.count,
count_details: reaction.count_details,
me: reaction.me,
me_burst: reaction.me_burst,
};
});
}
if (data.stickers !== undefined) {
this.stickers = data.stickers;
}
if (data.sticker_items !== undefined) {
this.stickerItems = data.sticker_items.map((sticker) => {
if (sticker.user) {
sticker.user = this._client.users.update(sticker.user, client);
}
return sticker;
});
}
if (data.components !== undefined) {
this.components = data.components;
}
if (data.call !== undefined) {
this.call = {
endedTimestamp: Date.parse(data.call.ended_timestamp) || null,
participants: data.call.participants,
};
}
}
get channelMentions() {
if (this._channelMentions) {
return this._channelMentions;
}
return (this._channelMentions = ((this.content && this.content.match(/<#[0-9]+>/g)) || []).map((mention) => mention.substring(2, mention.length - 1)));
}
get cleanContent() {
let cleanContent = (this.content && this.content.replace(/<a?(:\w+:)[0-9]+>/g, "$1")) || "";
let authorName = this.author.username;
if (this.channel.guild) {
const member = this.channel.guild.members.get(this.author.id);
if (member && member.nick) {
authorName = member.nick;
}
}
cleanContent = cleanContent.replace(new RegExp(`<@!?${this.author.id}>`, "g"), "@\u200b" + authorName);
if (this.mentions) {
this.mentions.forEach((mention) => {
if (this.channel.guild) {
const member = this.channel.guild.members.get(mention.id);
if (member && member.nick) {
cleanContent = cleanContent.replace(new RegExp(`<@!?${mention.id}>`, "g"), "@\u200b" + member.nick);
}
}
cleanContent = cleanContent.replace(new RegExp(`<@!?${mention.id}>`, "g"), "@\u200b" + mention.username);
});
}
if (this.channel.guild && this.roleMentions) {
for (const roleID of this.roleMentions) {
const role = this.channel.guild.roles.get(roleID);
const roleName = role ? role.name : "deleted-role";
cleanContent = cleanContent.replace(new RegExp(`<@&${roleID}>`, "g"), "@\u200b" + roleName);
}
}
this.channelMentions.forEach((id) => {
const channel = this._client.getChannel(id);
if (channel && channel.name && channel.mention) {
cleanContent = cleanContent.replace(channel.mention, "#" + channel.name);
}
});
return cleanContent.replace(/@everyone/g, "@\u200beveryone").replace(/@here/g, "@\u200bhere");
}
get jumpLink() {
return `${Endpoints.CLIENT_URL}${Endpoints.MESSAGE_LINK(this.guildID || "@me", this.channel.id, this.id)}`; // Messages outside of guilds (DMs) will never have a guildID property assigned
}
/**
* Add a reaction to a message
* @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji)
* @returns {Promise}
*/
addReaction(reaction) {
if (this.flags & MessageFlags.EPHEMERAL) {
throw new Error("Ephemeral messages cannot have reactions");
}
return this._client.addMessageReaction.call(this._client, this.channel.id, this.id, reaction);
}
/**
* Create a thread with this message
* @arg {Object} options The thread options
* @arg {Number} [options.autoArchiveDuration] Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080
* @arg {String} options.name The thread channel name
* @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions)
* @arg {String} [options.reason] The reason to be displayed in audit logs
* @returns {Promise<NewsThreadChannel | PublicThreadChannel>}
*/
createThreadWithMessage(options) {
return this._client.createThreadWithMessage.call(this._client, this.channel.id, this.id, options);
}
/**
* Crosspost (publish) a message to subscribed channels (NewsChannel only)
* @returns {Promise<Message>}
*/
crosspost() {
if (this.flags & MessageFlags.EPHEMERAL) {
throw new Error("Ephemeral messages cannot be crossposted");
}
return this._client.crosspostMessage.call(this._client, this.channel.id, this.id);
}
/**
* Delete the message
* @arg {String} [reason] The reason to be displayed in audit logs
* @returns {Promise}
*/
delete(reason) {
if (this.flags & MessageFlags.EPHEMERAL) {
throw new Error("Ephemeral messages cannot be deleted");
}
return this._client.deleteMessage.call(this._client, this.channel.id, this.id, reason);
}
/**
* Delete the message as a webhook
* @arg {String} token The token of the webhook
* @returns {Promise}
*/
deleteWebhook(token) {
if (!this.webhookID) {
throw new Error("Message is not a webhook");
}
if (this.flags & MessageFlags.EPHEMERAL) {
throw new Error("Ephemeral messages cannot be deleted");
}
return this._client.deleteWebhookMessage.call(this._client, this.webhookID, token, this.id);
}
/**
* Edit the message
* @arg {String | Array | Object} content A string, array of strings, or object. If an object is passed:
* @arg {Object} [content.allowedMentions] A list of mentions to allow (overrides default)
* @arg {Boolean} [content.allowedMentions.everyone] Whether or not to allow @everyone/@here
* @arg {Boolean | Array<String>} [content.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow
* @arg {Boolean | Array<String>} [content.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow
* @arg {Array<Object>} [content.attachments] An array of attachment objects that will be appended to the message, including new files. Only the provided files will be appended
* @arg {String} [content.attachments[].description] The description of the file
* @arg {String} [content.attachments[].filename] The name of the file. This is not required if you are attaching a new file
* @arg {Number | String} content.attachments[].id The ID of the file. If you are attaching a new file, this would be the index of the file
* @arg {Array<Object>} [content.components] An array of component objects
* @arg {String} [content.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3,5,6,7,8 only)
* @arg {Boolean} [content.components[].disabled] Whether the component is disabled (type 2 and 3,5,6,7,8 only)
* @arg {Object} [content.components[].emoji] The emoji to be displayed in the component (type 2)
* @arg {String} [content.components[].label] The label to be displayed in the component (type 2)
* @arg {Array<Object>} [content.components[].default_values] default values for the component (type 5,6,7,8 only)
* @arg {String} [content.components[].default_values[].id] id of a user, role, or channel
* @arg {String} [content.components[].default_values[].type] type of value that id represents (user, role, or channel)
* @arg {Number} [content.components[].max_values] The maximum number of items that can be chosen (1-25, default 1)
* @arg {Number} [content.components[].min_values] The minimum number of items that must be chosen (0-25, default 1)
* @arg {Array<Object>} [content.components[].options] The options for this component (type 3 only)
* @arg {Boolean} [content.components[].options[].default] Whether this option should be the default value selected
* @arg {String} [content.components[].options[].description] The description for this option
* @arg {Object} [content.components[].options[].emoji] The emoji to be displayed in this option
* @arg {String} content.components[].options[].label The label for this option
* @arg {Number | String} content.components[].options[].value The value for this option
* @arg {String} [content.components[].placeholder] The placeholder text for the component when no option is selected (type 3,5,6,7,8 only)
* @arg {Number} [content.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required
* @arg {Number} content.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a string select; if 5, it is a user select; if 6, it is a role select; if 7, it is a mentionable select; if 8, it is a channel select
* @arg {String} [content.components[].url] The URL that the component should open for users (type 2 style 5 only)
* @arg {String} [content.content] A content string
* @arg {Object} [content.embed] [DEPRECATED] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure
* @arg {Array<Object>} [content.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure
* @arg {Object | Array<Object>} [content.file] A file object (or an Array of them)
* @arg {Buffer} content.file[].file A buffer containing file data
* @arg {String} content.file[].name What to name the file
* @arg {Number} [content.flags] A number representing the flags to apply to the message. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#message-object-message-flags) for flags reference
* @returns {Promise<Message>}
*/
edit(content) {
if (this.flags & MessageFlags.EPHEMERAL) {
throw new Error("Ephemeral messages cannot be edited via this method");
}
return this._client.editMessage.call(this._client, this.channel.id, this.id, content);
}
/**
* Edit the message as a webhook
* @arg {String} token The token of the webhook
* @arg {Object} options Webhook message edit options
* @arg {Object} [options.allowedMentions] A list of mentions to allow (overrides default)
* @arg {Boolean} [options.allowedMentions.everyone] Whether or not to allow @everyone/@here
* @arg {Boolean} [options.allowedMentions.repliedUser] Whether or not to mention the author of the message being replied to
* @arg {Boolean | Array<String>} [options.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow
* @arg {Boolean | Array<String>} [options.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow
* @arg {Array<Object>} [options.attachments] An array of attachment objects that will be appended to the message, including new files. Only the provided files will be appended
* @arg {String} [options.attachments[].description] The description of the file
* @arg {String} [options.attachments[].filename] The name of the file. This is not required if you are attaching a new file
* @arg {Number | String} options.attachments[].id The ID of the file. If you are attaching a new file, this would be the index of the file
* @arg {Array<Object>} [options.components] An array of component objects
* @arg {String} [options.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3,5,6,7,8 only)
* @arg {Boolean} [options.components[].disabled] Whether the component is disabled (type 2 and type 3,5,6,7,8 only)
* @arg {Object} [options.components[].emoji] The emoji to be displayed in the component (type 2)
* @arg {String} [options.components[].label] The label to be displayed in the component (type 2)
* @arg {Array<Object>} [options.components[].default_values] default values for the component (type 5,6,7,8 only)
* @arg {String} [options.components[].default_values[].id] id of a user, role, or channel
* @arg {String} [options.components[].default_values[].type] type of value that id represents (user, role, or channel)
* @arg {Number} [options.components[].max_values] The maximum number of items that can be chosen (1-25, default 1)
* @arg {Number} [options.components[].min_values] The minimum number of items that must be chosen (0-25, default 1)
* @arg {Array<Object>} [options.components[].options] The options for this component (type 3 only)
* @arg {Boolean} [options.components[].options[].default] Whether this option should be the default value selected
* @arg {String} [options.components[].options[].description] The description for this option
* @arg {Object} [options.components[].options[].emoji] The emoji to be displayed in this option
* @arg {String} options.components[].options[].label The label for this option
* @arg {Number | String} options.components[].options[].value The value for this option
* @arg {String} [options.components[].placeholder] The placeholder text for the component when no option is selected (type 3,5,6,7,8 only)
* @arg {Number} [options.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required
* @arg {Number} options.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a string select; if 5, it is a user select; if 6, it is a role select; if 7, it is a mentionable select; if 8, it is a channel select
* @arg {String} [options.components[].url] The URL that the component should open for users (type 2 style 5 only)
* @arg {String} [options.content] A content string
* @arg {Object} [options.embed] [DEPRECATED] An embed object. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure
* @arg {Array<Object>} [options.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure
* @arg {Object | Array<Object>} [options.file] A file object (or an Array of them)
* @arg {Buffer} options.file.file A buffer containing file data
* @arg {String} options.file.name What to name the file
* @returns {Promise<Message>}
*/
editWebhook(token, options) {
if (!this.webhookID) {
throw new Error("Message is not a webhook");
}
return this._client.editWebhookMessage.call(this._client, this.webhookID, token, this.id, options);
}
/**
* Get a list of users who reacted with a specific reaction
* @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji)
* @arg {Object} [options] Options for the request. If this is a number, it is treated as `options.limit` ([DEPRECATED] behavior)
* @arg {String} [options.after] Get users after this user ID
* @arg {Number} [options.limit=100] The maximum number of users to get
* @arg {Number} [options.type=0] The type of reaction (`0` for normal, `1` for burst)
* @arg {String} [before] [DEPRECATED] Get users before this user ID. Discord no longer supports this parameter
* @arg {String} [after] [DEPRECATED] Get users after this user ID
* @returns {Promise<Array<User>>}
*/
getReaction(reaction, options, before, after) {
if (this.flags & MessageFlags.EPHEMERAL) {
throw new Error("Ephemeral messages cannot have reactions");
}
return this._client.getMessageReaction.call(this._client, this.channel.id, this.id, reaction, options, before, after);
}
/**
* Pin the message
* @returns {Promise}
*/
pin() {
if (this.flags & MessageFlags.EPHEMERAL) {
throw new Error("Ephemeral messages cannot be pinned");
}
return this._client.pinMessage.call(this._client, this.channel.id, this.id);
}
/**
* Remove a reaction from a message
* @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji)
* @arg {String} [userID="@me"] The ID of the user to remove the reaction for
* @returns {Promise}
*/
removeReaction(reaction, userID) {
if (this.flags & MessageFlags.EPHEMERAL) {
throw new Error("Ephemeral messages cannot have reactions");
}
return this._client.removeMessageReaction.call(this._client, this.channel.id, this.id, reaction, userID);
}
/**
* Remove all reactions from a message for a single emoji
* @arg {String} reaction The reaction (Unicode string if Unicode emoji, `emojiName:emojiID` if custom emoji)
* @returns {Promise}
*/
removeReactionEmoji(reaction) {
if (this.flags & MessageFlags.EPHEMERAL) {
throw new Error("Ephemeral messages cannot have reactions");
}
return this._client.removeMessageReactionEmoji.call(this._client, this.channel.id, this.id, reaction);
}
/**
* Remove all reactions from a message
* @returns {Promise}
*/
removeReactions() {
if (this.flags & MessageFlags.EPHEMERAL) {
throw new Error("Ephemeral messages cannot have reactions");
}
return this._client.removeMessageReactions.call(this._client, this.channel.id, this.id);
}
/**
* Unpin the message
* @returns {Promise}
*/
unpin() {
if (this.flags & MessageFlags.EPHEMERAL) {
throw new Error("Ephemeral messages cannot be pinned");
}
return this._client.unpinMessage.call(this._client, this.channel.id, this.id);
}
toJSON(props = []) {
return super.toJSON([
"activity",
"application",
"attachments",
"author",
"call",
"content",
"editedTimestamp",
"embeds",
"flags",
"guildID",
"member",
"mentionEveryone",
"mentions",
"messageReference",
"pinned",
"reactions",
"referencedMesssage",
"roleMentions",
"stickers",
"stickerItems",
"timestamp",
"tts",
"type",
"webhookID",
...props,
]);
}
}
module.exports = Message;