// Effektive Laufzeit-Konfiguration: DB-Setting (Webinterface) vor Env-Variable. // Wird bei jedem Zugriff frisch aufgelöst — Änderungen greifen ohne Neustart. import { getSetting, setSetting } from './db.js'; import { config } from './config.js'; export function commitChannelId() { return getSetting('commit_channel_id') || config.commitChannelId; } export function devlogChannelId() { return getSetting('devlog_channel_id') || config.devlogChannelId; } /** Öffentliche Basis-URL (Buttons, RSS, OAuth-Redirect) */ export function publicUrl() { return (getSetting('public_url') || config.publicUrl).replace(/\/$/, ''); } /** * Community-Hub — Ziel für Devlog-Buttons, RSS, Vorschaubilder und die * geteilte Navigation. Ohne eigene Einstellung bleibt alles wie bisher. */ export function hubUrl() { return (getSetting('hub_url') || publicUrl()).replace(/\/$/, ''); } /** Bot-Produktseite mit Dashboard */ export function botUrl() { return (getSetting('bot_url') || publicUrl()).replace(/\/$/, ''); } /** Cookie-Domain für den Login über mehrere Subdomains (leer = nur diese) */ export function cookieDomain() { return getSetting('cookie_domain') || null; } /** Gitea-Basis-URL (API für /bug) */ export function giteaUrl() { return (getSetting('gitea_url') || config.giteaUrl).replace(/\/$/, ''); } /** Release-Ankündigungs-Kanal — nur per Settings-Seite, leer = Feature aus */ export function releaseChannelId() { return getSetting('release_channel_id') || null; } /** Diskussions-Thread unter jedem Devlog-Post (Default: an) */ export function devlogThreadsEnabled() { return getSetting('devlog_threads_enabled') !== '0'; } /** Playtester-Rolle (Bewerbungs-Button) — leer = Feature aus */ export function playtesterRoleId() { return getSetting('playtester_role_id') || null; } /** Starboard-Kanal — leer = Feature aus */ export function starboardChannelId() { return getSetting('starboard_channel_id') || null; } /** Ab wie vielen ⭐ landet eine Nachricht im Starboard (Default: 3) */ export function starboardThreshold() { const n = Number(getSetting('starboard_threshold')); return Number.isInteger(n) && n >= 1 ? n : 3; } /** Gitea-API-Token (für /bug) — Setting hat Vorrang vor Env */ export function giteaApiToken() { return getSetting('gitea_api_token') || config.giteaApiToken || null; } /** Guild-ID für die Slash-Command-Registrierung (greift beim nächsten Start) */ export function discordGuildId() { return getSetting('discord_guild_id') || config.discordGuildId || null; } /* ── Branding (Config → Brand-Tab) ── */ /** Brand-Name für Footer/Embeds (Default: D4RKST3R) */ export function brandName() { return getSetting('brand_name') || 'D4RKST3R'; } /** Primärfarbe als Discord-Int (Default: #f5c518) */ export function brandColor() { const hex = (getSetting('brand_color') || '#f5c518').replace('#', ''); const n = parseInt(hex, 16); return Number.isNaN(n) ? 0xf5c518 : n; } /** Sekundärfarbe (Default: #ff4d00) */ export function brandColor2() { const hex = (getSetting('brand_color2') || '#ff4d00').replace('#', ''); const n = parseInt(hex, 16); return Number.isNaN(n) ? 0xff4d00 : n; } /** Embed-Footer im Brand-Stil: "D4RKST3R // DEVLOG" */ export function brandFooter(tag) { return `${brandName()} // ${tag}`; } /** Bot-Status: Text (leer = keiner; Server-Monitor darf dann die Presence nutzen) */ export function botStatusText() { return getSetting('bot_status_text') || ''; } /** Bot-Status: Typ (playing|watching|listening|custom) */ export function botStatusType() { return getSetting('bot_status_type') || 'custom'; } /** Bot-Presence-Status: online | idle | dnd */ export function botPresenceStatus() { return getSetting('bot_presence_status') || 'online'; } /** Ankündigungs-Kanal für neue Discord-Events — leer = aus */ export function eventsAnnounceChannelId() { return getSetting('events_announce_channel_id') || null; } /** Announce-Kanal für Twitch/YouTube — leer = aus */ export function socialAnnounceChannelId() { return getSetting('social_announce_channel_id') || null; } /** YouTube-Kanal-ID (UC…) für neue-Video-Benachrichtigungen */ export function youtubeChannelId() { return getSetting('youtube_channel_id') || null; } /** Twitch-Login-Name + App-Credentials (Client-Credentials-Flow) */ export function twitchChannel() { return getSetting('twitch_channel') || null; } export function twitchClientId() { return getSetting('twitch_client_id') || null; } export function twitchClientSecret() { return getSetting('twitch_client_secret') || null; } /** "Join to Create"-Voice-Kanal — leer = Temp-Voice aus */ export function tempVoiceChannelId() { return getSetting('tempvoice_channel_id') || null; } /** Auto-Rolle für neue Member — leer = aus */ export function autoroleId() { return getSetting('autorole_id') || null; } /** Sticky-Roles: Rollen bei Rejoin wiederherstellen (Default: aus) */ export function stickyRolesEnabled() { return getSetting('sticky_roles_enabled') === '1'; } /** Level-System an/aus (Default: aus — bewusst opt-in) */ export function levelsEnabled() { return getSetting('levels_enabled') === '1'; } /** Level-Up-Nachricht im Kanal (Default: an, wenn Levels an) */ export function levelsAnnounce() { return getSetting('levels_announce') !== '0'; } /** Rollen-Belohnungen: "5=roleId, 10=roleId" → [{level, roleId}] aufsteigend */ export function levelRewards() { return (getSetting('level_rewards') ?? '') .split(',') .map((entry) => { const [lvl, roleId] = entry.split('=').map((s) => s.trim()); return Number(lvl) > 0 && roleId ? { level: Number(lvl), roleId } : null; }) .filter(Boolean) .sort((a, b) => a.level - b.level); } /** Betreiber-Angaben fürs Impressum (§ 5 DDG) — leer = Seite zeigt Hinweis */ export function legalInfo() { return { name: getSetting('legal_name') ?? '', address: getSetting('legal_address') ?? '', email: getSetting('legal_email') ?? '', extra: getSetting('legal_extra') ?? '', }; } /** Member-Gate: Web-Login nur für Discord-Member (Default: an, wenn Guild gesetzt) */ export function memberGateEnabled() { return getSetting('member_gate_enabled') !== '0'; } /** Discord-Invite-Link für die „Tritt erst dem Server bei"-Seite — leer = keiner */ export function discordInviteUrl() { return getSetting('discord_invite_url') || null; } /** Geburtstags-Kanal (morgendliche Gratulation) — leer = Feature aus */ export function birthdayChannelId() { return getSetting('birthday_channel_id') || null; } /** Geburtstags-Rolle für den Tag — leer = keine Rolle */ export function birthdayRoleId() { return getSetting('birthday_role_id') || null; } /** Kanal für Feature-Wünsche (/wunsch) — leer = Feature aus */ export function votingChannelId() { return getSetting('voting_channel_id') || null; } /** Ticket-Kanal (dort entstehen die privaten Ticket-Threads) — leer = Feature aus */ export function ticketChannelId() { return getSetting('ticket_channel_id') || null; } /** Modmail-Staff-Kanal (privat!) — leer = Feature aus */ export function modmailChannelId() { return getSetting('modmail_channel_id') || null; } /** Willkommens-Kanal — leer = Feature aus */ export function welcomeChannelId() { return getSetting('welcome_channel_id') || null; } /** * Wert einer Einstellung so, wie er tatsächlich wirkt — also inklusive der * Env-Fallbacks aus config.js. * * Ohne das meldet die Config Sachen als „fehlt", die über die Umgebung längst * gesetzt sind: Wer sein Gitea-Token per GITEA_API_TOKEN übergibt, sieht in * der Liste „Fehlermeldungen — Gitea-Token", obwohl /bug einwandfrei läuft. * * @param {string} key * @returns {string} */ export function effectiveSetting(key) { const gesetzt = String(getSetting(key) ?? '').trim(); if (gesetzt) return gesetzt; const ausDerEnv = { devlog_channel_id: config.devlogChannelId, commit_channel_id: config.commitChannelId, gitea_api_token: config.giteaApiToken, discord_guild_id: config.discordGuildId, }[key]; return String(ausDerEnv ?? '').trim(); } /** * Aussehen der Willkommens-Karte. Die Standardwerte sind der Marken-Look; * die Marken-Farben ziehen automatisch nach, solange nichts eigenes gesetzt ist. */ export function welcomeCard() { const hex = (key, fallback) => { const value = String(getSetting(key) ?? '').trim(); return /^#[0-9a-fA-F]{6}$/.test(value) ? value : fallback; }; const brand = (n) => `#${n.toString(16).padStart(6, '0')}`; return { enabled: getSetting('welcome_card_enabled') !== '0', background: hex('welcome_card_bg', '#0a0a0a'), accent: hex('welcome_card_accent', brand(brandColor())), accent2: hex('welcome_card_accent2', brand(brandColor2())), text: hex('welcome_card_text', '#e8e0d0'), kicker: (getSetting('welcome_card_kicker') ?? 'WILLKOMMEN').slice(0, 24), sub: getSetting('welcome_card_sub') ?? 'Member #{count}', image: String(getSetting('welcome_card_image') ?? '').trim(), watermark: getSetting('welcome_card_watermark') !== '0', }; } /** Mod-Log-Kanal (privat!) — leer = Feature aus */ export function modlogChannelId() { return getSetting('modlog_channel_id') || null; } /** Screenshot-Kanal für die Galerie — leer = Feature aus */ export function screenshotChannelId() { return getSetting('screenshot_channel_id') || null; } /** Ziel-Repo für /bug-Reports (Default: EcoGame) */ export function bugReportRepo() { return getSetting('bug_report_repo') || 'D4rkst3r/EcoGame'; } /** Repo, dessen Milestones die öffentliche Roadmap speisen */ export function roadmapRepo() { return getSetting('roadmap_repo') || 'D4rkst3r/EcoGame'; } /** Rolle, die bei Devlog-Posts gepingt wird — leer = kein Ping */ export function devlogPingRoleId() { return getSetting('devlog_ping_role_id') || null; } /** Commit-Feed global an/aus (Default: an). Aus = weiter archivieren, nur nicht posten. */ export function commitFeedEnabled() { return getSetting('commit_feed_enabled') !== '0'; } /** Kommagetrennte Liste in Array übersetzen ('' → leer) */ function csv(value) { return (value ?? '') .split(',') .map((s) => s.trim()) .filter(Boolean); } /** Branch-Filter: leer = alle Branches werden gepostet */ export function branchAllowed(branch) { const allowed = csv(getSetting('commit_branch_filter')); return allowed.length === 0 || allowed.includes(branch); } /** Ignorierte Repos (z. B. "D4rkst3r/ecobot") — werden komplett übersprungen */ export function repoIgnored(repo) { return csv(getSetting('ignored_repos')).some( (r) => r.toLowerCase() === repo.toLowerCase() ); } /** * Welche Protokoll-Meldungen überhaupt geschrieben werden. * * Vorher hing alles an einem einzigen Modulschalter: gelöschte und bearbeitete * Nachrichten, Beitritte, Austritte, Namens- und Rollenwechsel — entweder alles * oder nichts. Wer nur die Löschungen sehen wollte, bekam auch jeden * Rollenmenü-Klick. * * `!== '0'` und nicht `=== '1'`: unbekannt heisst an. Ein Protokoll, das nach * einem Update stillschweigend aufhört zu protokollieren, wäre die schlechtere * Überraschung. */ export const modlogArten = () => ({ loeschungen: getSetting('modlog_loeschungen') !== '0', bearbeitungen: getSetting('modlog_bearbeitungen') !== '0', mitglieder: getSetting('modlog_mitglieder') !== '0', beitritte: getSetting('modlog_beitritte') !== '0', }); /** * Die vier Schalter einmalig auf „an" setzen. * * Nötig, weil das Panel Schalter aus `fields` mit `=== '1'` liest: ohne * geschriebenen Wert stünden sie dort auf aus, während sie sich in Wahrheit * an verhalten. Ein Formular, das das Gegenteil dessen zeigt, was gilt, ist * schlimmer als gar keins. * * Nur wenn nichts dasteht — wer abschaltet, behält es abgeschaltet. */ export function modlogArtenGrundstock() { for (const key of ['modlog_loeschungen', 'modlog_bearbeitungen', 'modlog_mitglieder', 'modlog_beitritte']) { if (getSetting(key) === null) setSetting(key, '1'); } } /** * Lautstärke des Radios, in Prozent. * * Die Vorgabe ist 50 und nicht 100, und das ist eine Beobachtung aus dem * Betrieb, keine Geschmacksfrage: mit voller Aussteuerung kommt der Bot in * den Kanal, und wer schon drin sitzt, erschrickt. Jeder kann ihn zwar für * sich selbst leiser drehen — aber erst *nachdem* es zu laut war. Also liegt * die Vorgabe auf der leisen Seite; lauter machen kostet zwei Knopfdrücke. * * Bewusst hier und nicht in `radio_state`: der Merker dort wird beim Stoppen * gelöscht, die eingestellte Lautstärke soll das überleben. */ export const LAUT_MIN = 10; export const LAUT_MAX = 200; export const LAUT_SCHRITT = 10; export const LAUT_VORGABE = 50; export function radioLautstaerke() { const roh = getSetting('radio_lautstaerke'); // Der unbelegte Fall muss VOR der Umwandlung abgefangen werden. // // `Number(null)` ist 0, und 0 ist endlich — die Prüfung unten hat „noch // nie eingestellt" deshalb nie erwischt, sondern auf LAUT_MIN geklemmt. // Ergebnis: eine frische Installation startete mit 10 % statt mit den 50, // die zwei Zeilen weiter oben als Vorgabe begründet werden. Gemessen am // 28.08.2026 gegen eine leere Datenbank. if (roh === null || roh === undefined || roh === '') return LAUT_VORGABE; const wert = Number(roh); if (!Number.isFinite(wert)) return LAUT_VORGABE; return Math.min(LAUT_MAX, Math.max(LAUT_MIN, Math.round(wert))); } /** * Darf das Radio auch YouTube abspielen? * * Getrennt vom Modul-Schalter, und zwar mit Absicht: YouTube ist der Teil, * der von aussen kaputtgehen kann (gesperrte Adressen, PO-Token, ein yt-dlp, * das seit drei Wochen nicht aktualisiert wurde). Wenn das klemmt, soll man * genau das abschalten können — und nicht das Radio mit, das davon nichts hat. * * Vorgabe: aus. Wer YouTube will, sagt es einmal. */ export function youtubeAn() { return getSetting('radio_youtube_enabled') === '1'; } /** * Eigene Argumente für yt-dlp, roh wie eingetippt. * * Das Feld gibt es, weil YouTube seine Sperren schneller ändert, als hier ein * Abbild neu gebaut wird: `--extractor-args`, ein anderer Client, eine * Bandbreitenbremse. Ohne dieses Feld hiesse jede solche Anpassung Deploy. */ export function ytdlpArgumente() { return getSetting('ytdlp_args') ?? ''; }