Files
d4rkbot/src/templates.js
T
D4rkst3randClaude Opus 5 b3758f1f76
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Screenshots zu Ideen, und der Bot legt die Forum-Tags selbst an
Zwei Fragen, zwei Antworten — eine davon ein Nein mit Umweg.

## Bilder im Eingabefenster: geht nicht

Discord-Modals koennen nur Textfelder. Kein Datei-Anhang, keine Umgehung — das
ist eine Grenze der Plattform, nicht unseres Codes.

Der Umweg ist besser als er klingt: Wer nach dem Einreichen einen Screenshot in
den Thread haengt, sieht ihn auf der Ideen-Seite. Die Bestaetigung nach dem
Absenden sagt das jetzt auch. Damit kann man auch nachtraeglich noch Bilder
dazulegen, was im Eingabefenster gar nicht ginge.

## Warum nicht einfach der Discord-Link

Discord haengt seit einiger Zeit eine Signatur an die Anhang-Links
(?ex=&is=&hm=), die nach etwa einem Tag ablaeuft. Ein gespeicherter Link zeigt
morgen ein kaputtes Bild. Deshalb laden Devlogs und Galerie seit dem ersten Tag
herunter — im Kopf von devlog-archive.js steht der Grund. Wuensche machen es
jetzt genauso, ausgeliefert unter /wish-assets/.

Beim Loeschen eines Beitrags oder eines ganzen Wunsches gehen die Dateien mit;
sonst waechst der Ordner mit Bildern, auf die nichts mehr zeigt.

Das Herunterladen stand vorher zweimal fast wortgleich im Code. Mit den
Wuenschen waeren es drei Kopien geworden — also einmal nach bot/bilder.js, und
Devlogs und Galerie ziehen mit um. Ein Fehler steckt jetzt an einer Stelle
statt an dreien.

## Forum-Tags per Knopf

Bisher musstest du sie von Hand anlegen. Jetzt macht es das Panel: die Bereiche
von dort plus die fuenf Staende, mit ihren Emojis. Vorhandene bleiben, auch
fremde — die Liste wird ergaenzt und nicht ersetzt. Discords Grenze von 20 Tags
wird eingehalten, was nicht mehr reinpasst, wird gemeldet statt still
verschluckt.

Geprueft: forumTagsAnlegen gegen eine Kanal-Attrappe — leeres Forum, teilweise
vorhandene, Gross/Kleinschreibung, fremde Tags, nichts zu tun (dann wird auch
nicht geschrieben), volles Forum, kein Forum, kein Kanal. Im Browser die
Ideen-Seite mit Bildern in einem Beitrag, einem Beitrag nur aus Bild ohne Text,
und der Lightbox darueber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 00:17:01 +02:00

291 lines
12 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})!\n'
+ 'Screenshot dazu? Häng ihn einfach in den Thread — er landet mit auf der Ideen-Seite.',
},
{
id: 'wish.panel_title', group: 'sonstiges', module: 'voting',
name: 'Ideen-Aufruf — Überschrift',
vars: [],
default: '💡 Deine Idee für EcoGame',
},
{
id: 'wish.panel_text', group: 'sonstiges', module: 'voting',
name: 'Ideen-Aufruf — Text', long: true,
vars: [],
default:
'Dir fehlt etwas, oder du hättest gern, dass etwas anders läuft?\n\n' +
'Such dir unten den passenden Bereich, schreib deine Idee auf — sie landet als '
+ 'Post hier im Kanal. Mit 👍 stimmt die Community ab, und im Thread darunter '
+ 'wird darüber geredet. Auf der Roadmap siehst du, was daraus geworden ist.',
},
{
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' +
'Klick auf den Button und füll das kurze Formular aus. Wir schauen es uns an und ' +
'melden uns per DM — bei einer Zusage bekommst du Rolle und Zugang.',
},
];
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()),
};
});
}