Zwei Stellen, an denen die Marke bisher fehlte. Willkommens-Karte: Das Wasserzeichen war der Markenname als Text mit 5 % Deckkraft — jetzt der Katzenkopf. Die Datei wird einmal beim Start gelesen statt bei jedem Beitritt, und fehlt sie, rendert die Karte wie bisher ohne Wasserzeichen. Der Schalter heisst entsprechend "Logo im Hintergrund". Link-Vorschau: og:image war nur gesetzt, wenn ein Devlog einen Screenshot hatte. Startseite, Roadmap, Funktionen und jedes Devlog ohne Bild kamen in Discord als graue Textzeile. Jetzt faellt alles ohne eigenes Bild auf og-default.png zurueck (1200x630, Lockup auf Schwarz mit Akzentstreifen), und twitter:card ist durchgehend summary_large_image. Die Laufzeit-Datei liegt unter src/bot/assets/, nicht in docs/ — das Dockerfile kopiert nur src/ und frontend/dist/. Geprueft: librsvg zeichnet das eingebettete <image> wirklich (42726 Pixel unterscheiden sich gegen eine Fassung ohne), das OG-Bild landet im dist. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
124 lines
5.5 KiB
JavaScript
124 lines
5.5 KiB
JavaScript
// 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<Buffer>} 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
|
|
? `<rect width="${W}" height="${H}" fill="${look.background}" fill-opacity=".62"/>`
|
|
: `<rect width="${W}" height="${H}" fill="url(#grid)"/>`;
|
|
|
|
const svg = `<svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
<defs>
|
|
<linearGradient id="line" x1="0" y1="0" x2="1" y2="0">
|
|
<stop offset="0" stop-color="${look.accent}" stop-opacity="0"/>
|
|
<stop offset=".3" stop-color="${look.accent}"/>
|
|
<stop offset=".7" stop-color="${look.accent2}"/>
|
|
<stop offset="1" stop-color="${look.accent2}" stop-opacity="0"/>
|
|
</linearGradient>
|
|
<linearGradient id="ring" x1="0" y1="0" x2="1" y2="1">
|
|
<stop offset="0" stop-color="${look.accent}"/>
|
|
<stop offset="1" stop-color="${look.accent2}"/>
|
|
</linearGradient>
|
|
<pattern id="grid" width="45" height="45" patternUnits="userSpaceOnUse">
|
|
<path d="M 45 0 L 0 0 0 45" fill="none" stroke="${look.accent}" stroke-opacity=".06" stroke-width="1"/>
|
|
</pattern>
|
|
<clipPath id="avatar"><circle cx="150" cy="150" r="75"/></clipPath>
|
|
<clipPath id="card"><rect width="${W}" height="${H}"/></clipPath>
|
|
</defs>
|
|
|
|
<rect width="${W}" height="${H}" fill="${look.background}"/>
|
|
${bgData
|
|
? `<image href="${bgData}" x="0" y="0" width="${W}" height="${H}" clip-path="url(#card)" preserveAspectRatio="xMidYMid slice"/>`
|
|
: ''}
|
|
${veil}
|
|
${look.watermark && MARK_URI
|
|
? `<image href="${MARK_URI}" x="${W - 300}" y="5" width="290" height="290" opacity=".08"
|
|
preserveAspectRatio="xMidYMid meet"/>`
|
|
: ''}
|
|
<rect x="0" y="0" width="${W}" height="3" fill="url(#line)"/>
|
|
<rect x="0" y="${H - 3}" width="${W}" height="3" fill="url(#line)"/>
|
|
|
|
<circle cx="150" cy="150" r="80" fill="none" stroke="url(#ring)" stroke-width="4"/>
|
|
${avatarData
|
|
? `<image href="${avatarData}" x="75" y="75" width="150" height="150" clip-path="url(#avatar)" preserveAspectRatio="xMidYMid slice"/>`
|
|
: `<circle cx="150" cy="150" r="75" fill="#161616"/>
|
|
<text x="150" y="172" text-anchor="middle" font-family="DejaVu Sans, sans-serif" font-size="64" fill="#8a8378">?</text>`}
|
|
|
|
${kicker
|
|
? `<text x="270" y="118" font-family="DejaVu Sans, sans-serif" font-weight="bold" font-size="26"
|
|
letter-spacing="10" fill="#8a8378">${kicker}</text>`
|
|
: ''}
|
|
<text x="270" y="182" font-family="DejaVu Sans, sans-serif" font-weight="bold" font-size="52"
|
|
fill="${look.text}">${name}</text>
|
|
${sub
|
|
? `<text x="270" y="228" font-family="DejaVu Sans, sans-serif" font-size="24"
|
|
fill="${look.accent}">${sub}</text>`
|
|
: ''}
|
|
</svg>`;
|
|
|
|
return sharp(Buffer.from(svg)).png().toBuffer();
|
|
}
|