// Willkommens-Karte: gerendertes PNG (SVG → sharp). Aussehen kommt aus den // Einstellungen (Config → Support → Willkommens-Karte), die Standardwerte sind // der Marken-Look: Avatar mit Neon-Ring, großer Schriftzug, Member-Nummer. import { readFileSync } from 'node:fs'; import sharp from 'sharp'; import { welcomeCard } from '../runtime-settings.js'; const W = 900; const H = 300; const MAX_IMAGE_BYTES = 8 * 1024 * 1024; // Marke fürs Wasserzeichen: einmal beim Start lesen, nicht bei jedem Beitritt. // Fehlt die Datei, bleibt die Karte einfach ohne — wie bei den anderen Bildern // hier auch soll ein fehlendes Detail nicht die ganze Karte kosten. // Erzeugt von docs/brand/build.py. const MARK_URI = (() => { try { const pfad = new URL('./assets/mark-card.png', import.meta.url); return `data:image/png;base64,${readFileSync(pfad).toString('base64')}`; } catch { return ''; } })(); /** XML-Sonderzeichen im Usernamen entschärfen */ function esc(s) { return String(s).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>') .replaceAll('"', '"').replaceAll("'", '''); } /** * Bild holen und als Data-URI einbetten. Gibt bei jedem Problem '' zurück — * die Karte wird dann ohne dieses Bild gerendert statt gar nicht. */ async function fetchDataUri(url, timeoutMs = 5000) { if (!/^https?:\/\//i.test(String(url ?? ''))) return ''; try { const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }); if (!res.ok) return ''; const type = res.headers.get('content-type') ?? ''; if (!type.startsWith('image/')) return ''; const buf = Buffer.from(await res.arrayBuffer()); if (buf.byteLength > MAX_IMAGE_BYTES) return ''; return `data:${type.split(';')[0]};base64,${buf.toString('base64')}`; } catch { return ''; } } /** * Karte rendern. * @param {{ username: string, avatarUrl?: string, memberNumber?: number }} member * @param {object} [override] — Aussehen für die Vorschau, sonst aus den Einstellungen * @returns {Promise} PNG-Buffer */ export async function buildWelcomeCard({ username, avatarUrl, memberNumber }, override) { const look = { ...welcomeCard(), ...(override ?? {}) }; const [avatarData, bgData] = await Promise.all([ fetchDataUri(avatarUrl), look.image ? fetchDataUri(look.image, 6000) : Promise.resolve(''), ]); const name = esc(username.length > 22 ? `${username.slice(0, 21)}…` : username); const number = Number(memberNumber) || 0; const sub = esc(String(look.sub).replaceAll('{count}', number || '?').slice(0, 40)); const kicker = esc(look.kicker); // Über einem Hintergrundbild braucht der Text einen Schleier, sonst säuft er ab const veil = bgData ? `` : ``; const svg = ` ${bgData ? `` : ''} ${veil} ${look.watermark && MARK_URI ? `` : ''} ${avatarData ? `` : ` ?`} ${kicker ? `${kicker}` : ''} ${name} ${sub ? `${sub}` : ''} `; return sharp(Buffer.from(svg)).png().toBuffer(); }