Files
d4rkbot/src/templates.js
T
D4rkst3randClaude Fable 5 dadd75d452
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Texte: 27 Bot-Nachrichten im Panel bearbeitbar
Bisher standen Begrüßung, Level-Ansage, Ticket-Texte und alle
Bestätigungs-Nachrichten fest im Code. Wer den Ton ändern wollte, musste
die Quelldateien anfassen und neu deployen — für alle außer mir also gar
nicht.

src/templates.js hält jetzt alle nach außen gehenden Texte an einer
Stelle: Standardtext, Platzhalter mit Erklärung und die Zuordnung zum
Modul. Der neue Setup-Tab „Texte" zeigt sie als Karten mit Textfeld;
die Bausteine darunter fügen sich per Klick an der Cursor-Position ein.

Der Standard bleibt im Code, die Datenbank speichert nur echte
Abweichungen. Ein leeres Feld heißt deshalb „wieder Standard" — und
angepasste Vorlagen sind an einer Marke erkennbar. Vorlagen zu
ausgeschalteten Modulen sind als solche gekennzeichnet, damit niemand
einen Text feilt, den gerade nichts sendet.

Unbekannte Platzhalter bleiben stehen statt still zu verschwinden, damit
ein Tippfehler in der Nachricht sichtbar wird.

Die Route heißt /api/texts, nicht /api/templates — letzteres sind seit je
die gespeicherten Composer-Nachrichten.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 16:06:27 +02:00

274 lines
11 KiB
JavaScript

// Text-Vorlagen: eine Stelle, an der alle Nachrichten stehen, die der Bot
// nach außen schreibt — Begrüßungen, Bestätigungen, Direktnachrichten.
//
// Jede Vorlage hat einen Standardtext. Wird sie im Webinterface geändert,
// landet die Fassung als Einstellung `tpl_<id>` in der Datenbank; ein leerer
// Wert bedeutet „wieder der Standard". So bleibt der Code die Quelle für den
// Standard und die Datenbank enthält nur echte Abweichungen.
//
// Platzhalter sind `{name}`. Unbekannte Namen bleiben unverändert stehen,
// damit ein Tippfehler in der Nachricht sichtbar wird statt still zu
// verschwinden.
import { getSetting, setSetting } from './db.js';
export const TEMPLATE_GROUPS = [
{ id: 'willkommen', label: 'Ankommen & Community' },
{ id: 'support', label: 'Support & Bewerbung' },
{ id: 'spass', label: 'Verlosung, Geburtstag & Level' },
{ id: 'sonstiges', label: 'Sonstiges' },
];
/** Platzhalter, die es in fast jeder Vorlage gibt */
const WHO = [
{ key: 'user', desc: 'Anzeigename' },
{ key: 'mention', desc: 'Erwähnung (@Name)' },
];
export const TEMPLATES = [
/* ── Ankommen & Community ─────────────────────────── */
{
id: 'welcome.title', group: 'willkommen', module: 'welcome',
name: 'Willkommen — Überschrift',
vars: [...WHO, { key: 'server', desc: 'Servername' }, { key: 'count', desc: 'Mitglieder-Nummer' }],
default: '👋 Willkommen, {user}!',
},
{
id: 'welcome.text', group: 'willkommen', module: 'welcome',
name: 'Willkommen — Text', long: true,
vars: [...WHO, { key: 'server', desc: 'Servername' }, { key: 'count', desc: 'Mitglieder-Nummer' }, { key: 'url', desc: 'Adresse der Webseite' }],
default:
'Schön, dass du da bist — du bist Mitglied **#{count}**.\n\n' +
'📔 Devlogs & Roadmap: {url}\n' +
'🧪 Playtester werden? Schau nach dem Bewerbungs-Post!\n' +
'🐛 Bug gefunden? Einfach `/bug` benutzen.',
},
{
id: 'playtester.joined', group: 'willkommen', module: 'playtester',
name: 'Playtester — angemeldet', long: true,
vars: WHO,
default: '🧪 **Willkommen im Playtest-Team!** Du stehst auf der Liste — sobald es losgeht, meldet sich der Bot per DM.',
},
{
id: 'playtester.left', group: 'willkommen', module: 'playtester',
name: 'Playtester — abgemeldet',
vars: WHO,
default: '🚪 Von der Playtester-Liste **abgemeldet**.',
},
{
id: 'tempvoice.title', group: 'willkommen', module: 'temp_voice',
name: 'Sprachkanal — Überschrift',
vars: WHO,
default: '🔊 Dein Kanal',
},
{
id: 'tempvoice.text', group: 'willkommen', module: 'temp_voice',
name: 'Sprachkanal — Text', long: true,
vars: WHO,
default: '{mention}, das hier ist dein temporärer Voice-Kanal — er verschwindet automatisch, wenn alle raus sind. Steuern kannst du ihn hier:',
},
/* ── Support & Bewerbung ──────────────────────────── */
{
id: 'ticket.panel_title', group: 'support', module: 'tickets',
name: 'Ticket-Post — Überschrift',
vars: [],
default: '🎫 Support-Ticket',
},
{
id: 'ticket.panel_text', group: 'support', module: 'tickets',
name: 'Ticket-Post — Text', long: true,
vars: [],
default:
'Du hast eine Frage, ein Problem oder ein privates Anliegen?\n\n' +
'Klick auf den Button — es öffnet sich ein **privater Thread**, den nur du und das Team sehen können.',
},
{
id: 'ticket.opened', group: 'support', module: 'tickets',
name: 'Ticket — Begrüßung im Thread', long: true,
vars: WHO,
default:
'Willkommen in deinem Ticket! Beschreib dein Anliegen — das Team meldet sich.\n' +
'Wenn alles geklärt ist, kann das Ticket hier geschlossen werden.',
},
{
id: 'ticket.closed_dm', group: 'support', module: 'tickets',
name: 'Ticket — Direktnachricht beim Schließen', long: true,
vars: [{ key: 'ticket', desc: 'Name des Tickets' }, { key: 'closedBy', desc: 'wer geschlossen hat' }],
default: '🔒 Dein Ticket **{ticket}** wurde geschlossen — hier dein Transcript. Danke für deine Anfrage!',
},
{
id: 'modmail.thread_intro', group: 'support', module: 'modmail',
name: 'Modmail — Kopf des Staff-Threads', long: true,
vars: WHO,
default:
'Neue Modmail-Konversation mit **{user}** ({mention}).\n' +
'Antworten in diesem Thread gehen als DM an den User.',
},
{
id: 'application.submitted', group: 'support', module: 'applications',
name: 'Bewerbung — Bestätigung', long: true,
vars: [...WHO, { key: 'form', desc: 'Name des Formulars' }],
default: '✅ Bewerbung eingereicht — danke! Du bekommst eine DM, sobald sie geprüft wurde.',
},
{
id: 'application.approved', group: 'support', module: 'applications',
name: 'Bewerbung — angenommen', long: true,
vars: [...WHO, { key: 'form', desc: 'Name des Formulars' }],
default: '✅ Deine Bewerbung **„{form}"** wurde angenommen — willkommen! 🎉',
},
{
id: 'application.denied', group: 'support', module: 'applications',
name: 'Bewerbung — abgelehnt', long: true,
vars: [...WHO, { key: 'form', desc: 'Name des Formulars' }],
default: '❌ Deine Bewerbung **„{form}"** wurde leider abgelehnt.',
},
/* ── Verlosung, Geburtstag & Level ────────────────── */
{
id: 'giveaway.winner', group: 'spass', module: 'giveaways',
name: 'Verlosung — Gewinner', long: true,
vars: [
{ key: 'prize', desc: 'Gewinn' },
{ key: 'winners', desc: 'Erwähnungen der Gewinner' },
{ key: 'entries', desc: 'Anzahl Teilnahmen' },
],
default: '🎉 **{prize}** geht an: {winners} — Glückwunsch! 🏆',
},
{
id: 'giveaway.empty', group: 'spass', module: 'giveaways',
name: 'Verlosung — keine Teilnahmen',
vars: [{ key: 'prize', desc: 'Gewinn' }],
default: 'Keine Teilnahmen — niemand hat gewonnen. 😢',
},
{
id: 'giveaway.reroll', group: 'spass', module: 'giveaways',
name: 'Verlosung — neu ausgelost', long: true,
vars: [{ key: 'prize', desc: 'Gewinn' }, { key: 'winners', desc: 'Erwähnung des Gewinners' }],
default: '🔁 Neu ausgelost: **{prize}** geht jetzt an {winners} — Glückwunsch! 🏆',
},
{
id: 'birthday.title', group: 'spass', module: 'birthdays',
name: 'Geburtstag — Überschrift',
vars: [{ key: 'count', desc: 'Anzahl Geburtstagskinder' }],
default: '🎂 Alles Gute zum Geburtstag!',
},
{
id: 'birthday.title_multi', group: 'spass', module: 'birthdays',
name: 'Geburtstag — Überschrift (mehrere)',
vars: [{ key: 'count', desc: 'Anzahl Geburtstagskinder' }],
default: '🎂 Heute wird gleich mehrfach gefeiert!',
},
{
id: 'birthday.text', group: 'spass', module: 'birthdays',
name: 'Geburtstag — Text', long: true,
vars: [{ key: 'mentions', desc: 'Erwähnungen' }, { key: 'count', desc: 'Anzahl' }],
default: '{mentions} hat heute Geburtstag — lasst mal ordentlich 🎉 da!',
},
{
id: 'birthday.text_multi', group: 'spass', module: 'birthdays',
name: 'Geburtstag — Text (mehrere)', long: true,
vars: [{ key: 'mentions', desc: 'Erwähnungen' }, { key: 'count', desc: 'Anzahl' }],
default: '{mentions} haben heute Geburtstag — lasst mal ordentlich 🎉 da!',
},
{
id: 'level.up', group: 'spass', module: 'levels',
name: 'Level-Aufstieg', long: true,
vars: [...WHO, { key: 'level', desc: 'neues Level' }, { key: 'xp', desc: 'Gesamt-XP' }],
default: '🎉 **{user}** ist jetzt **Level {level}**!',
},
/* ── Sonstiges ────────────────────────────────────── */
{
id: 'reminder.dm', group: 'sonstiges', module: 'reminders',
name: 'Erinnerung', long: true,
vars: [...WHO, { key: 'text', desc: 'der erinnerte Text' }],
default: '⏰ **Erinnerung:** {text}',
},
{
id: 'wish.submitted', group: 'sonstiges', module: 'voting',
name: 'Feature-Wunsch — Bestätigung', long: true,
vars: [...WHO, { key: 'url', desc: 'Link zum Voting-Post' }],
default: '✅ Wunsch eingereicht — [zur Abstimmung]({url})!',
},
{
id: 'bug.submitted', group: 'sonstiges', module: 'bug_reports',
name: 'Fehlermeldung — Bestätigung', long: true,
vars: [
...WHO,
{ key: 'number', desc: 'Nummer des Issues' },
{ key: 'url', desc: 'Link zum Issue' },
{ key: 'title', desc: 'Titel der Meldung' },
],
default: '✅ Danke! Dein Bug ist erfasst als [#{number}]({url}) — du bekommst eine DM, sobald er behoben ist.',
},
{
id: 'playtester.panel_title', group: 'sonstiges', module: 'playtester',
name: 'Playtester-Post — Überschrift',
vars: [],
default: '🧪 Playtester werden',
},
{
id: 'playtester.panel_text', group: 'sonstiges', module: 'playtester',
name: 'Playtester-Post — Text', long: true,
vars: [],
default:
'Du willst **EcoGame** vor allen anderen spielen und mit deinem Feedback die Entwicklung mitgestalten?\n\n' +
'Ein Klick auf den Button setzt dich auf die Playtester-Liste — sobald die Alpha startet, ' +
'bekommst du deinen Zugang direkt per DM. Abmelden geht jederzeit mit einem zweiten Klick.',
},
];
const byId = new Map(TEMPLATES.map((t) => [t.id, t]));
/** Standardtext einer Vorlage */
export function templateDefault(id) {
return byId.get(id)?.default ?? '';
}
/** Aktueller Rohtext — eigene Fassung, sonst der Standard */
export function getTemplate(id) {
const custom = getSetting(`tpl_${id}`);
return custom?.trim() ? custom : templateDefault(id);
}
/** Eigene Fassung setzen. Leerer Text stellt den Standard wieder her. */
export function setTemplate(id, text) {
if (!byId.has(id)) return false;
setSetting(`tpl_${id}`, String(text ?? '').trim());
return true;
}
/** Platzhalter ersetzen — Unbekanntes bleibt stehen */
export function fillPlaceholders(text, vars = {}) {
return String(text).replace(/\{(\w+)\}/g, (match, key) =>
Object.hasOwn(vars, key) ? String(vars[key] ?? '') : match
);
}
/**
* Vorlage rendern.
* @param {string} id
* @param {Record<string, unknown>} vars
*/
export function renderTemplate(id, vars = {}) {
return fillPlaceholders(getTemplate(id), vars);
}
/** Alle Vorlagen mit Zustand — fürs Webinterface */
export function templateStates() {
return TEMPLATES.map((t) => {
const custom = getSetting(`tpl_${t.id}`);
return {
id: t.id,
name: t.name,
group: t.group,
module: t.module ?? null,
long: Boolean(t.long),
vars: t.vars ?? [],
default: t.default,
text: custom?.trim() ? custom : t.default,
customized: Boolean(custom?.trim()),
};
});
}