Die Status-Seite listet Module, denen ein Kanal oder eine Rolle fehlt. Bisher hieß das: rüber zu Discord, Kanal anlegen, Rechte setzen, zurück ins Panel, auswählen, speichern. Jetzt steht daneben ein Knopf, der genau das in einem Schritt macht — inklusive Eintragen der Einstellung. Die Vorlagen hängen bei den Modulen (modules.js): Name, Thema und ob der Kanal privat sein soll. Private Kanäle bekommen @everyone entzogen und Bot plus alle Rollen mit Moderationsrecht eingetragen — sonst legt man einen Modmail-Kanal an, den das Team nicht sieht. Gibt es Kanal oder Rolle schon (bei Rollen unabhängig von Groß- und Kleinschreibung), werden sie verknüpft statt doppelt angelegt. Beim Temp-Voice-Hub entsteht ein Sprachkanal statt eines Textkanals. Fehlen dem Bot die Rechte, steht das im Panel im Klartext („Dem Bot fehlt das Recht Kanäle verwalten") statt einer Discord-Fehlernummer. Bewusst nur Anlegen: kein Löschen, kein Umbenennen. Ein Fehlklick beim Anlegen kostet einen überflüssigen Kanal, einer beim Löschen dessen Verlauf. Umbenennen wäre zusätzlich eine Falle, weil Discord es auf zwei Änderungen pro zehn Minuten und Kanal begrenzt. moduleMissing liefert dafür Objekte statt Beschriftungen, damit das Panel weiß, was sich anlegen lässt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
280 lines
13 KiB
JavaScript
280 lines
13 KiB
JavaScript
// Modul-Register: eine Stelle, an der alle Funktionen des Bots stehen.
|
|
//
|
|
// Jedes Modul hat einen Schalter im Webinterface. Damit nichts kaputtgeht,
|
|
// nutzen Funktionen mit einem bereits vorhandenen Schalter (etwa das
|
|
// Level-System) weiterhin denselben Einstellungs-Namen — es entsteht also
|
|
// keine zweite Wahrheit.
|
|
//
|
|
// `requires` beschreibt, was gesetzt sein muss, damit ein Modul wirklich
|
|
// arbeiten kann. Fehlt etwas, zeigt das Panel „noch nicht einsatzbereit"
|
|
// statt das Modul stillschweigend nichts tun zu lassen.
|
|
import { getSetting, setSetting } from './db.js';
|
|
|
|
export const MODULE_GROUPS = [
|
|
{ id: 'inhalte', label: 'Inhalte & Feeds' },
|
|
{ id: 'community', label: 'Community' },
|
|
{ id: 'moderation', label: 'Moderation & Support' },
|
|
{ id: 'server', label: 'Server & Technik' },
|
|
];
|
|
|
|
export const MODULES = [
|
|
/* ── Inhalte & Feeds ──────────────────────────── */
|
|
{
|
|
id: 'devlogs', group: 'inhalte', name: 'Devlogs',
|
|
desc: 'Entwicklungs-Berichte als gebrandetes Embed posten und durchsuchbar archivieren.',
|
|
tab: 'feeds', default: true,
|
|
requires: [{ setting: 'devlog_channel_id', label: 'Devlog-Kanal', create: { name: 'dev-log', topic: 'Entwicklungs-Updates vom Bot' } }],
|
|
},
|
|
{
|
|
id: 'commit_feed', group: 'inhalte', name: 'Commit-Feed',
|
|
desc: 'Pushes aus Gitea landen als Embed im Kanal. Archiviert wird unabhängig davon immer.',
|
|
tab: 'feeds', setting: 'commit_feed_enabled', defaultOn: true,
|
|
requires: [{ setting: 'commit_channel_id', label: 'Commit-Kanal', create: { name: 'commits', privat: true, topic: 'Pushes aus Gitea' } }],
|
|
},
|
|
{
|
|
id: 'releases', group: 'inhalte', name: 'Release-Ankündigungen',
|
|
desc: 'Neue Releases werden angekündigt und landen im öffentlichen Changelog.',
|
|
tab: 'feeds', default: true,
|
|
requires: [{ setting: 'release_channel_id', label: 'Release-Kanal', create: { name: 'releases', topic: 'Neue Versionen' } }],
|
|
},
|
|
{
|
|
id: 'weekly_recap', group: 'inhalte', name: 'Wochen-Rückblick',
|
|
desc: 'Sonntags um 20 Uhr eine Zusammenfassung der Woche.',
|
|
tab: 'feeds', setting: 'weekly_recap_enabled', defaultOn: true,
|
|
requires: [{ setting: 'devlog_channel_id', label: 'Devlog-Kanal', create: { name: 'dev-log', topic: 'Entwicklungs-Updates vom Bot' } }],
|
|
},
|
|
{
|
|
id: 'devlog_threads', group: 'inhalte', name: 'Devlog-Threads',
|
|
desc: 'Unter jedem Devlog entsteht ein Diskussions-Thread.',
|
|
tab: 'feeds', setting: 'devlog_threads_enabled', defaultOn: true,
|
|
},
|
|
{
|
|
id: 'social', group: 'inhalte', name: 'Twitch & YouTube',
|
|
desc: 'Meldet Livestreams und neue Videos.',
|
|
tab: 'system', default: true,
|
|
requires: [{ setting: 'social_announce_channel_id', label: 'Social-Kanal', create: { name: 'streams', topic: 'Twitch- und YouTube-Meldungen' } }],
|
|
},
|
|
|
|
/* ── Community ────────────────────────────────── */
|
|
{
|
|
id: 'levels', group: 'community', name: 'Level-System',
|
|
desc: 'XP fürs Mitreden, Rollen-Belohnungen und öffentliche Bestenliste.',
|
|
tab: 'community', setting: 'levels_enabled', defaultOn: false,
|
|
},
|
|
{
|
|
id: 'starboard', group: 'community', name: 'Starboard',
|
|
desc: 'Nachrichten mit genug Sternen landen in einem Best-of-Kanal.',
|
|
tab: 'community', default: true,
|
|
requires: [{ setting: 'starboard_channel_id', label: 'Starboard-Kanal', create: { name: 'best-of', topic: 'Nachrichten mit genug Sternen' } }],
|
|
},
|
|
{
|
|
id: 'gallery', group: 'community', name: 'Screenshot-Galerie',
|
|
desc: 'Bilder aus einem Kanal erscheinen in der öffentlichen Galerie.',
|
|
tab: 'community', default: true,
|
|
requires: [{ setting: 'screenshot_channel_id', label: 'Screenshot-Kanal', create: { name: 'screenshots', topic: 'Bilder landen in der Galerie' } }],
|
|
},
|
|
{
|
|
id: 'voting', group: 'community', name: 'Feature-Wünsche',
|
|
desc: 'Wünsche einreichen und abstimmen — im Discord und auf der Roadmap.',
|
|
tab: 'community', default: true,
|
|
requires: [{ setting: 'voting_channel_id', label: 'Voting-Kanal', create: { name: 'ideen', topic: 'Wuensche und Abstimmungen' } }],
|
|
},
|
|
{
|
|
id: 'giveaways', group: 'community', name: 'Verlosungen',
|
|
desc: 'Teilnahme per Knopf, automatische Ziehung, Neuauslosung möglich.',
|
|
tab: 'community', default: true,
|
|
},
|
|
{
|
|
id: 'birthdays', group: 'community', name: 'Geburtstage',
|
|
desc: 'Gratulation am großen Tag, optional mit Tages-Rolle.',
|
|
tab: 'community', default: true,
|
|
requires: [{ setting: 'birthday_channel_id', label: 'Geburtstags-Kanal', create: { name: 'geburtstage', topic: 'Der Bot gratuliert hier' } }],
|
|
},
|
|
{
|
|
id: 'events', group: 'community', name: 'Event-Ankündigungen',
|
|
desc: 'Neue Discord-Events werden angekündigt und erscheinen auf der Events-Seite.',
|
|
tab: 'system', default: true,
|
|
requires: [{ setting: 'events_announce_channel_id', label: 'Event-Kanal', create: { name: 'events', topic: 'Playtests, Streams, Runden' } }],
|
|
},
|
|
{
|
|
id: 'playtester', group: 'community', name: 'Playtester-Programm',
|
|
desc: 'Bewerbungs-Knopf, Rolle und Teilnehmerliste.',
|
|
tab: 'community', default: true,
|
|
requires: [{ setting: 'playtester_role_id', label: 'Playtester-Rolle', create: { rolle: true, name: 'Playtester', farbe: '#23a55a' } }],
|
|
},
|
|
{
|
|
id: 'alpha_keys', group: 'community', name: 'Alpha-Keys',
|
|
desc: 'Schlüssel-Pool mit Verteilung per Direktnachricht.',
|
|
tab: 'community', default: true,
|
|
},
|
|
{
|
|
id: 'role_menus', group: 'community', name: 'Rollen-Menüs',
|
|
desc: 'Selbstbedienungs-Rollen per Knopf — im Discord und im Profil.',
|
|
tab: 'rollen', default: true,
|
|
},
|
|
{
|
|
id: 'applications', group: 'community', name: 'Bewerbungen',
|
|
desc: 'Formulare als Eingabefenster, Prüfung mit Annehmen/Ablehnen.',
|
|
tab: 'bewerbungen', default: true,
|
|
},
|
|
|
|
/* ── Moderation & Support ─────────────────────── */
|
|
{
|
|
id: 'moderation', group: 'moderation', name: 'Moderation',
|
|
desc: 'Verwarnen, Auszeiten, Aufräumen — mit Verlauf und Protokoll.',
|
|
tab: 'support', default: true,
|
|
},
|
|
{
|
|
id: 'modlog', group: 'moderation', name: 'Protokoll',
|
|
desc: 'Gelöschte und bearbeitete Nachrichten, Beitritte, Namens- und Rollenwechsel.',
|
|
tab: 'support', default: true,
|
|
requires: [{ setting: 'modlog_channel_id', label: 'Protokoll-Kanal', create: { name: 'protokoll', privat: true, topic: 'Bearbeitet, geloescht, beigetreten' } }],
|
|
},
|
|
{
|
|
id: 'modmail', group: 'moderation', name: 'Modmail',
|
|
desc: 'Direktnachrichten an den Bot landen als Thread beim Team.',
|
|
tab: 'support', default: true,
|
|
requires: [{ setting: 'modmail_channel_id', label: 'Modmail-Kanal', create: { name: 'modmail', privat: true, topic: 'Direktnachrichten an den Bot' } }],
|
|
},
|
|
{
|
|
id: 'tickets', group: 'moderation', name: 'Tickets',
|
|
desc: 'Private Threads per Knopf, beim Schließen ein Gesprächsprotokoll.',
|
|
tab: 'support', default: true,
|
|
requires: [{ setting: 'ticket_channel_id', label: 'Ticket-Kanal', create: { name: 'support', topic: 'Tickets per Knopf' } }],
|
|
},
|
|
{
|
|
id: 'welcome', group: 'moderation', name: 'Willkommens-Karten',
|
|
desc: 'Begrüßung neuer Mitglieder mit gerendertem Bild.',
|
|
tab: 'support', default: true,
|
|
requires: [{ setting: 'welcome_channel_id', label: 'Willkommens-Kanal', create: { name: 'willkommen', topic: 'Begruessung neuer Mitglieder' } }],
|
|
},
|
|
{
|
|
id: 'autorole', group: 'moderation', name: 'Auto-Rolle',
|
|
desc: 'Neue Mitglieder bekommen automatisch eine Startrolle.',
|
|
tab: 'community', default: true,
|
|
requires: [{ setting: 'autorole_id', label: 'Auto-Rolle', create: { rolle: true, name: 'Member' } }],
|
|
},
|
|
{
|
|
id: 'sticky_roles', group: 'moderation', name: 'Rollen merken',
|
|
desc: 'Wer den Server verlässt und zurückkommt, bekommt seine Rollen wieder.',
|
|
tab: 'community', setting: 'sticky_roles_enabled', defaultOn: false,
|
|
},
|
|
{
|
|
id: 'bug_reports', group: 'moderation', name: 'Fehlermeldungen',
|
|
desc: 'Meldungen werden zu Issues im Repository, mit Rückmeldung beim Schließen.',
|
|
tab: 'system', default: true,
|
|
requires: [{ setting: 'gitea_api_token', label: 'Gitea-Token' }],
|
|
},
|
|
|
|
/* ── Server & Technik ─────────────────────────── */
|
|
{
|
|
id: 'server_monitor', group: 'server', name: 'Server-Monitor',
|
|
desc: 'Live-Status der Game-Server mit Verlauf und Ausfall-Alarm.',
|
|
tab: 'server', default: true,
|
|
},
|
|
{
|
|
id: 'watchdog', group: 'server', name: 'Erreichbarkeits-Wächter',
|
|
desc: 'Prüft hinterlegte Adressen und meldet Ausfälle per Direktnachricht.',
|
|
tab: 'system', default: true,
|
|
requires: [{ setting: 'watchdog_urls', label: 'Adressen' }],
|
|
},
|
|
{
|
|
id: 'temp_voice', group: 'server', name: 'Temporäre Sprachkanäle',
|
|
desc: 'Beitritt zum Hub erzeugt einen eigenen Kanal mit Bedienfeld.',
|
|
tab: 'system', default: true,
|
|
requires: [{ setting: 'tempvoice_channel_id', label: 'Hub-Kanal', create: { name: 'Eigenen Kanal erstellen', sprache: true } }],
|
|
},
|
|
{
|
|
id: 'triggers', group: 'server', name: 'Auto-Antworten',
|
|
desc: 'Der Bot antwortet auf hinterlegte Schlüsselwörter.',
|
|
tab: 'composer', default: true,
|
|
},
|
|
{
|
|
id: 'reminders', group: 'server', name: 'Erinnerungen',
|
|
desc: 'Mitglieder lassen sich per Direktnachricht erinnern.',
|
|
tab: 'system', default: true,
|
|
},
|
|
{
|
|
id: 'scheduled_posts', group: 'server', name: 'Geplante Beiträge',
|
|
desc: 'Nachrichten zu festen Zeiten, einmalig oder wiederkehrend.',
|
|
tab: 'composer', default: true,
|
|
},
|
|
{
|
|
id: 'tags', group: 'server', name: 'Textbausteine',
|
|
desc: 'Gespeicherte Texte per Befehl abrufen.',
|
|
tab: 'composer', default: true,
|
|
},
|
|
{
|
|
id: 'backups', group: 'server', name: 'Datenbank-Sicherung',
|
|
desc: 'Nächtliche Sicherung mit Aufbewahrung über 14 Tage.',
|
|
tab: 'system', setting: 'backup_enabled', defaultOn: true,
|
|
},
|
|
{
|
|
id: 'repo_backups', group: 'server', name: 'Repository-Sicherung',
|
|
desc: 'Alle Gitea-Repositories als Bundle sichern.',
|
|
tab: 'system', setting: 'repo_backup_enabled', defaultOn: false,
|
|
},
|
|
{
|
|
id: 'member_gate', group: 'server', name: 'Login nur für Mitglieder',
|
|
desc: 'Nur wer auf dem Discord-Server ist, kann sich im Web anmelden.',
|
|
tab: 'system', setting: 'member_gate_enabled', defaultOn: true,
|
|
},
|
|
];
|
|
|
|
/** Einstellungs-Name des Schalters für ein Modul */
|
|
function toggleKey(mod) {
|
|
return mod.setting ?? `module_${mod.id}`;
|
|
}
|
|
|
|
/** Läuft dieses Modul? (unabhängig davon, ob alles Nötige konfiguriert ist) */
|
|
export function moduleEnabled(id) {
|
|
const mod = MODULES.find((m) => m.id === id);
|
|
if (!mod) return true; // unbekannt → nicht blockieren
|
|
const value = getSetting(toggleKey(mod));
|
|
if (value === '1') return true;
|
|
if (value === '0') return false;
|
|
return mod.setting ? Boolean(mod.defaultOn) : mod.default !== false;
|
|
}
|
|
|
|
/** Schalter setzen */
|
|
export function setModuleEnabled(id, on) {
|
|
const mod = MODULES.find((m) => m.id === id);
|
|
if (!mod) return false;
|
|
setSetting(toggleKey(mod), on ? '1' : '0');
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Was fehlt diesem Modul noch? (leere Liste = einsatzbereit)
|
|
* Liefert Objekte statt nur Beschriftungen, damit das Panel weiß, was es
|
|
* anlegen könnte: `{ setting, label, create }`.
|
|
*/
|
|
export function moduleMissing(mod) {
|
|
return (mod.requires ?? [])
|
|
.filter((r) => !String(getSetting(r.setting) ?? '').trim())
|
|
.map((r) => ({ setting: r.setting, label: r.label, create: r.create ?? null }));
|
|
}
|
|
|
|
/** Bauanleitung zu einer Einstellung — null, wenn sie sich nicht anlegen lässt */
|
|
export function creationPlan(setting) {
|
|
for (const mod of MODULES) {
|
|
for (const req of mod.requires ?? []) {
|
|
if (req.setting === setting && req.create) return { ...req.create, label: req.label };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Alle Module mit Zustand — für das Webinterface */
|
|
export function moduleStates() {
|
|
return MODULES.map((mod) => ({
|
|
id: mod.id,
|
|
name: mod.name,
|
|
desc: mod.desc,
|
|
group: mod.group,
|
|
tab: mod.tab ?? null,
|
|
enabled: moduleEnabled(mod.id),
|
|
missing: moduleMissing(mod),
|
|
}));
|
|
}
|