'use strict'; const { Buffer } = require('node:buffer'); const BaseMessageComponent = require('./BaseMessageComponent'); const MessageEmbed = require('./MessageEmbed'); const WebEmbed = require('./WebEmbed'); const { RangeError } = require('../errors'); const DataResolver = require('../util/DataResolver'); const MessageFlags = require('../util/MessageFlags'); const Util = require('../util/Util'); /** * Represents a message to be sent to the API. */ class MessagePayload { /** * @param {MessageTarget} target The target for this message to be sent to * @param {MessageOptions|WebhookMessageOptions} options Options passed in from send */ constructor(target, options) { /** * The target for this message to be sent to * @type {MessageTarget} */ this.target = target; /** * Options passed in from send * @type {MessageOptions|WebhookMessageOptions} */ this.options = options; /** * Data sendable to the API * @type {?APIMessage} */ this.data = null; /** * @typedef {Object} MessageFile * @property {Buffer|string|Stream} attachment The original attachment that generated this file * @property {string} name The name of this file * @property {Buffer|Stream} file The file to be sent to the API */ /** * Files sendable to the API * @type {?MessageFile[]} */ this.files = null; } /** * Whether or not the target is a {@link Webhook} or a {@link WebhookClient} * @type {boolean} * @readonly */ get isWebhook() { const Webhook = require('./Webhook'); const WebhookClient = require('../client/WebhookClient'); return this.target instanceof Webhook || this.target instanceof WebhookClient; } /** * Whether or not the target is a {@link User} * @type {boolean} * @readonly */ get isUser() { const User = require('./User'); const { GuildMember } = require('./GuildMember'); return this.target instanceof User || this.target instanceof GuildMember; } /** * Whether or not the target is a {@link Message} * @type {boolean} * @readonly */ get isMessage() { const { Message } = require('./Message'); return this.target instanceof Message; } /** * Whether or not the target is a {@link MessageManager} * @type {boolean} * @readonly */ get isMessageManager() { const MessageManager = require('../managers/MessageManager'); return this.target instanceof MessageManager; } /** * Whether or not the target is an {@link Interaction} or an {@link InteractionWebhook} * @type {boolean} * @readonly */ get isInteraction() { const Interaction = require('./Interaction'); const InteractionWebhook = require('./InteractionWebhook'); return this.target instanceof Interaction || this.target instanceof InteractionWebhook; } /** * Makes the content of this message. * @returns {?string} */ makeContent() { let content; if (this.options.content === null) { content = ''; } else if (typeof this.options.content !== 'undefined') { content = Util.verifyString(this.options.content, RangeError, 'MESSAGE_CONTENT_TYPE', false); } return content; } /** * Resolves data. * @returns {MessagePayload} */ async resolveData() { if (this.data) return this; const isInteraction = this.isInteraction; const isWebhook = this.isWebhook; let content = this.makeContent(); const tts = Boolean(this.options.tts); let nonce; if (typeof this.options.nonce !== 'undefined') { nonce = this.options.nonce; // eslint-disable-next-line max-len if (typeof nonce === 'number' ? !Number.isInteger(nonce) : typeof nonce !== 'string') { throw new RangeError('MESSAGE_NONCE_TYPE'); } } const components = this.options.components?.map(c => BaseMessageComponent.create(c).toJSON()); let username; let avatarURL; if (isWebhook) { username = this.options.username ?? this.target.name; if (this.options.avatarURL) avatarURL = this.options.avatarURL; } let flags; if (this.isMessage || this.isMessageManager) { // eslint-disable-next-line eqeqeq flags = this.options.flags != null ? new MessageFlags(this.options.flags).bitfield : this.target.flags?.bitfield; } else if (isInteraction && this.options.ephemeral) { flags = MessageFlags.FLAGS.EPHEMERAL; } let allowedMentions = typeof this.options.allowedMentions === 'undefined' ? this.target.client.options.allowedMentions : this.options.allowedMentions; if (allowedMentions) { allowedMentions = Util.cloneObject(allowedMentions); allowedMentions.replied_user = allowedMentions.repliedUser; delete allowedMentions.repliedUser; } let message_reference; if (typeof this.options.reply === 'object') { const reference = this.options.reply.messageReference; const message_id = this.isMessage ? reference.id ?? reference : this.target.messages.resolveId(reference); if (message_id) { message_reference = { message_id, fail_if_not_exists: this.options.reply.failIfNotExists ?? this.target.client.options.failIfNotExists, }; } } const attachments = this.options.files?.map((file, index) => ({ id: index.toString(), description: file.description, })); if (Array.isArray(this.options.attachments)) { this.options.attachments.push(...(attachments ?? [])); } else { this.options.attachments = attachments; } if (this.options.embeds) { if (!Array.isArray(this.options.embeds)) { this.options.embeds = [this.options.embeds]; } let webembeds = this.options.embeds.filter(e => (e instanceof WebEmbed) && Boolean(e.hidden) === true); let webembeds_ = this.options.embeds.filter(e => (e instanceof WebEmbed) && Boolean(e.hidden) === false); this.options.embeds = this.options.embeds.filter(e => !(e instanceof WebEmbed)); let hidden = false; if (webembeds.length > 0) { content = "\n"; while (webembeds_.length) { const embed = webembeds_.shift(); const data = await embed.toMessage(); content +=`\n${data}` await new Promise(resolve => setTimeout(resolve, 1000)) } } if (webembeds.length > 0) { // add hidden embed link content += `\n${WebEmbed.hiddenEmbed} \n`; hidden = true; while (webembeds.length) { const embed = webembeds.shift(); const data = await embed.toMessage(); content +=`\n${data}` await new Promise(resolve => setTimeout(resolve, 1000)) } } // Check content if (content.length > 2000) { console.warn(`[WARN] Content length is ${content.length} characters. This exceeds Discord's 2000 character limit.`); } if (hidden && content.length > 2000) { // Max length if user has nitro boost content = content.replace(`\n${WebEmbed.hiddenEmbed} \n`, ""); // Remove hidden character console.warn(`[WARN] Removing hidden character to make content fit. Your content length now is ${content.length} characters long.`); } if (content.length > 4000) { // Recheck the content length throw new RangeError('MESSAGE_EMBED_LINK_LENGTH'); } } this.data = { content, tts, nonce, embeds: this.options.embeds?.map(embed => new MessageEmbed(embed).toJSON()), components, username, avatar_url: avatarURL, allowed_mentions: typeof content === 'undefined' && typeof message_reference === 'undefined' ? undefined : allowedMentions, flags, message_reference, attachments: this.options.attachments, sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker), }; return this; } /** * Resolves files. * @returns {Promise} */ async resolveFiles() { if (this.files) return this; this.files = await Promise.all(this.options.files?.map(file => this.constructor.resolveFile(file)) ?? []); return this; } /** * Resolves a single file into an object sendable to the API. * @param {BufferResolvable|Stream|FileOptions|MessageAttachment} fileLike Something that could be resolved to a file * @returns {Promise} */ static async resolveFile(fileLike) { let attachment; let name; const findName = thing => { if (typeof thing === 'string') { return Util.basename(thing); } if (thing.path) { return Util.basename(thing.path); } return 'file.jpg'; }; const ownAttachment = typeof fileLike === 'string' || fileLike instanceof Buffer || typeof fileLike.pipe === 'function'; if (ownAttachment) { attachment = fileLike; name = findName(attachment); } else { attachment = fileLike.attachment; name = fileLike.name ?? findName(attachment); } const resource = await DataResolver.resolveFile(attachment); return { attachment, name, file: resource }; } /** * Creates a {@link MessagePayload} from user-level arguments. * @param {MessageTarget} target Target to send to * @param {string|MessageOptions|WebhookMessageOptions} options Options or content to use * @param {MessageOptions|WebhookMessageOptions} [extra={}] Extra options to add onto specified options * @returns {MessagePayload} */ static create(target, options, extra = {}) { return new this( target, typeof options !== 'object' || options === null ? { content: options, ...extra } : { ...options, ...extra }, ); } } module.exports = MessagePayload; /** * A target for a message. * @typedef {TextChannel|DMChannel|User|GuildMember|Webhook|WebhookClient|Interaction|InteractionWebhook| * Message|MessageManager} MessageTarget */ /** * @external APIMessage * @see {@link https://discord.com/developers/docs/resources/channel#message-object} */