Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/builders/src/messages/Attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export class AttachmentBuilder implements JSONEncodable<RESTAPIAttachment> {
* Creates a new attachment builder.
*
* @param data - The API data to create this attachment with
* @example
* ```ts
* const attachment = new AttachmentBuilder().setId(1).setFileData(':)').setFilename('smiley.txt')
* ```
* @remarks Please note that the `id` field is required, it's rather easy to miss!
*/
public constructor(data: Partial<RESTAPIAttachment> = {}) {
this.data = structuredClone(data);
Expand Down
1 change: 0 additions & 1 deletion packages/discord.js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ exports.ApplicationEmoji = require('./structures/ApplicationEmoji.js').Applicati
exports.ApplicationRoleConnectionMetadata =
require('./structures/ApplicationRoleConnectionMetadata.js').ApplicationRoleConnectionMetadata;
exports.Attachment = require('./structures/Attachment.js').Attachment;
exports.AttachmentBuilder = require('./structures/AttachmentBuilder.js').AttachmentBuilder;
exports.AutocompleteInteraction = require('./structures/AutocompleteInteraction.js').AutocompleteInteraction;
exports.AutoModerationActionExecution =
require('./structures/AutoModerationActionExecution.js').AutoModerationActionExecution;
Expand Down
17 changes: 10 additions & 7 deletions packages/discord.js/src/managers/ChannelManager.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const process = require('node:process');
const { lazy } = require('@discordjs/util');
const { lazy, isFileBodyEncodable, isJSONEncodable } = require('@discordjs/util');
const { Routes } = require('discord-api-types/v10');
const { BaseChannel } = require('../structures/BaseChannel.js');
const { MessagePayload } = require('../structures/MessagePayload.js');
Expand Down Expand Up @@ -147,7 +147,7 @@ class ChannelManager extends CachedManager {
* Creates a message in a channel.
*
* @param {TextChannelResolvable} channel The channel to send the message to
* @param {string|MessagePayload|MessageCreateOptions} options The options to provide
* @param {string|MessagePayload|MessageCreateOptions|JSONEncodable<RESTPostAPIChannelMessageJSONBody>|FileBodyEncodable<RESTPostAPIChannelMessageJSONBody>} options The options to provide
* @returns {Promise<Message>}
* @example
* // Send a basic message
Expand All @@ -174,18 +174,21 @@ class ChannelManager extends CachedManager {
* .catch(console.error);
*/
async createMessage(channel, options) {
let messagePayload;
let payload;

if (options instanceof MessagePayload) {
messagePayload = options.resolveBody();
payload = await options.resolveBody().resolveFiles();
} else if (isFileBodyEncodable(options)) {
payload = options.toFileBody();
} else if (isJSONEncodable(options)) {
payload = { body: options.toJSON() };
} else {
messagePayload = MessagePayload.create(this, options).resolveBody();
payload = await MessagePayload.create(this, options).resolveBody().resolveFiles();
}

const resolvedChannelId = this.resolveId(channel);
const resolvedChannel = this.resolve(channel);
const { body, files } = await messagePayload.resolveFiles();
const data = await this.client.rest.post(Routes.channelMessages(resolvedChannelId), { body, files });
const data = await this.client.rest.post(Routes.channelMessages(resolvedChannelId), payload);

return resolvedChannel?.messages._add(data) ?? new (getMessage())(this.client, data);
}
Expand Down
25 changes: 16 additions & 9 deletions packages/discord.js/src/managers/MessageManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { isFileBodyEncodable, isJSONEncodable } = require('@discordjs/util');
const { Routes } = require('discord-api-types/v10');
const { DiscordjsTypeError, ErrorCodes } = require('../errors/index.js');
const { Message } = require('../structures/Message.js');
Expand Down Expand Up @@ -223,21 +224,27 @@ class MessageManager extends CachedManager {
* Edits a message, even if it's not cached.
*
* @param {MessageResolvable} message The message to edit
* @param {string|MessageEditOptions|MessagePayload} options The options to edit the message
* @param {string|MessageEditOptions|MessagePayload|FileBodyEncodable<RESTPatchAPIChannelMessageJSONBody>|JSONEncodable<RESTPatchAPIChannelMessageJSONBody>} options The options to edit the message
* @returns {Promise<Message>}
*/
async edit(message, options) {
const messageId = this.resolveId(message);
if (!messageId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'message', 'MessageResolvable');

const { body, files } = await (
options instanceof MessagePayload
? options
: MessagePayload.create(message instanceof Message ? message : this, options)
)
.resolveBody()
.resolveFiles();
const data = await this.client.rest.patch(Routes.channelMessage(this.channel.id, messageId), { body, files });
let payload;
if (options instanceof MessagePayload) {
payload = await options.resolveBody().resolveFiles();
} else if (isFileBodyEncodable(options)) {
payload = options.toFileBody();
} else if (isJSONEncodable(options)) {
payload = { body: options.toJSON() };
} else {
payload = await MessagePayload.create(message instanceof Message ? message : this, options)
.resolveBody()
.resolveFiles();
}

const data = await this.client.rest.patch(Routes.channelMessage(this.channel.id, messageId), payload);

const existing = this.cache.get(messageId);
if (existing) {
Expand Down
185 changes: 0 additions & 185 deletions packages/discord.js/src/structures/AttachmentBuilder.js

This file was deleted.

2 changes: 1 addition & 1 deletion packages/discord.js/src/structures/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ class Message extends Base {
/**
* Edits the content of the message.
*
* @param {string|MessagePayload|MessageEditOptions} options The options to provide
* @param {string|MessageEditOptions|MessagePayload|FileBodyEncodable<RESTPatchAPIChannelMessageJSONBody>|JSONEncodable<RESTPatchAPIChannelMessageJSONBody>} options The options to provide
* @returns {Promise<Message>}
* @example
* // Update the content of a message
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<(AttachmentBuilder|Attachment|AttachmentPayload|BufferResolvable)>} [files]
* @property {Array<(Attachment|AttachmentPayload|BufferResolvable)>} [files]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer if there was still a way to pass an AttachmentBuilder into here, albeit not sure what type would make sense to put for that here.

Another helper type like RawFileEncodable which extends JSONEncodable with the additional getRawFile() method? Seems clunky but 🤷‍♂️

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main issue I have with this is that AttachmentBuilder includes data tied to.. well, the attachments array.

What do we do when the user sets that data and they also provide attachments manually?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same we did before:

const attachments = this.options.files?.map((file, index) => ({
id: index.toString(),
description: file.description,
title: file.title,
waveform: file.waveform,
duration_secs: file.duration,
}));
if (Array.isArray(this.options.attachments)) {
this.options.attachments.push(...(attachments ?? []));
} else {
this.options.attachments = attachments;
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is absolutely awful in terms of library behavior. That snippet effectively means that if you pass both files and attachments at the same time, it's a guaranteed API error, unless it's a message edit and the elements the user passed to the attachments array specifically refer to existing attachments.

In essence, this renders the attachments field as edit-only, we're just completely taking over it with this behavior, largely hiding how the API payload works too (which I am not fond of at all, no matter how rich the the abstraction).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But that's what AttachmentBuilder does too... it hides the same things the same way.

Copy link
Member Author

@didinele didinele Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. This isn't about hiding things, it's allowing the user to easily pass garbage. AttachmentBuilder on its own is fundamentally different from what you want, it removes your granular control of the files/attachments fields, it ties them together completely. There is no way when using it to get non-sense miss-matched arrays. All the more so when you use MessageBuilder

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circling back on this, the only way I'd accept it is making it a union type that forbids files if attachments is AttachmentBuilder[], I'm not compromising otherwise

Copy link
Member

@Qjuh Qjuh Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thing is that all the other types allowed for files here will also fill the attachments. So if anything it would make sense to completely disallow attachments in create Message options.

And you got it backwards: it's an accepted type for the files property and will disallow the attachments property.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. I'm okay with designing the API that way, but we do also have to acknowledge the complexity it adds

* 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
Expand Down Expand Up @@ -156,7 +156,7 @@ class TextBasedChannel {
/**
* Sends a message to this channel.
*
* @param {string|MessagePayload|MessageCreateOptions} options The options to provide
* @param {string|MessagePayload|MessageCreateOptions|JSONEncodable<RESTPostAPIChannelMessageJSONBody>|FileBodyEncodable<RESTPostAPIChannelMessageJSONBody>} options The options to provide
* @returns {Promise<Message>}
* @example
* // Send a basic message
Expand Down
10 changes: 10 additions & 0 deletions packages/discord.js/src/util/APITypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -683,3 +683,13 @@
* @external WebhookType
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/WebhookType}
*/

/**
* @external RESTPostAPIChannelMessageJSONBody
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/RESTPostAPIChannelMessageJSONBody}
*/

/**
* @external RESTPatchAPIChannelMessageJSONBody
* @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/RESTPatchAPIChannelMessageJSONBody}
*/
Loading