fix(MessagePayload): allow AttachmentBuilder in files payload (#11423)

* fix(MessagePayload): allow AttachmentBuilder in files payload

* fix: typo

* chore: apply suggestions from code review

Co-authored-by: Almeida <github@almeidx.dev>

---------

Co-authored-by: Almeida <github@almeidx.dev>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Qjuh
2026-04-09 11:27:02 +02:00
committed by GitHub
parent 1f9affd979
commit aa2767bd6f
4 changed files with 43 additions and 11 deletions

View File

@@ -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,16 +190,24 @@ 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,
}));
let 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)) {
attachments ??= [];
attachments.push(
// Note how we don't check for file body encodable, since we aren't expecting file data here
...this.options.attachments.map(attachment => (isJSONEncodable(attachment) ? attachment.toJSON() : attachment)),
@@ -276,6 +284,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);

View File

@@ -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<APIAttachment>|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

View File

@@ -4,7 +4,7 @@ import { Stream } from 'node:stream';
import { MessagePort, Worker } from 'node:worker_threads';
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 {
@@ -6754,7 +6754,7 @@ export interface BaseMessageOptions {
)[];
content?: string | null;
embeds?: readonly (APIEmbed | JSONEncodable<APIEmbed>)[];
files?: readonly (Attachment | AttachmentPayload | BufferResolvable | FileBodyEncodable<APIAttachment> | Stream)[];
files?: readonly (Attachment | AttachmentPayload | BufferResolvable | RawFileEncodable | Stream)[];
}
export interface BaseMessageSendOptions extends BaseMessageOptions {

View File

@@ -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<BodyValue> {
export function isFileBodyEncodable(maybeEncodable: unknown): maybeEncodable is FileBodyEncodable<unknown> {
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<RESTAPIAttachment> {
/**
* Returns the raw file of an attachment.
*/
getRawFile(): Partial<RawFile> | 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;
}