diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index 1a8c70777..bec6e3352 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -123,6 +123,7 @@ exports.InviteStageInstance = require('./structures/InviteStageInstance'); exports.InviteGuild = require('./structures/InviteGuild'); exports.Message = require('./structures/Message').Message; exports.Attachment = require('./structures/Attachment'); +exports.AttachmentBuilder = require('./structures/AttachmentBuilder'); exports.ModalBuilder = require('./structures/ModalBuilder'); exports.MessageCollector = require('./structures/MessageCollector'); exports.MessageComponentInteraction = require('./structures/MessageComponentInteraction'); diff --git a/packages/discord.js/src/managers/GuildStickerManager.js b/packages/discord.js/src/managers/GuildStickerManager.js index 1d8c73ad8..4e1864451 100644 --- a/packages/discord.js/src/managers/GuildStickerManager.js +++ b/packages/discord.js/src/managers/GuildStickerManager.js @@ -41,7 +41,7 @@ class GuildStickerManager extends CachedManager { /** * Creates a new custom sticker in the guild. - * @param {BufferResolvable|Stream|FileOptions|Attachment} file The file for the sticker + * @param {BufferResolvable|Stream|JSONEncodable} file The file for the sticker * @param {string} name The name for the sticker * @param {string} tags The Discord name of a unicode emoji representing the sticker's expression * @param {GuildStickerCreateOptions} [options] Options diff --git a/packages/discord.js/src/structures/Attachment.js b/packages/discord.js/src/structures/Attachment.js index ead506c98..ed547e437 100644 --- a/packages/discord.js/src/structures/Attachment.js +++ b/packages/discord.js/src/structures/Attachment.js @@ -3,74 +3,30 @@ const Util = require('../util/Util'); /** - * Represents an attachment. + * @typedef {Object} AttachmentPayload + * @property {?string} name The name of the attachment + * @property {Stream|BufferResolvable} attachment The attachment in this payload + * @property {?string} description The description of the attachment + */ + +/** + * Represents an attachment */ class Attachment { /** - * @param {BufferResolvable|Stream} attachment The file - * @param {string} [name=null] The name of the file, if any - * @param {APIAttachment} [data] Extra data + * @param {APIAttachment} data Attachment data + * @private */ - constructor(attachment, name = null, data) { - this.attachment = attachment; + constructor({ url, filename, ...data }) { + this.attachment = url; /** * The name of this attachment - * @type {?string} + * @type {string} */ - this.name = name; + this.name = filename; if (data) this._patch(data); } - /** - * Sets the description of this attachment. - * @param {string} description The description of the file - * @returns {Attachment} This attachment - */ - setDescription(description) { - this.description = description; - return this; - } - - /** - * Sets the file of this attachment. - * @param {BufferResolvable|Stream} attachment The file - * @param {string} [name=null] The name of the file, if any - * @returns {Attachment} This attachment - */ - setFile(attachment, name = null) { - this.attachment = attachment; - this.name = name; - return this; - } - - /** - * Sets the name of this attachment. - * @param {string} name The name of the file - * @returns {Attachment} This attachment - */ - setName(name) { - this.name = name; - return this; - } - - /** - * Sets whether this attachment is a spoiler - * @param {boolean} [spoiler=true] Whether the attachment should be marked as a spoiler - * @returns {Attachment} 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; - } - _patch(data) { /** * The attachment's id @@ -164,8 +120,3 @@ class Attachment { } module.exports = Attachment; - -/** - * @external APIAttachment - * @see {@link https://discord.com/developers/docs/resources/channel#attachment-object} - */ diff --git a/packages/discord.js/src/structures/AttachmentBuilder.js b/packages/discord.js/src/structures/AttachmentBuilder.js new file mode 100644 index 000000000..fe4ce59a2 --- /dev/null +++ b/packages/discord.js/src/structures/AttachmentBuilder.js @@ -0,0 +1,110 @@ +'use strict'; + +const Util = require('../util/Util'); + +/** + * Represents an attachment builder + */ +class AttachmentBuilder { + /** + * @param {BufferResolvable|Stream} attachment The file + * @param {APIAttachment} [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; + } + + /** + * 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 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 Util.basename(this.name).startsWith('SPOILER_'); + } + + toJSON() { + return Util.flatten(this); + } + + /** + * Makes a new builder instance from a preexisting attachment structure. + * @param {JSONEncodable} 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, + }); + } +} + +module.exports = AttachmentBuilder; + +/** + * @external APIAttachment + * @see {@link https://discord.com/developers/docs/resources/channel#attachment-object} + */ diff --git a/packages/discord.js/src/structures/CommandInteraction.js b/packages/discord.js/src/structures/CommandInteraction.js index 9691f5459..cfce2c8bc 100644 --- a/packages/discord.js/src/structures/CommandInteraction.js +++ b/packages/discord.js/src/structures/CommandInteraction.js @@ -133,7 +133,7 @@ class CommandInteraction extends Interaction { if (attachments) { result.attachments = new Collection(); for (const attachment of Object.values(attachments)) { - const patched = new Attachment(attachment.url, attachment.filename, attachment); + const patched = new Attachment(attachment); result.attachments.set(attachment.id, patched); } } @@ -189,7 +189,7 @@ class CommandInteraction extends Interaction { if (role) result.role = this.guild?.roles._add(role) ?? role; const attachment = resolved.attachments?.[option.value]; - if (attachment) result.attachment = new Attachment(attachment.url, attachment.filename, attachment); + if (attachment) result.attachment = new Attachment(attachment); } return result; diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index 4f69b4b1b..c2cbab66b 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -159,7 +159,7 @@ class Message extends Base { this.attachments = new Collection(); if (data.attachments) { for (const attachment of data.attachments) { - this.attachments.set(attachment.id, new Attachment(attachment.url, attachment.filename, attachment)); + this.attachments.set(attachment.id, new Attachment(attachment)); } } } else { @@ -644,7 +644,8 @@ class Message extends Base { * Only `MessageFlags.SuppressEmbeds` can be edited. * @property {Attachment[]} [attachments] An array of attachments to keep, * all attachments will be kept if omitted - * @property {FileOptions[]|BufferResolvable[]|Attachment[]} [files] Files to add to the message + * @property {Array>|BufferResolvable[]|Attachment[]|AttachmentBuilder[]} [files] + * Files to add to the message * @property {ActionRow[]|ActionRowOptions[]} [components] * Action rows containing interactive components for the message (buttons, select menus) */ diff --git a/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index 2bbeb764c..3f1769bcd 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -224,7 +224,8 @@ class MessagePayload { /** * Resolves a single file into an object sendable to the API. - * @param {BufferResolvable|Stream|FileOptions|Attachment} fileLike Something that could be resolved to a file + * @param {BufferResolvable|Stream|JSONEncodable} fileLike Something that could + * be resolved to a file * @returns {Promise} */ static async resolveFile(fileLike) { diff --git a/packages/discord.js/src/structures/Webhook.js b/packages/discord.js/src/structures/Webhook.js index 9fc21354e..f563954a8 100644 --- a/packages/discord.js/src/structures/Webhook.js +++ b/packages/discord.js/src/structures/Webhook.js @@ -132,7 +132,8 @@ class Webhook { * @typedef {Object} WebhookEditMessageOptions * @property {Embed[]|APIEmbed[]} [embeds] See {@link WebhookMessageOptions#embeds} * @property {string} [content] See {@link BaseMessageOptions#content} - * @property {FileOptions[]|BufferResolvable[]|Attachment[]} [files] See {@link BaseMessageOptions#files} + * @property {JSONEncodable|BufferResolvable[]|Attachment[]|AttachmentBuilder[]} [files] + * See {@link BaseMessageOptions#files} * @property {MessageMentionOptions} [allowedMentions] See {@link BaseMessageOptions#allowedMentions} * @property {Attachment[]} [attachments] Attachments to send with the message * @property {ActionRow[]|ActionRowOptions[]} [components] diff --git a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js index a9bf3643d..793ffbdf1 100644 --- a/packages/discord.js/src/structures/interfaces/TextBasedChannel.js +++ b/packages/discord.js/src/structures/interfaces/TextBasedChannel.js @@ -64,7 +64,7 @@ class TextBasedChannel { * @property {FileOptions[]|BufferResolvable[]|Attachment[]} [files] Files to send with the message * @property {ActionRow[]|ActionRowOptions[]} [components] * Action rows containing interactive components for the message (buttons, select menus) - * @property {Attachment[]} [attachments] Attachments to send in the message + * @property {Array>} [attachments] Attachments to send in the message */ /** diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 4fba933d7..70376ae47 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -117,6 +117,7 @@ import { LocalizationMap, LocaleString, MessageActivityType, + APIAttachment, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -1696,9 +1697,22 @@ export class Message extends Base { public inGuild(): this is Message & this; } -export class Attachment { +export class AttachmentBuilder { public constructor(attachment: BufferResolvable | Stream, name?: string, data?: RawAttachmentData); + public attachment: BufferResolvable | Stream; + public description: string | null; + public name: string | null; + public get spoiler(): boolean; + public setDescription(description: string): this; + public setFile(attachment: BufferResolvable | Stream, name?: string): this; + public setName(name: string): this; + public setSpoiler(spoiler?: boolean): this; + public toJSON(): unknown; + public static from(other: JSONEncodable): AttachmentBuilder; +} +export class Attachment { + private constructor(data: APIAttachment); public attachment: BufferResolvable | Stream; public contentType: string | null; public description: string | null; @@ -1711,10 +1725,6 @@ export class Attachment { public get spoiler(): boolean; public url: string; public width: number | null; - public setDescription(description: string): this; - public setFile(attachment: BufferResolvable | Stream, name?: string): this; - public setName(name: string): this; - public setSpoiler(spoiler?: boolean): this; public toJSON(): unknown; } @@ -1838,7 +1848,9 @@ export class MessagePayload { options: string | MessageOptions | WebhookMessageOptions, extra?: MessageOptions | WebhookMessageOptions, ): MessagePayload; - public static resolveFile(fileLike: BufferResolvable | Stream | FileOptions | Attachment): Promise; + public static resolveFile( + fileLike: BufferResolvable | Stream | AttachmentPayload | JSONEncodable, + ): Promise; public makeContent(): string | undefined; public resolveBody(): this; @@ -3199,7 +3211,7 @@ export class GuildStickerManager extends CachedManager); public guild: Guild; public create( - file: BufferResolvable | Stream | FileOptions | Attachment, + file: BufferResolvable | Stream | AttachmentPayload | JSONEncodable, name: string, tags: string, options?: GuildStickerCreateOptions, @@ -3939,7 +3951,7 @@ export interface CommandInteractionOption member?: CacheTypeReducer; channel?: CacheTypeReducer; role?: CacheTypeReducer; - attachment?: Attachment; + attachment?: AttachmentBuilder; message?: GuildCacheMessage; } @@ -3949,7 +3961,7 @@ export interface CommandInteractionResolvedData>; channels?: Collection>; messages?: Collection>; - attachments?: Collection; + attachments?: Collection; } export declare const Colors: { @@ -4241,7 +4253,7 @@ export interface FetchThreadsOptions { active?: boolean; } -export interface FileOptions { +export interface AttachmentPayload { attachment: BufferResolvable | Stream; name?: string; description?: string; @@ -4672,10 +4684,10 @@ export type MessageChannelComponentCollectorOptions; export interface MessageEditOptions { - attachments?: Attachment[]; + attachments?: JSONEncodable[]; content?: string | null; embeds?: (JSONEncodable | APIEmbed)[] | null; - files?: (FileOptions | BufferResolvable | Stream | Attachment)[]; + files?: (AttachmentPayload | BufferResolvable | Stream | AttachmentBuilder)[]; flags?: BitFieldResolvable; allowedMentions?: MessageMentionOptions; components?: ( @@ -4726,10 +4738,10 @@ export interface MessageOptions { | APIActionRowComponent )[]; allowedMentions?: MessageMentionOptions; - files?: (FileOptions | BufferResolvable | Stream | Attachment)[]; + files?: (Attachment | AttachmentBuilder | BufferResolvable | Stream)[]; reply?: ReplyOptions; stickers?: StickerResolvable[]; - attachments?: Attachment[]; + attachments?: (Attachment | AttachmentBuilder)[]; flags?: BitFieldResolvable, number>; } diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index 0d20f30fe..c58c0e225 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -57,7 +57,7 @@ import { Interaction, InteractionCollector, Message, - Attachment, + AttachmentBuilder, MessageCollector, MessageComponentInteraction, MessageReaction, @@ -615,7 +615,7 @@ client.on('messageCreate', async message => { assertIsMessage(channel.send({})); assertIsMessage(channel.send({ embeds: [] })); - const attachment = new Attachment('file.png'); + const attachment = new AttachmentBuilder('file.png'); const embed = new EmbedBuilder(); assertIsMessage(channel.send({ files: [attachment] })); assertIsMessage(channel.send({ embeds: [embed] })); @@ -1492,8 +1492,8 @@ expectNotAssignable>({ declare const chatInputInteraction: ChatInputCommandInteraction; -expectType(chatInputInteraction.options.getAttachment('attachment', true)); -expectType(chatInputInteraction.options.getAttachment('attachment')); +expectType(chatInputInteraction.options.getAttachment('attachment', true)); +expectType(chatInputInteraction.options.getAttachment('attachment')); declare const modal: ModalBuilder;