Willkommens-Karte einstellbar: Farben, Bild, Texte, Live-Vorschau
Die Karte war komplett fest verdrahtet — Farben, der Schriftzug „WILLKOMMEN", die Zeile „Member #N", das Hintergrund-Raster. Wer etwas ändern wollte, musste das SVG im Code anfassen. Jetzt steht das Aussehen unter Config → Support: vier Farben, die beiden Textzeilen, ein optionales Hintergrundbild per Adresse, der Markenname im Hintergrund an/aus, und die Karte selbst abschaltbar (dann bleibt das Text-Embed mit Avatar als Vorschaubild). Daneben eine Vorschau, die beim Tippen mitrendert — sie geht über /api/welcome-preview.png und bekommt die Werte als Query, damit man sieht, was man einstellt, bevor gespeichert wird. Gespeichert wird davon nichts. Die Akzentfarben greifen auf die Markenfarben zurück, solange nichts Eigenes gesetzt ist. Wer also die Marke umfärbt, hat die Karte gleich mit. Beim Hintergrundbild ist Vorsicht eingebaut: nur http(s), Zeitlimit, Größenbegrenzung, und ein nicht erreichbares Bild lässt die Karte ohne Bild rendern statt sie ausfallen zu lassen. Über einem Bild liegt ein Schleier, sonst säuft der Text ab. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+75
-1
@@ -29,7 +29,7 @@ import { publishRoleMenu, unpublishRoleMenu, MAX_ENTRIES } from '../bot/role-men
|
||||
import { computeNextRun } from './scheduled-posts.js';
|
||||
import { config } from '../config.js';
|
||||
import { removeDevlog } from '../bot/devlog-archive.js';
|
||||
import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl, roadmapRepo, brandColor, brandFooter, brandName, giteaApiToken, votingChannelId, discordGuildId, discordInviteUrl, legalInfo, hubUrl, botUrl } from '../runtime-settings.js';
|
||||
import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl, roadmapRepo, brandColor, brandFooter, brandName, giteaApiToken, votingChannelId, discordGuildId, discordInviteUrl, legalInfo, hubUrl, botUrl, welcomeCard } from '../runtime-settings.js';
|
||||
import { xpForLevel } from '../bot/levels.js';
|
||||
import { getMilestones } from '../gitea-api.js';
|
||||
import { getSessionUser, isAdmin, isGuildMember } from './auth.js';
|
||||
@@ -185,6 +185,38 @@ export function registerApiRoutes(app, client) {
|
||||
return { ok: true, modules: moduleStates() };
|
||||
});
|
||||
|
||||
// Vorschau der Willkommens-Karte. Nimmt das Aussehen als Query entgegen,
|
||||
// damit man im Panel sieht, was man einstellt, bevor gespeichert wird.
|
||||
app.get('/api/welcome-preview.png', async (request, reply) => {
|
||||
if (requireScope(request, reply, 'settings')) return;
|
||||
const q = request.query ?? {};
|
||||
const hex = (v) => (/^#[0-9a-fA-F]{6}$/.test(String(v ?? '')) ? String(v).toLowerCase() : undefined);
|
||||
// Nur bekannte Felder übernehmen — der Rest kommt aus den Einstellungen
|
||||
const override = {};
|
||||
for (const key of ['background', 'accent', 'accent2', 'text']) {
|
||||
const value = hex(q[key]);
|
||||
if (value) override[key] = value;
|
||||
}
|
||||
if (q.kicker !== undefined) override.kicker = String(q.kicker).slice(0, 24);
|
||||
if (q.sub !== undefined) override.sub = String(q.sub).slice(0, 60);
|
||||
if (q.image !== undefined) override.image = String(q.image).trim();
|
||||
if (q.watermark !== undefined) override.watermark = q.watermark !== '0';
|
||||
|
||||
try {
|
||||
const { buildWelcomeCard } = await import('../bot/welcome-card.js');
|
||||
const png = await buildWelcomeCard({
|
||||
username: String(q.name ?? '').trim() || client.user?.username || 'Neues Mitglied',
|
||||
avatarUrl: client.user?.displayAvatarURL?.({ extension: 'png', size: 128 }),
|
||||
memberNumber: Number(q.count) || 42,
|
||||
}, override);
|
||||
reply.header('Cache-Control', 'no-store');
|
||||
return reply.type('image/png').send(png);
|
||||
} catch (error) {
|
||||
request.log.error({ err: error }, 'Willkommens-Vorschau fehlgeschlagen');
|
||||
return reply.code(500).send({ error: 'Vorschau fehlgeschlagen' });
|
||||
}
|
||||
});
|
||||
|
||||
// Funktionsliste für die Produktseite — öffentlich und ohne Zustand:
|
||||
// was der Bot kann, nicht was hier gerade eingeschaltet ist. Quelle ist
|
||||
// dasselbe Register wie die Module, damit die Seite nicht veraltet.
|
||||
@@ -493,6 +525,20 @@ ${rssItems}
|
||||
screenshot_channel_id: getSetting('screenshot_channel_id') ?? '',
|
||||
modmail_channel_id: getSetting('modmail_channel_id') ?? '',
|
||||
welcome_channel_id: getSetting('welcome_channel_id') ?? '',
|
||||
...(() => {
|
||||
const card = welcomeCard();
|
||||
return {
|
||||
welcome_card_enabled: card.enabled,
|
||||
welcome_card_bg: card.background,
|
||||
welcome_card_accent: card.accent,
|
||||
welcome_card_accent2: card.accent2,
|
||||
welcome_card_text: card.text,
|
||||
welcome_card_kicker: card.kicker,
|
||||
welcome_card_sub: card.sub,
|
||||
welcome_card_image: card.image,
|
||||
welcome_card_watermark: card.watermark,
|
||||
};
|
||||
})(),
|
||||
modlog_channel_id: getSetting('modlog_channel_id') ?? '',
|
||||
status_channel_id: getSetting('status_channel_id') ?? '',
|
||||
voting_channel_id: getSetting('voting_channel_id') ?? '',
|
||||
@@ -647,6 +693,34 @@ ${rssItems}
|
||||
if (body.level_rewards !== undefined) {
|
||||
setSetting('level_rewards', String(body.level_rewards).trim());
|
||||
}
|
||||
// Willkommens-Karte
|
||||
if (body.welcome_card_enabled !== undefined) {
|
||||
setSetting('welcome_card_enabled', body.welcome_card_enabled ? '1' : '0');
|
||||
}
|
||||
if (body.welcome_card_watermark !== undefined) {
|
||||
setSetting('welcome_card_watermark', body.welcome_card_watermark ? '1' : '0');
|
||||
}
|
||||
for (const key of ['welcome_card_bg', 'welcome_card_accent', 'welcome_card_accent2', 'welcome_card_text']) {
|
||||
if (body[key] === undefined) continue;
|
||||
const value = String(body[key]).trim();
|
||||
if (!/^#[0-9a-fA-F]{6}$/.test(value)) {
|
||||
return reply.code(400).send({ error: `${key}: Hex-Farbe wie #f5c518 erwartet` });
|
||||
}
|
||||
setSetting(key, value.toLowerCase());
|
||||
}
|
||||
if (body.welcome_card_kicker !== undefined) {
|
||||
setSetting('welcome_card_kicker', String(body.welcome_card_kicker).slice(0, 24));
|
||||
}
|
||||
if (body.welcome_card_sub !== undefined) {
|
||||
setSetting('welcome_card_sub', String(body.welcome_card_sub).slice(0, 60));
|
||||
}
|
||||
if (body.welcome_card_image !== undefined) {
|
||||
const value = String(body.welcome_card_image).trim();
|
||||
if (value && !/^https?:\/\//i.test(value)) {
|
||||
return reply.code(400).send({ error: 'welcome_card_image: Bild-Adresse mit http(s) erwartet' });
|
||||
}
|
||||
setSetting('welcome_card_image', value);
|
||||
}
|
||||
// Branding
|
||||
if (body.brand_name !== undefined) {
|
||||
setSetting('brand_name', String(body.brand_name).trim().slice(0, 40) || 'D4RKST3R');
|
||||
|
||||
Reference in New Issue
Block a user