Neues Modul, standardmaessig aus. Ein Mitglied klickt in Discord auf "Rolle holen", landet auf /linked-roles, bestaetigt den OAuth-Dialog — und der Bot schiebt ihm drei Kennzahlen zu, die Discord dann gegen die Rollen-Einstellung prueft: level Zahl >= eingestelltem Wert dabei_seit Datum <= "vor X Tagen", also Mitglied seit mindestens X Tagen playtester Wahrheitswert Der Bot vergibt dabei keine Rollen. Er liefert nur die Zahlen; was daraus wird, entscheidet die Rollen-Einstellung im Server. Die Daten kommen aus dem, was ohnehin da ist: Level-Tabelle, Playtester-Liste, Discords Beitrittsdatum. Eigener OAuth-Weg statt /auth/login, weil die Berechtigungen andere sind — role_connections.write hat der normale Login nicht. Die Tokens werden gespeichert, und das ist keine Bequemlichkeit: ohne Refresh-Token bliebe jede Rolle auf dem Stand des Verknuepfungs-Tages stehen, ein Level-Up kaeme nie an. Discords eigene Anleitung sagt dasselbe. Der Umfang ist eng (nur identify + role_connections.write), beim Widerruf oder Entkoppeln fliegt der Datensatz sofort raus. Steht so auch als Kommentar an der Tabelle. Abgeglichen wird alle 6 Stunden, einstellbar. Dafuer kann everyTuned jetzt auch Stunden — und wirft bei einer unbekannten Einheit, statt den Abstand still um Faktor 60 oder 3600 danebenzulegen. Beim Bauen gefunden: moduleEnabled fehlte im Import von client.js. Der Bot waere beim ClientReady mit ReferenceError gestorben. Geprueft: alle drei Kennzahlen gegen Discords Formatgrenzen (Schluessel-Regex, Laengen, gueltige Typ-Codes), Modul- und Stellwert-Register, die Stunden-Umrechnung (6 h = 21600000 ms), Speichern und Loeschen einer Verknuepfung, dazu 186 SQL-Abfragen und alle Routen auf Rechtepruefung. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
251 lines
11 KiB
JavaScript
251 lines
11 KiB
JavaScript
// Stellwerte: alle Zahlen, die vorher fest im Code standen — XP-Beträge,
|
||
// Wartezeiten, Prüf-Intervalle, Obergrenzen.
|
||
//
|
||
// `module` verbindet einen Wert mit seiner Funktion — die Modul-Seite zeigt ihn
|
||
// dann dort, wo er hingehört, statt nur in der flachen Werte-Liste. Ein Wert
|
||
// darf zu mehreren gehören (die Thread-Archivdauer gilt für Tickets UND
|
||
// Modmail); dann steht dort eine Liste.
|
||
//
|
||
// Wie bei den Modulen und Vorlagen lebt der Standard im Code und die
|
||
// Datenbank enthält nur Abweichungen. `min`/`max` sind keine Kosmetik: sie
|
||
// verhindern, dass ein Vertipper den Bot in eine Prüfschleife im
|
||
// Sekundentakt schickt oder das Level-System unbrauchbar macht.
|
||
import { getSetting, setSetting } from './db.js';
|
||
|
||
export const TUNING_GROUPS = [
|
||
{ id: 'level', label: 'Level & Aktivität' },
|
||
{ id: 'zeiten', label: 'Wann der Bot postet' },
|
||
{ id: 'pruefung', label: 'Prüf-Intervalle' },
|
||
{ id: 'grenzen', label: 'Obergrenzen' },
|
||
];
|
||
|
||
export const TUNING = [
|
||
/* ── Level & Aktivität ────────────────────────────── */
|
||
{
|
||
id: 'xp_min', module: 'levels', group: 'level', label: 'XP pro Nachricht — mindestens',
|
||
unit: 'XP', default: 15, min: 1, max: 500,
|
||
},
|
||
{
|
||
id: 'xp_max', module: 'levels', group: 'level', label: 'XP pro Nachricht — höchstens',
|
||
hint: 'Der Bot würfelt zwischen beiden Werten.',
|
||
unit: 'XP', default: 25, min: 1, max: 500,
|
||
},
|
||
{
|
||
id: 'xp_cooldown', module: 'levels', group: 'level', label: 'Wartezeit zwischen zwei XP-Gutschriften',
|
||
hint: 'Verhindert, dass Vielschreiber das Level-System leerlaufen lassen.',
|
||
unit: 'Sekunden', default: 60, min: 5, max: 3600,
|
||
},
|
||
{
|
||
id: 'trigger_cooldown', module: 'triggers', group: 'level', label: 'Wartezeit für Auto-Antworten',
|
||
hint: 'Pro Schlüsselwort und Kanal — sonst antwortet der Bot sich in Grund und Boden.',
|
||
unit: 'Sekunden', default: 30, min: 5, max: 3600,
|
||
},
|
||
|
||
/* ── Wann der Bot postet ──────────────────────────── */
|
||
{
|
||
id: 'birthday_hour', module: 'birthdays', group: 'zeiten', label: 'Geburtstags-Gratulation ab',
|
||
hint: 'Uhrzeit in Europe/Berlin. Gratuliert wird einmal am Tag.',
|
||
unit: 'Uhr', default: 9, min: 0, max: 23,
|
||
},
|
||
{
|
||
id: 'recap_weekday', module: 'weekly_recap', group: 'zeiten', label: 'Wochen-Rückblick am',
|
||
hint: '0 = Sonntag, 1 = Montag … 6 = Samstag.',
|
||
unit: 'Wochentag', default: 0, min: 0, max: 6,
|
||
},
|
||
{
|
||
id: 'recap_hour', module: 'weekly_recap', group: 'zeiten', label: 'Wochen-Rückblick ab',
|
||
unit: 'Uhr', default: 20, min: 0, max: 23,
|
||
},
|
||
|
||
/* ── Prüf-Intervalle ──────────────────────────────── */
|
||
{
|
||
id: 'monitor_interval', module: 'server_monitor', group: 'pruefung', label: 'Game-Server prüfen alle',
|
||
unit: 'Minuten', default: 2, min: 1, max: 120,
|
||
},
|
||
{
|
||
id: 'monitor_timeout', module: 'server_monitor', group: 'pruefung', label: 'Game-Server — Antwort abwarten',
|
||
unit: 'Sekunden', default: 8, min: 2, max: 60,
|
||
},
|
||
{
|
||
id: 'monitor_fails', module: 'server_monitor', group: 'pruefung', label: 'Game-Server — Alarm nach',
|
||
hint: 'Fehlversuchen in Folge. 1 meldet jeden Aussetzer, höher meldet nur echte Ausfälle.',
|
||
unit: 'Fehlversuchen', default: 2, min: 1, max: 10,
|
||
},
|
||
{
|
||
id: 'watchdog_interval', module: 'watchdog', group: 'pruefung', label: 'Adressen prüfen alle',
|
||
unit: 'Minuten', default: 2, min: 1, max: 120,
|
||
},
|
||
{
|
||
id: 'watchdog_timeout', module: 'watchdog', group: 'pruefung', label: 'Adressen — Antwort abwarten',
|
||
unit: 'Sekunden', default: 10, min: 2, max: 60,
|
||
},
|
||
{
|
||
id: 'watchdog_fails', module: 'watchdog', group: 'pruefung', label: 'Adressen — Alarm nach',
|
||
unit: 'Fehlversuchen', default: 2, min: 1, max: 10,
|
||
},
|
||
{
|
||
id: 'social_interval', module: 'social', group: 'pruefung', label: 'Twitch & YouTube prüfen alle',
|
||
hint: 'Zu kurz bringt nichts — YouTube-Feeds aktualisieren sich ohnehin nur alle paar Minuten.',
|
||
unit: 'Minuten', default: 5, min: 1, max: 180,
|
||
},
|
||
{
|
||
id: 'birthday_interval', module: 'birthdays', group: 'pruefung', label: 'Geburtstage prüfen alle',
|
||
unit: 'Minuten', default: 15, min: 1, max: 120,
|
||
},
|
||
|
||
/* ── Obergrenzen ──────────────────────────────────── */
|
||
{
|
||
id: 'raid_joins', module: 'anti_raid', group: 'grenzen', label: 'Raid-Verdacht ab',
|
||
hint: 'So viele Beitritte im Zeitfenster lösen die Sperre aus. Zu niedrig, und ein Stream-Shoutout reicht schon.',
|
||
unit: 'Beitritte', default: 8, min: 3, max: 100,
|
||
},
|
||
{
|
||
id: 'raid_window', module: 'anti_raid', group: 'grenzen', label: 'Raid — Zeitfenster',
|
||
unit: 'Sekunden', default: 20, min: 5, max: 600,
|
||
},
|
||
{
|
||
id: 'linked_sync', module: 'linked_roles', group: 'pruefung', label: 'Verknüpfte Rollen abgleichen alle',
|
||
hint: 'Discord fragt die Werte nicht selbst nach — ohne Abgleich bliebe eine Rolle auf dem Stand des Verknüpfungs-Tages.',
|
||
unit: 'Stunden', default: 6, min: 1, max: 168,
|
||
},
|
||
{
|
||
id: 'raid_lockdown', module: 'anti_raid', group: 'zeiten', label: 'Raid-Sperre dauert',
|
||
hint: 'Danach hebt der Bot sie von selbst wieder auf.',
|
||
unit: 'Minuten', default: 15, min: 1, max: 720,
|
||
},
|
||
{
|
||
id: 'ticket_transcript_max', module: 'tickets', group: 'grenzen', label: 'Nachrichten im Ticket-Protokoll',
|
||
hint: 'Ältere fallen weg. Hoch gesetzt dauert das Schließen länger.',
|
||
unit: 'Nachrichten', default: 500, min: 50, max: 2000,
|
||
},
|
||
{
|
||
id: 'gallery_max_images', module: 'gallery', group: 'grenzen', label: 'Bilder je Galerie-Beitrag',
|
||
unit: 'Bilder', default: 4, min: 1, max: 10,
|
||
},
|
||
{
|
||
id: 'commits_shown', module: 'commit_feed', group: 'grenzen', label: 'Commits je Push-Embed',
|
||
hint: 'Der Rest wird als „… und N weitere" zusammengefasst.',
|
||
unit: 'Commits', default: 10, min: 1, max: 20,
|
||
},
|
||
{
|
||
id: 'thread_archive_days', module: ['tickets', 'modmail'], group: 'grenzen', label: 'Threads archivieren nach',
|
||
hint: 'Für Ticket- und Modmail-Threads. Discord erlaubt 1, 3 oder 7 Tage.',
|
||
unit: 'Tagen', default: 7, min: 1, max: 7, choices: [1, 3, 7],
|
||
},
|
||
];
|
||
|
||
const byId = new Map(TUNING.map((t) => [t.id, t]));
|
||
|
||
/**
|
||
* Wert lesen — immer eine gültige Zahl im erlaubten Bereich.
|
||
* @param {string} id
|
||
* @returns {number}
|
||
*/
|
||
export function tuning(id) {
|
||
const def = byId.get(id);
|
||
if (!def) return 0;
|
||
// Achtung: Number('') ist 0, nicht NaN — ohne diese Prüfung würde ein
|
||
// ungesetzter Wert auf das Minimum fallen statt auf den Standard.
|
||
const stored = String(getSetting(`tune_${id}`) ?? '').trim();
|
||
if (stored === '') return def.default;
|
||
const raw = Number(stored);
|
||
if (!Number.isFinite(raw)) return def.default;
|
||
return Math.min(def.max, Math.max(def.min, Math.round(raw)));
|
||
}
|
||
|
||
/** Derselbe Wert in Millisekunden — spart die Rechnerei an jeder Aufrufstelle */
|
||
export const tuningMs = {
|
||
seconds: (id) => tuning(id) * 1000,
|
||
minutes: (id) => tuning(id) * 60 * 1000,
|
||
hours: (id) => tuning(id) * 60 * 60 * 1000,
|
||
};
|
||
|
||
/**
|
||
* Wert setzen. Leer stellt den Standard wieder her.
|
||
* Mit `dryRun` wird nur geprüft — damit ein Formular mit mehreren Werten
|
||
* entweder ganz oder gar nicht gespeichert wird.
|
||
* @returns {{ ok: true } | { ok: false, error: string }}
|
||
*/
|
||
export function setTuning(id, value, { dryRun = false } = {}) {
|
||
const def = byId.get(id);
|
||
if (!def) return { ok: false, error: `Unbekannter Wert: ${id}` };
|
||
if (value === '' || value === null || value === undefined) {
|
||
if (!dryRun) setSetting(`tune_${id}`, '');
|
||
return { ok: true };
|
||
}
|
||
const n = Number(value);
|
||
if (!Number.isFinite(n) || !Number.isInteger(n)) {
|
||
return { ok: false, error: `${def.label}: ganze Zahl erwartet` };
|
||
}
|
||
if (n < def.min || n > def.max) {
|
||
return { ok: false, error: `${def.label}: ${def.min}–${def.max} ${def.unit}` };
|
||
}
|
||
if (def.choices && !def.choices.includes(n)) {
|
||
return { ok: false, error: `${def.label}: erlaubt sind ${def.choices.join(', ')}` };
|
||
}
|
||
if (!dryRun) setSetting(`tune_${id}`, String(n));
|
||
return { ok: true };
|
||
}
|
||
|
||
/** Alle Werte mit Zustand — fürs Webinterface */
|
||
export function tuningStates() {
|
||
return TUNING.map((t) => {
|
||
const custom = getSetting(`tune_${t.id}`);
|
||
return {
|
||
id: t.id, label: t.label, hint: t.hint ?? null,
|
||
group: t.group,
|
||
// Immer als Liste nach außen — der Aufrufer muss dann nicht
|
||
// zwischen einem Wert und mehreren unterscheiden
|
||
modules: t.module ? [t.module].flat() : [],
|
||
unit: t.unit, min: t.min, max: t.max,
|
||
choices: t.choices ?? null,
|
||
default: t.default,
|
||
value: tuning(t.id),
|
||
customized: Boolean(String(custom ?? '').trim()),
|
||
};
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Wiederkehrende Aufgabe, deren Abstand aus einem Stellwert kommt. Der Wert
|
||
* wird vor jeder Runde neu gelesen — ein geändertes Intervall greift damit ab
|
||
* dem nächsten Durchlauf, ohne den Bot neu zu starten. (Ein setInterval mit
|
||
* festem Abstand könnte das nicht.)
|
||
*
|
||
* @param {string} id — Stellwert
|
||
* @param {'hours'|'minutes'|'seconds'} unit
|
||
* @param {() => unknown} task
|
||
* @param {string} tag — Kennung fürs Fehler-Log
|
||
* @returns {() => void} Abbrechen
|
||
*/
|
||
export function everyTuned(id, unit, task, tag = id) {
|
||
let timer;
|
||
// Unbekannte Einheit wäre schlimmer als ein Tippfehler: der Abstand läge
|
||
// um Faktor 60 oder 3600 daneben, ohne dass es jemand merkt.
|
||
const delay = () => {
|
||
const f = tuningMs[unit];
|
||
if (!f) throw new Error(`everyTuned(${id}): unbekannte Einheit "${unit}"`);
|
||
return f(id);
|
||
};
|
||
const run = async () => {
|
||
try {
|
||
await task();
|
||
} catch (error) {
|
||
console.error(`[${tag}]`, error);
|
||
}
|
||
timer = setTimeout(run, delay());
|
||
};
|
||
timer = setTimeout(run, delay());
|
||
return () => clearTimeout(timer);
|
||
}
|
||
|
||
/**
|
||
* XP-Spanne — sorgt dafür, dass Minimum und Maximum nicht vertauscht sind,
|
||
* falls jemand das Maximum unter das Minimum setzt.
|
||
*/
|
||
export function xpRange() {
|
||
const a = tuning('xp_min');
|
||
const b = tuning('xp_max');
|
||
return { min: Math.min(a, b), max: Math.max(a, b) };
|
||
}
|