Willkommens-Karten, Geburtstage, Devlog-Permalinks mit OG-Vorschau, Auto-Publish
- Willkommens-Karten: gerendertes PNG (SVG → sharp) mit Avatar im Neon-Ring, Willkommens-Schriftzug und Member-Nummer; Fallback aufs bisherige Embed wenn das Rendering scheitert; Dockerfile installiert fonts-dejavu-core für die Text-Darstellung - Geburtstags-System: /geburtstag setzen|entfernen (birthdays-Tabelle), tägliche Runde ab 09:00 Europe/Berlin (Doppel-Post-Schutz über last_birthday_run), Gratulations-Embed + Tages-Rolle (wird am nächsten Morgen wieder abgeräumt); Kanal + Rolle im Community-Tab - Devlog-Permalinks: /devlogs/:id als eigene Seite (GET /api/devlogs/:id), Link-Symbol an jeder Karte, Link-kopieren-Button; der Server injiziert Open-Graph-Tags ins SPA-HTML — Devlog-Links zeigen Titel, Anriss und Bild, alle anderen Seiten bekommen Default-Tags (auch die Startseite) - Auto-Publish: maybeCrosspost() veröffentlicht Devlog-, Release-, Composer- und geplante Posts automatisch in Ankündigungs-Kanälen - brandEmbed: leere Avatar-URL crasht nicht mehr die Footer-Validierung Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
// Willkommens-Karte: gerendertes PNG (SVG → sharp) im D4RKST3R-Look —
|
||||
// Avatar mit Neon-Ring, großer Willkommens-Schriftzug, Member-Nummer.
|
||||
import sharp from 'sharp';
|
||||
import { brandName } from '../runtime-settings.js';
|
||||
|
||||
const W = 900;
|
||||
const H = 300;
|
||||
|
||||
/** XML-Sonderzeichen im Usernamen entschärfen */
|
||||
function esc(s) {
|
||||
return String(s).replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>')
|
||||
.replaceAll('"', '"').replaceAll("'", ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Karte rendern.
|
||||
* @returns {Promise<Buffer>} PNG-Buffer
|
||||
*/
|
||||
export async function buildWelcomeCard({ username, avatarUrl, memberNumber }) {
|
||||
// Avatar holen und als Data-URI einbetten (128px reicht für den 150px-Kreis)
|
||||
let avatarData = '';
|
||||
try {
|
||||
const res = await fetch(avatarUrl, { signal: AbortSignal.timeout(5000) });
|
||||
if (res.ok) {
|
||||
avatarData = `data:image/png;base64,${Buffer.from(await res.arrayBuffer()).toString('base64')}`;
|
||||
}
|
||||
} catch { /* ohne Avatar rendern */ }
|
||||
|
||||
const name = esc(username.length > 22 ? `${username.slice(0, 21)}…` : username);
|
||||
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="#f5c518" stop-opacity="0"/>
|
||||
<stop offset=".3" stop-color="#f5c518"/>
|
||||
<stop offset=".7" stop-color="#ff4d00"/>
|
||||
<stop offset="1" stop-color="#ff4d00" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="ring" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#f5c518"/>
|
||||
<stop offset="1" stop-color="#ff4d00"/>
|
||||
</linearGradient>
|
||||
<pattern id="grid" width="45" height="45" patternUnits="userSpaceOnUse">
|
||||
<path d="M 45 0 L 0 0 0 45" fill="none" stroke="#f5c518" stroke-opacity=".06" stroke-width="1"/>
|
||||
</pattern>
|
||||
<clipPath id="avatar"><circle cx="150" cy="150" r="75"/></clipPath>
|
||||
</defs>
|
||||
|
||||
<rect width="${W}" height="${H}" fill="#0a0a0a"/>
|
||||
<rect width="${W}" height="${H}" fill="url(#grid)"/>
|
||||
<text x="${W - 18}" y="${H - 24}" text-anchor="end" font-family="DejaVu Sans, sans-serif" font-weight="bold"
|
||||
font-size="120" fill="#f5c518" fill-opacity=".05">${esc(brandName())}</text>
|
||||
<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>`}
|
||||
|
||||
<text x="270" y="118" font-family="DejaVu Sans, sans-serif" font-weight="bold" font-size="26"
|
||||
letter-spacing="10" fill="#8a8378">WILLKOMMEN</text>
|
||||
<text x="270" y="182" font-family="DejaVu Sans, sans-serif" font-weight="bold" font-size="52"
|
||||
fill="#e8e0d0">${name}</text>
|
||||
<text x="270" y="228" font-family="DejaVu Sans, sans-serif" font-size="24"
|
||||
fill="#f5c518">Member #${Number(memberNumber) || '?'}</text>
|
||||
</svg>`;
|
||||
|
||||
return sharp(Buffer.from(svg)).png().toBuffer();
|
||||
}
|
||||
Reference in New Issue
Block a user