Files
d4rkbot/src/tuning.js
T
D4rkst3randClaude Fable 5 aef708c405
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Raid-Schutz: erkennt Beitritts-Wellen und sperrt voruebergehend
Neues Modul anti_raid, standardmaessig AUS. Zaehlt Beitritte in einem
Zeitfenster; reisst es, geht der Server dicht: Verifizierung auf Hoch,
Einladungen gesperrt, DM an den Owner und Eintrag ins Protokoll.

Bewusst zurueckhaltend — es wird niemand gekickt oder gebannt. Bei einem
Fehlalarm, etwa nach einem Stream-Shoutout, waere ein Massenkick schlimmer
als der Raid. Beide Massnahmen sind reversibel und treffen niemanden, der
schon drin ist.

Der Sperrzustand liegt in den Einstellungen, nicht nur im Speicher: startet
der Bot mitten in einer Sperre neu, waere der Timer sonst weg und der Server
bliebe fuer immer dicht. Beim Start wird die Restzeit abgesessen oder, wenn
laengst abgelaufen, sofort aufgehoben.

Drei Werte im Panel: Schwelle (8), Fenster (20 s), Sperrdauer (15 min).

disableInvites() gibt es erst ab discord.js 14.4 und nur mit ManageGuild —
beides wird geprueft, statt im Ernstfall einen Fehler zu werfen.

Die Fenster-Logik ist als reine Funktion herausgezogen und mit neun Faellen
geprueft: langsame Beitritte loesen nicht aus, die Welle schon, der Rand des
Zeitfensters zaehlt nicht mehr mit, und nach Ruhe faengt das Fenster von vorn.

Offen: ein Befehl zum sofortigen Aufheben. Bis dahin laeuft die Sperre aus
oder man setzt die Verifizierung in Discord selbst zurueck.

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

239 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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: '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,
};
/**
* 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 {'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;
const delay = () => (unit === 'minutes' ? tuningMs.minutes(id) : tuningMs.seconds(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) };
}