import type { APIFileComponent, APIUnfurledMediaItem } from "discord.js"; import { ComponentBuilder, ComponentType } from "discord.js"; /** * Builder for creating file components in Discord messages * File components must be used alone in an action row * * IMPORTANT: File URLs must use the `attachment://` protocol. * Use the `createAttachmentUrl()` helper to ensure correct URL format. */ export class V2FileBuilder extends ComponentBuilder { private file!: APIUnfurledMediaItem; private spoiler?: boolean; constructor() { super({ type: ComponentType.File }); } /** * Sets the file data for this component * @param file - The file data with url in `attachment://` format * @throws {Error} If the file URL doesn't use the attachment:// protocol */ setFile(file: APIUnfurledMediaItem) { if (!file.url.startsWith("attachment://")) { throw new Error( "File URL must use the attachment:// protocol. Use createAttachmentUrl() helper.", ); } this.file = file; return this; } /** * Sets whether the file should be marked as a spoiler * @param spoiler - Whether to mark as spoiler */ setSpoiler(spoiler: boolean) { this.spoiler = spoiler; return this; } /** * Get the API-compatible JSON data for this component */ toJSON(): APIFileComponent { if (!this.file) { throw new Error("File data must be set using setFile()"); } return { id: this.data.id, type: ComponentType.File, file: this.file, spoiler: this.spoiler ?? false, }; } } /** * Creates a properly formatted attachment URL for Discord file components * @param filename - The name of the file to reference * @returns A properly formatted attachment URL */ export function createAttachmentUrl(filename: string): string { // Remove any leading 'attachment://' if present to avoid duplication const cleanFilename = filename.replace(/^attachment:\/\//, ""); return `attachment://${cleanFilename}`; } /** * Helper function to create a file component * @param file - The file data (must use attachment:// protocol) * @param options - Optional configuration * @returns A new file component builder instance * * @example * ```typescript * // Correct usage with attachment protocol * const fileComponent = makeFile( * { url: createAttachmentUrl('avatar.png') }, * { id: 123, spoiler: false } * ); * * // When sending the message, provide the actual file: * channel.send({ * files: [ * { attachment: './path/to/avatar.png', name: 'avatar.png' } * ], * components: [new Discord.ActionRowBuilder().addComponents(fileComponent)] * }); * ``` */ export function makeFile( file: APIUnfurledMediaItem, options?: { id?: number; spoiler?: boolean }, ): V2FileBuilder { const builder = new V2FileBuilder(); builder.setFile(file); if (options?.id !== undefined) { builder.setId(options.id); } if (options?.spoiler !== undefined) { builder.setSpoiler(options.spoiler); } return builder; }