a little carrier pigeon that ferries figma events to discord
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 102 lines 2.9 kB view raw
1/** 2 * Build and send Discord channel webhook messages. 3 * 4 * Discord embed limits we respect: 5 * - field.value <= 1024 chars 6 * - up to 25 fields 7 * - total embed <= 6000 chars 8 * See: https://discord.com/developers/docs/resources/webhook 9 */ 10 11import type { BatchState, LibraryItem } from "./types.js"; 12import { figmaFileUrl } from "./figma.js"; 13 14const MAX_ITEMS_PER_FIELD = 15; 15const FIELD_VALUE_MAX = 1024; 16 17export interface DiscordEmbed { 18 title: string; 19 url: string; 20 description?: string; 21 timestamp?: string; 22 color?: number; 23 fields?: Array<{ name: string; value: string; inline?: boolean }>; 24 footer?: { text: string }; 25} 26 27/** 28 * Format a list of library items into a Discord field value. Truncates to 29 * MAX_ITEMS_PER_FIELD entries and/or the field-value character limit, adding 30 * a "+ X more" tail when necessary. 31 */ 32function formatItemList(items: LibraryItem[]): string { 33 if (items.length === 0) return ""; 34 35 const shown = items.slice(0, MAX_ITEMS_PER_FIELD); 36 const remaining = items.length - shown.length; 37 38 const lines = shown.map((it) => `\u2022 ${it.name}`); 39 if (remaining > 0) lines.push(`\u2026 + ${remaining} more`); 40 41 let value = lines.join("\n"); 42 if (value.length > FIELD_VALUE_MAX) { 43 // Hard cap: trim and append an ellipsis. 44 value = value.slice(0, FIELD_VALUE_MAX - 1) + "\u2026"; 45 } 46 return value; 47} 48 49export function buildEmbed(batch: BatchState): DiscordEmbed { 50 const components = Object.values(batch.items.components); 51 const styles = Object.values(batch.items.styles); 52 const variables = Object.values(batch.items.variables); 53 54 const fields: DiscordEmbed["fields"] = []; 55 if (components.length > 0) { 56 fields.push({ 57 name: `Components (${components.length})`, 58 value: formatItemList(components), 59 }); 60 } 61 if (styles.length > 0) { 62 fields.push({ 63 name: `Styles (${styles.length})`, 64 value: formatItemList(styles), 65 }); 66 } 67 if (variables.length > 0) { 68 fields.push({ 69 name: `Variables (${variables.length})`, 70 value: formatItemList(variables), 71 }); 72 } 73 74 const embed: DiscordEmbed = { 75 title: `\u{1F4E6} ${batch.fileName} \u2014 library published`, 76 url: figmaFileUrl(batch.fileKey, batch.fileName), 77 timestamp: new Date(batch.firstSeenAt).toISOString(), 78 color: 0x0acf83, // Figma green-ish 79 }; 80 81 if (batch.fileDescription && batch.fileDescription.trim().length > 0) { 82 embed.description = batch.fileDescription.slice(0, 4096); 83 } 84 if (fields.length > 0) embed.fields = fields; 85 86 return embed; 87} 88 89/** 90 * POST an embed to a Discord channel webhook URL. Returns the HTTP response 91 * so the caller can decide whether to retry. 92 */ 93export async function sendToDiscord( 94 webhookUrl: string, 95 embed: DiscordEmbed, 96): Promise<Response> { 97 return fetch(webhookUrl, { 98 method: "POST", 99 headers: { "Content-Type": "application/json" }, 100 body: JSON.stringify({ embeds: [embed] }), 101 }); 102}