diff --git a/packages/builders/src/messages/Attachment.ts b/packages/builders/src/messages/Attachment.ts index ba631a53930c..4e12e9d21bce 100644 --- a/packages/builders/src/messages/Attachment.ts +++ b/packages/builders/src/messages/Attachment.ts @@ -28,6 +28,11 @@ export class AttachmentBuilder implements JSONEncodable { * 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 = {}) { this.data = structuredClone(data); diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index edb1ad4c644c..23617c058d66 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -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; diff --git a/packages/discord.js/src/managers/ChannelManager.js b/packages/discord.js/src/managers/ChannelManager.js index b1d3a183af0c..306715c4c0d9 100644 --- a/packages/discord.js/src/managers/ChannelManager.js +++ b/packages/discord.js/src/managers/ChannelManager.js @@ -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'); @@ -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|FileBodyEncodable} options The options to provide * @returns {Promise} * @example * // Send a basic message @@ -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); } diff --git a/packages/discord.js/src/managers/MessageManager.js b/packages/discord.js/src/managers/MessageManager.js index d0a4402b4490..b9e0366b0b05 100644 --- a/packages/discord.js/src/managers/MessageManager.js +++ b/packages/discord.js/src/managers/MessageManager.js @@ -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'); @@ -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|JSONEncodable} options The options to edit the message * @returns {Promise} */ 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) { diff --git a/packages/discord.js/src/structures/AttachmentBuilder.js b/packages/discord.js/src/structures/AttachmentBuilder.js deleted file mode 100644 index 3dd8d1e86c89..000000000000 --- a/packages/discord.js/src/structures/AttachmentBuilder.js +++ /dev/null @@ -1,185 +0,0 @@ -'use strict'; - -const { basename, flatten } = require('../util/Util.js'); - -/** - * Represents an attachment builder - */ -class AttachmentBuilder { - /** - * @param {BufferResolvable|Stream} attachment The file - * @param {AttachmentData} [data] Extra data - */ - constructor(attachment, data = {}) { - /** - * The file associated with this attachment. - * - * @type {BufferResolvable|Stream} - */ - this.attachment = attachment; - - /** - * The name of this attachment - * - * @type {?string} - */ - this.name = data.name; - - /** - * The description of the attachment - * - * @type {?string} - */ - this.description = data.description; - - /** - * The title of the attachment - * - * @type {?string} - */ - this.title = data.title; - - /** - * The base64 encoded byte array representing a sampled waveform - * This is only for voice message attachments. - * - * @type {?string} - */ - this.waveform = data.waveform; - - /** - * The duration of the attachment in seconds - * This is only for voice message attachments. - * - * @type {?number} - */ - this.duration = data.duration; - } - - /** - * Sets the description of this attachment. - * - * @param {string} description The description of the file - * @returns {AttachmentBuilder} This attachment - */ - setDescription(description) { - this.description = description; - return this; - } - - /** - * Sets the file of this attachment. - * - * @param {BufferResolvable|Stream} attachment The file - * @returns {AttachmentBuilder} This attachment - */ - setFile(attachment) { - this.attachment = attachment; - return this; - } - - /** - * Sets the name of this attachment. - * - * @param {string} name The name of the file - * @returns {AttachmentBuilder} This attachment - */ - setName(name) { - this.name = name; - return this; - } - - /** - * Sets the title of this attachment. - * - * @param {string} title The title of the file - * @returns {AttachmentBuilder} This attachment - */ - setTitle(title) { - this.title = title; - return this; - } - - /** - * Sets the waveform of this attachment. - * This is only for voice message attachments. - * - * @param {string} waveform The base64 encoded byte array representing a sampled waveform - * @returns {AttachmentBuilder} This attachment - */ - setWaveform(waveform) { - this.waveform = waveform; - return this; - } - - /** - * Sets the duration of this attachment. - * This is only for voice message attachments. - * - * @param {number} duration The duration of the attachment in seconds - * @returns {AttachmentBuilder} This attachment - */ - setDuration(duration) { - this.duration = duration; - return this; - } - - /** - * Sets whether this attachment is a spoiler - * - * @param {boolean} [spoiler=true] Whether the attachment should be marked as a spoiler - * @returns {AttachmentBuilder} This attachment - */ - setSpoiler(spoiler = true) { - if (spoiler === this.spoiler) return this; - - if (!spoiler) { - while (this.spoiler) { - this.name = this.name.slice('SPOILER_'.length); - } - - return this; - } - - this.name = `SPOILER_${this.name}`; - return this; - } - - /** - * Whether or not this attachment has been marked as a spoiler - * - * @type {boolean} - * @readonly - */ - get spoiler() { - return basename(this.name).startsWith('SPOILER_'); - } - - toJSON() { - return flatten(this); - } - - /** - * Makes a new builder instance from a preexisting attachment structure. - * - * @param {AttachmentBuilder|Attachment|AttachmentPayload} other The builder to construct a new instance from - * @returns {AttachmentBuilder} - */ - static from(other) { - return new AttachmentBuilder(other.attachment, { - name: other.name, - description: other.description, - }); - } -} - -exports.AttachmentBuilder = AttachmentBuilder; - -/** - * @typedef {Object} AttachmentData - * @property {string} [name] The name of the attachment - * @property {string} [description] The description of the attachment - * @property {string} [title] The title of the attachment - * @property {string} [waveform] The base64 encoded byte array representing a sampled waveform (for voice message attachments) - * @property {number} [duration] The duration of the attachment in seconds (for voice message attachments) - */ diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index 0879b5231d71..fde3d89a55b7 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -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|JSONEncodable} options The options to provide * @returns {Promise} * @example * // Update the content of a message diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js index fb752ab92df8..29c0c6d7beca 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<(AttachmentBuilder|Attachment|AttachmentPayload|BufferResolvable)>} [files] + * @property {Array<(Attachment|AttachmentPayload|BufferResolvable)>} [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 @@ -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|FileBodyEncodable} options The options to provide * @returns {Promise} * @example * // Send a basic message diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index 2dc952239b65..2ee48c44304d 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -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} + */ diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 0cba25f74951..b277fea387b7 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, JSONEncodable } from '@discordjs/util'; +import { Awaitable, FileBodyEncodable, JSONEncodable } from '@discordjs/util'; import { WebSocketManager, WebSocketManagerOptions } from '@discordjs/ws'; import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; import { @@ -2234,7 +2234,12 @@ export class Message extends Base { ): InteractionCollector[ComponentType]>; public delete(): Promise>>; public edit( - content: MessageEditOptions | MessagePayload | string, + content: + | FileBodyEncodable + | JSONEncodable + | MessageEditOptions + | MessagePayload + | string, ): Promise>>; public equals(message: Message, rawData: unknown): boolean; public fetchReference(): Promise>>; @@ -2259,26 +2264,6 @@ export class Message extends Base { public inGuild(): this is Message; } -export class AttachmentBuilder { - public constructor(attachment: BufferResolvable | Stream, data?: AttachmentData); - public attachment: BufferResolvable | Stream; - public description: string | null; - public name: string | null; - public title: string | null; - public waveform: string | null; - public duration: number | null; - public get spoiler(): boolean; - public setDescription(description: string): this; - public setFile(attachment: BufferResolvable | Stream, name?: string): this; - public setName(name: string): this; - public setTitle(title: string): this; - public setWaveform(waveform: string): this; - public setDuration(duration: number): this; - public setSpoiler(spoiler?: boolean): this; - public toJSON(): unknown; - public static from(other: JSONEncodable): AttachmentBuilder; -} - export class Attachment { private constructor(data: APIAttachment); private readonly attachment: BufferResolvable | Stream; @@ -4278,7 +4263,12 @@ export class ChannelManager extends CachedManager, iterable: Iterable); public createMessage( channel: Exclude, - options: MessageCreateOptions | MessagePayload | string, + options: + | FileBodyEncodable + | JSONEncodable + | MessageCreateOptions + | MessagePayload + | string, ): Promise>; public fetch(id: Snowflake, options?: FetchChannelOptions): Promise; } @@ -4630,7 +4620,12 @@ export abstract class MessageManager extends public delete(message: MessageResolvable): Promise; public edit( message: MessageResolvable, - options: MessageEditOptions | MessagePayload | string, + options: + | FileBodyEncodable + | JSONEncodable + | MessageEditOptions + | MessagePayload + | string, ): Promise>; public fetch(options: FetchMessageOptions | MessageResolvable): Promise>; public fetch(options?: FetchMessagesOptions): Promise>>; @@ -4807,7 +4802,14 @@ export class VoiceStateManager extends CachedManager = abstract new (...args: any[]) => Entity; export interface SendMethod { - send(options: MessageCreateOptions | MessagePayload | string): Promise>; + send( + options: + | FileBodyEncodable + | JSONEncodable + | MessageCreateOptions + | MessagePayload + | string, + ): Promise>; } export interface PinnableChannelFields { @@ -6270,7 +6272,7 @@ export interface GuildEmojiEditOptions { export interface GuildStickerCreateOptions { description?: string | null; - file: AttachmentPayload | BufferResolvable | JSONEncodable | Stream; + file: AttachmentPayload | BufferResolvable | Stream; name: string; reason?: string; tags: string; @@ -6688,14 +6690,7 @@ export interface BaseMessageOptions { )[]; content?: string; embeds?: readonly (APIEmbed | JSONEncodable)[]; - files?: readonly ( - | Attachment - | AttachmentBuilder - | AttachmentPayload - | BufferResolvable - | JSONEncodable - | Stream - )[]; + files?: readonly (Attachment | AttachmentPayload | BufferResolvable | Stream)[]; } export interface MessageOptionsPoll { diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 7afce5f9a2ba..7969242ea084 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -203,7 +203,6 @@ import type { } from './index.js'; import { ActionRowBuilder, - AttachmentBuilder, ChannelSelectMenuBuilder, Client, Collection, @@ -230,6 +229,7 @@ import { UserSelectMenuComponent, UserSelectMenuInteraction, Webhook, + MessageBuilder, } from './index.js'; // Test type transformation: @@ -453,15 +453,9 @@ client.on('messageCreate', async message => { assertIsMessage(client.channels.createMessage(channel, {})); assertIsMessage(client.channels.createMessage(channel, { embeds: [] })); - const attachment = new AttachmentBuilder('file.png'); const embed = new EmbedBuilder(); - assertIsMessage(channel.send({ files: [attachment] })); assertIsMessage(channel.send({ embeds: [embed] })); - assertIsMessage(channel.send({ embeds: [embed], files: [attachment] })); - - assertIsMessage(client.channels.createMessage(channel, { files: [attachment] })); assertIsMessage(client.channels.createMessage(channel, { embeds: [embed] })); - assertIsMessage(client.channels.createMessage(channel, { embeds: [embed], files: [attachment] })); if (message.inGuild()) { expectAssignable>(message); @@ -3034,14 +3028,11 @@ await guildScheduledEventManager.edit(snowflake, { recurrenceRule: null }); }); } -await textChannel.send({ - files: [ - new AttachmentBuilder('https://example.com/voice-message.ogg') - .setDuration(2) - .setWaveform('AFUqPDw3Eg2hh4+gopOYj4xthU4='), - ], - flags: MessageFlags.IsVoiceMessage, -}); +await textChannel.send( + new MessageBuilder() + .setContent(':)') + .addAttachments(attachment => attachment.setId(1).setFileData(':)').setFilename('smiley.txt')), +); await textChannel.send({ files: [