diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index 8806a761da4b..4f204b721575 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -1,7 +1,7 @@ 'use strict'; const { Buffer } = require('node:buffer'); -const { isJSONEncodable, lazy } = require('@discordjs/util'); +const { isJSONEncodable, isRawFileEncodable, lazy } = require('@discordjs/util'); const { DiscordSnowflake } = require('@sapphire/snowflake'); const { DiscordjsError, DiscordjsRangeError, ErrorCodes } = require('../errors/index.js'); const { resolveFile } = require('../util/DataResolver.js'); @@ -190,13 +190,20 @@ class MessagePayload { } } - const attachments = this.options.files?.map((file, index) => ({ - id: index.toString(), - description: file.description, - title: file.title, - waveform: file.waveform, - duration_secs: file.duration, - })); + const attachments = this.options.files?.map((file, index) => + isRawFileEncodable(file) + ? { + id: index.toString(), + ...file.toJSON(), + } + : { + id: index.toString(), + description: file.description, + title: file.title, + waveform: file.waveform, + duration_secs: file.duration, + }, + ); // Only passable during edits if (Array.isArray(this.options.attachments)) { @@ -276,6 +283,8 @@ class MessagePayload { if (ownAttachment) { attachment = fileLike; name = findName(attachment); + } else if (isRawFileEncodable(fileLike)) { + return fileLike.getRawFile(); } else { attachment = fileLike.attachment; name = fileLike.name ?? findName(attachment); diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js index 875ebc4a7cc1..d69461ab1103 100644 --- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js +++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -88,7 +88,7 @@ class TextBasedChannel { * @property {Array<(EmbedBuilder|Embed|APIEmbed)>} [embeds] The embeds for the message * @property {MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the message content * (see {@link https://discord.com/developers/docs/resources/message#allowed-mentions-object here} for more details) - * @property {Array<(Attachment|AttachmentPayload|BufferResolvable|FileBodyEncodable|Stream)>} [files] + * @property {Array<(Attachment|AttachmentPayload|BufferResolvable|RawFileEncodable|Stream)>} [files] * The files to send with the message. * @property {Array<(ActionRowBuilder|MessageTopLevelComponent|APIMessageTopLevelComponent)>} [components] * Action rows containing interactive components for the message (buttons, select menus) and other diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index bc8bba0e8107..a6a0c39387be 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -5,7 +5,7 @@ import { MessagePort, Worker } from 'node:worker_threads'; import { ApplicationCommandOptionAllowedChannelType, MessageActionRowComponentBuilder } from '@discordjs/builders'; import { Collection, ReadonlyCollection } from '@discordjs/collection'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions, EmojiURLOptions } from '@discordjs/rest'; -import { Awaitable, FileBodyEncodable, JSONEncodable } from '@discordjs/util'; +import { Awaitable, FileBodyEncodable, JSONEncodable, RawFileEncodable } from '@discordjs/util'; import { WebSocketManager, WebSocketManagerOptions } from '@discordjs/ws'; import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; import { @@ -6721,7 +6721,7 @@ export interface BaseMessageOptions { )[]; content?: string; embeds?: readonly (APIEmbed | JSONEncodable)[]; - files?: readonly (Attachment | AttachmentPayload | BufferResolvable | FileBodyEncodable | Stream)[]; + files?: readonly (Attachment | AttachmentPayload | BufferResolvable | RawFileEncodable | Stream)[]; } export interface MessageOptionsPoll { diff --git a/packages/util/src/encodables.ts b/packages/util/src/encodables.ts index 392d7b4403ea..de5b246e6fce 100644 --- a/packages/util/src/encodables.ts +++ b/packages/util/src/encodables.ts @@ -1,3 +1,4 @@ +import type { RESTAPIAttachment } from 'discord-api-types/v10'; import type { RawFile } from './RawFile.js'; /** @@ -59,3 +60,24 @@ export interface FileBodyEncodable { export function isFileBodyEncodable(maybeEncodable: unknown): maybeEncodable is FileBodyEncodable { return maybeEncodable !== null && typeof maybeEncodable === 'object' && 'toFileBody' in maybeEncodable; } + +/** + * Represents an object capable of representing itself as a raw file attachment. + * Objects implementing this interface can return binary file data to be sent as part of + * multipart/form-data requests. + */ +export interface RawFileEncodable extends JSONEncodable { + /** + * Returns the raw file of an attachment. + */ + getRawFile(): Partial | undefined; +} + +/** + * Indicates if an object is raw file encodable or not. + * + * @param maybeEncodable - The object to check against + */ +export function isRawFileEncodable(maybeEncodable: unknown): maybeEncodable is RawFileEncodable { + return isJSONEncodable(maybeEncodable) && 'getRawFile' in maybeEncodable; +}