Files
d4rkbot/src/runtime-settings.js
T
D4rkst3r 409b28e9e0 Eine Quelle, zwei Arten -- Sender und Titel gehen denselben Weg
Der ganze Unterschied ist einer: ein Sender endet nie, ein Titel schon.
Also verzweigt genau eine Abspielfunktion und genau ein Idle-Melder nach
der Art der Quelle, statt dass ein zweiter Pfad daneben entsteht -- zwei
Pfade, die dasselbe tun sollen, driften auseinander.

Beim Sender heisst Idle "die Leitung ist weg" (neu aufbauen), bei YouTube
"fertig, der naechste bitte". Danach: Warteschlange, sonst der Sender von
vorher, sonst raus. Das Radio ist die Grundstellung, YouTube die
Unterbrechung.

Bremse gegen das Durchrutschen: ein Eintrag ohne Ton geht sofort wieder
auf Idle. Ohne Zaehlung waere eine Liste mit einem kaputten Eintrag in
Sekundenbruchteilen leer, und niemand haette gesehen warum. Nach drei
Fehlschlaegen in Folge wird angehalten und der Grund gemeldet. Von Hand
Uebersprungenes zaehlt nicht mit.

Zwei Fehler nebenbei, beide vorher schon da:

Der Idle-Melder hielt `kanal` aus dem Abschluss des ERSTEN Aufrufs fest.
Nach einem Kanalwechsel haette der Wiederanlauf in den alten Kanal
gespielt. Er liest den Kanal jetzt frisch aus `laufend`.

`radioLautstaerke()` gab bei unbelegter Einstellung 10 zurueck statt der
50, die der Kommentar zwei Zeilen darueber als Vorgabe begruendet:
`Number(null)` ist 0 und damit endlich, die Pruefung auf "nicht endlich"
hat den unbelegten Fall nie erwischt und auf die Untergrenze geklemmt.
Eine frische Installation startete also sehr leise. Gemessen gegen eine
leere Datenbank, mit Gegenproben fuer leeren String, Unsinn, 0 und 999.
2026-08-28 13:33:42 +02:00

421 lines
14 KiB
JavaScript

// 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') ?? '';
}