/** * Build and send Discord channel webhook messages. * * Discord embed limits we respect: * - field.value <= 1024 chars * - up to 25 fields * - total embed <= 6000 chars * See: https://discord.com/developers/docs/resources/webhook */ import type { BatchState, LibraryItem } from "./types.js"; import { figmaFileUrl } from "./figma.js"; const MAX_ITEMS_PER_FIELD = 15; const FIELD_VALUE_MAX = 1024; export interface DiscordEmbed { title: string; url: string; description?: string; timestamp?: string; color?: number; fields?: Array<{ name: string; value: string; inline?: boolean }>; footer?: { text: string }; } /** * Format a list of library items into a Discord field value. Truncates to * MAX_ITEMS_PER_FIELD entries and/or the field-value character limit, adding * a "+ X more" tail when necessary. */ function formatItemList(items: LibraryItem[]): string { if (items.length === 0) return ""; const shown = items.slice(0, MAX_ITEMS_PER_FIELD); const remaining = items.length - shown.length; const lines = shown.map((it) => `\u2022 ${it.name}`); if (remaining > 0) lines.push(`\u2026 + ${remaining} more`); let value = lines.join("\n"); if (value.length > FIELD_VALUE_MAX) { // Hard cap: trim and append an ellipsis. value = value.slice(0, FIELD_VALUE_MAX - 1) + "\u2026"; } return value; } export function buildEmbed(batch: BatchState): DiscordEmbed { const components = Object.values(batch.items.components); const styles = Object.values(batch.items.styles); const variables = Object.values(batch.items.variables); const fields: DiscordEmbed["fields"] = []; if (components.length > 0) { fields.push({ name: `Components (${components.length})`, value: formatItemList(components), }); } if (styles.length > 0) { fields.push({ name: `Styles (${styles.length})`, value: formatItemList(styles), }); } if (variables.length > 0) { fields.push({ name: `Variables (${variables.length})`, value: formatItemList(variables), }); } const embed: DiscordEmbed = { title: `\u{1F4E6} ${batch.fileName} \u2014 library published`, url: figmaFileUrl(batch.fileKey, batch.fileName), timestamp: new Date(batch.firstSeenAt).toISOString(), color: 0x0acf83, // Figma green-ish }; if (batch.fileDescription && batch.fileDescription.trim().length > 0) { embed.description = batch.fileDescription.slice(0, 4096); } if (fields.length > 0) embed.fields = fields; return embed; } /** * POST an embed to a Discord channel webhook URL. Returns the HTTP response * so the caller can decide whether to retry. */ export async function sendToDiscord( webhookUrl: string, embed: DiscordEmbed, ): Promise { return fetch(webhookUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ embeds: [embed] }), }); }