Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b4efb2cb9 | ||
|
|
c840f6fc7e | ||
|
|
b1ec1a2a29 |
+14
-14
@@ -74,26 +74,26 @@ GITEA_API_TOKEN=
|
||||
# YTDLP_PATH=yt-dlp
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# VERBINDUNG ZUM SPIELSERVER (fuer /whitelist)
|
||||
# ZUGANG ZUM ADMINPANEL (fuer /whitelist)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# WOZU: der Bot fragt den Spielserver, WER die Whitelist verwalten darf. Die
|
||||
# Antwort steht in der Rechtematrix - derselben, die Adminmenue und Panel
|
||||
# benutzen. Wuerde der Bot stattdessen Discord-Berechtigungen pruefen, waere
|
||||
# das eine zweite Rechteverwaltung, die von einer Aenderung in der Matrix nie
|
||||
# erfaehrt.
|
||||
# WOZU: der Bot fragt, WER die Whitelist verwalten darf. Die Antwort steht in
|
||||
# der Rechtematrix - derselben, die Adminmenue und Panel benutzen. Wuerde der
|
||||
# Bot stattdessen Discord-Berechtigungen pruefen, waere das eine zweite
|
||||
# Rechteverwaltung, die von einer Aenderung in der Matrix nie erfaehrt.
|
||||
#
|
||||
# WARUM UEBER DAS PANEL UND NICHT DIREKT ZUM SPIELSERVER: sonst haetten ZWEI
|
||||
# Dienste den Schluessel zu allem - Rechtematrix, Spielerliste, Protokoll,
|
||||
# Schreibweg. Der Bot bekommt stattdessen einen eigenen, der genau eine Tuer
|
||||
# oeffnet.
|
||||
#
|
||||
# OHNE DIESE ZWEI WERTE TUT /whitelist NICHTS und sagt es. Ein Bot, der bei
|
||||
# fehlender Konfiguration durchwinkt, waere schlimmer als einer, der schweigt.
|
||||
# ACP_URL=https://acp.d4rkst3r.de
|
||||
#
|
||||
# Die Adresse ist die INTERNE: der Endpunkt liegt auf dem Spielport, und dieser
|
||||
# Weg soll nicht ueber das oeffentliche Netz laufen.
|
||||
# SPIELSERVER_URL=http://host.docker.internal:30130/d4rk_web
|
||||
#
|
||||
# DASSELBE GEHEIMNIS wie im Adminpanel (d4rk_web:geheimnis in web.cfg). Der Bot
|
||||
# ist damit der ZWEITE Halter - beide sind eigene Container auf demselben Host,
|
||||
# aber es bleibt eine Verdopplung und ist hier ausdruecklich benannt.
|
||||
# SPIELSERVER_TOKEN=
|
||||
# Derselbe Wert wie BOT_TOKEN im Adminpanel. NICHT dessen D4RK_WEB_TOKEN -
|
||||
# das ist der Schluessel zum Spielserver und geht den Bot nichts an.
|
||||
# ACP_BOT_TOKEN=
|
||||
#
|
||||
# Die Rolle, die den Zutritt oeffnet. Sie MUSS dieselbe sein, die der
|
||||
# Spielserver prueft (Convar d4rk_discord:role) - stimmen sie nicht ueberein,
|
||||
|
||||
@@ -37,6 +37,25 @@ services:
|
||||
# Lokale Zeitzone — wichtig für den Wochen-Rückblick (sonntags 20:00) und Datumsangaben
|
||||
TZ: ${TZ:-Europe/Berlin}
|
||||
DB_PATH: /app/data/ecobot.db
|
||||
# ---------------------------------------------------------------
|
||||
# Zugang zum Adminpanel (fuer /whitelist)
|
||||
# ---------------------------------------------------------------
|
||||
#
|
||||
# Der Bot fragt das PANEL, nicht den Spielserver: sonst haetten zwei
|
||||
# Dienste den Schluessel zu allem. Dieser hier oeffnet genau eine Tuer.
|
||||
#
|
||||
# Kein Vorgabewert - ein Bot, der bei fehlender Konfiguration heimlich
|
||||
# irgendwohin verbindet, ist schlechter als einer, der "nicht
|
||||
# eingerichtet" sagt.
|
||||
ACP_URL: ${ACP_URL:-}
|
||||
ACP_BOT_TOKEN: ${ACP_BOT_TOKEN:-}
|
||||
# Die Discord-Rolle, die den Zutritt oeffnet. NUR NOCH RUECKFALL: seit
|
||||
# dem 04.09.2026 holt der Bot sie vom Adminpanel (/bot/config), wo sie
|
||||
# auf der Seite "Einstellungen" eingetragen wird. Was dort steht, gewinnt.
|
||||
#
|
||||
# Der Rueckfall bleibt, damit /whitelist auch dann noch etwas anzeigen
|
||||
# kann, wenn das Panel gerade nicht antwortet.
|
||||
WHITELIST_ROLE_ID: ${WHITELIST_ROLE_ID:-}
|
||||
# HTTP-Endpoint für Gitea-Webhooks + Webinterface
|
||||
# NPM leitet bot.d4rkst3r.de → host.docker.internal:3080
|
||||
ports:
|
||||
|
||||
@@ -24,20 +24,34 @@
|
||||
// gemessen hat, ist ein Schreibbefehl ins Ungewisse.
|
||||
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
|
||||
|
||||
import { person, spielserverBereit, darfWhitelist } from '../../spielserver.js';
|
||||
import { person, panelBereit, darfWhitelist, konfig } from '../../panel.js';
|
||||
|
||||
// Die Rollen-ID kommt aus derselben Umgebung wie der Rest der Botkonfiguration.
|
||||
// Sie ist kein Geheimnis, aber sie muss zu der passen, die der Spielserver
|
||||
// prüft (`d4rk_discord:role`) — stimmen sie nicht überein, zeigt dieser Befehl
|
||||
// „hat die Rolle" für eine Rolle, die den Zutritt gar nicht öffnet.
|
||||
const WHITELIST_ROLLE = process.env.WHITELIST_ROLE_ID || '';
|
||||
/*
|
||||
DIE ROLLEN-ID STEHT IM PANEL, NICHT HIER.
|
||||
|
||||
Sie ist kein Geheimnis, aber sie muss zu der passen, die der Spielserver
|
||||
prüft (`d4rk_discord:role`) — stimmen sie nicht überein, zeigt dieser Befehl
|
||||
„hat die Rolle" für eine Rolle, die den Zutritt gar nicht öffnet. Genau
|
||||
deshalb wird sie an EINEM Ort eingetragen: auf der Einstellungsseite des
|
||||
Adminpanels.
|
||||
|
||||
Die Umgebungsvariable bleibt als Rückfall, damit der Bot auch dann läuft,
|
||||
wenn das Panel gerade nicht antwortet.
|
||||
*/
|
||||
async function whitelistRolle() {
|
||||
const k = await konfig();
|
||||
|
||||
if (k.ok && k.whitelistRolle) return k.whitelistRolle;
|
||||
|
||||
return process.env.WHITELIST_ROLE_ID || '';
|
||||
}
|
||||
|
||||
const GRUENDE = {
|
||||
nicht_eingerichtet:
|
||||
'Der Bot kennt den Spielserver nicht — `SPIELSERVER_URL` und `SPIELSERVER_TOKEN` fehlen.',
|
||||
spielserver_nicht_erreichbar: 'Der Spielserver antwortet gerade nicht.',
|
||||
zeitueberschreitung: 'Der Spielserver hat zu lange gebraucht.',
|
||||
abgewiesen_oder_unbekannter_weg: 'Der Spielserver weist den Bot ab — stimmt das Geheimnis?',
|
||||
'Der Bot kennt das Adminpanel nicht — `ACP_URL` und `ACP_BOT_TOKEN` fehlen.',
|
||||
panel_nicht_erreichbar: 'Das Adminpanel antwortet gerade nicht.',
|
||||
zeitueberschreitung: 'Das Panel hat zu lange gebraucht.',
|
||||
abgewiesen_oder_unbekannter_weg: 'Das Panel weist den Bot ab — stimmt der Schlüssel?',
|
||||
rechtesystem_stumm: 'Das Rechtesystem auf dem Spielserver antwortet nicht.',
|
||||
recht_nicht_lesbar: 'Die Rechteprüfung war nicht lesbar.',
|
||||
};
|
||||
@@ -58,7 +72,7 @@ export async function execute(interaction) {
|
||||
// die gehören nicht in einen offenen Kanal.
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
if (!spielserverBereit()) {
|
||||
if (!panelBereit()) {
|
||||
return interaction.editReply(GRUENDE.nicht_eingerichtet);
|
||||
}
|
||||
|
||||
@@ -99,16 +113,18 @@ export async function execute(interaction) {
|
||||
Verbindung. Wer sie hier vom Server erwartet, bekommt eine Antwort, die
|
||||
geraten wäre.
|
||||
*/
|
||||
let rolleText = 'nicht prüfbar — `WHITELIST_ROLE_ID` ist nicht gesetzt';
|
||||
const rolle = await whitelistRolle();
|
||||
|
||||
if (WHITELIST_ROLLE) {
|
||||
let rolleText = 'nicht prüfbar — im Adminpanel ist keine Whitelist-Rolle eingetragen';
|
||||
|
||||
if (rolle) {
|
||||
const mitglied = await interaction.guild?.members
|
||||
.fetch(ziel.id)
|
||||
.catch(() => null);
|
||||
|
||||
rolleText = !mitglied
|
||||
? 'nicht auf diesem Discord'
|
||||
: mitglied.roles.cache.has(WHITELIST_ROLLE)
|
||||
: mitglied.roles.cache.has(rolle)
|
||||
? '**ja**'
|
||||
: '**nein**';
|
||||
}
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
// Die eine Stelle, an der der Bot mit dem Adminpanel redet.
|
||||
//
|
||||
// ============================================================================
|
||||
// WARUM ÜBER DAS PANEL UND NICHT DIREKT ZUM SPIELSERVER
|
||||
// ============================================================================
|
||||
// Der erste Entwurf gab dem Bot das Geheimnis von `d4rk_web`. Dann hätten
|
||||
// **zwei** Dienste den Schlüssel zu allem gehabt: Rechtematrix, Spielerliste,
|
||||
// Protokoll, Schreibweg. Ein Bot, der nur wissen soll, ob jemand die Whitelist
|
||||
// verwalten darf, braucht davon nichts.
|
||||
//
|
||||
// Betreiber am 04.09.2026: „können wir das nicht mit in die webseite einbauen,
|
||||
// das er sich das über das acp holt?" — genau so ist es jetzt. Das Panel bleibt
|
||||
// der einzige, der mit dem Spielserver spricht; der Bot bekommt einen eigenen
|
||||
// Schlüssel, der genau eine Tür öffnet.
|
||||
//
|
||||
// ============================================================================
|
||||
// EIN FEHLSCHLAG IST EINE ANTWORT, KEINE AUSNAHME
|
||||
// ============================================================================
|
||||
// Jede Funktion gibt `{ ok: false, grund }` zurück statt zu werfen. Ein
|
||||
// Discord-Befehl, der mit einem Stacktrace endet, zeigt dem Benutzer nichts —
|
||||
// der Grund gehört in die Antwort.
|
||||
|
||||
const BASIS = process.env.ACP_URL || '';
|
||||
const TOKEN = process.env.ACP_BOT_TOKEN || '';
|
||||
|
||||
/** Ist der Zugang überhaupt eingerichtet? */
|
||||
export function panelBereit() {
|
||||
return Boolean(BASIS && TOKEN);
|
||||
}
|
||||
|
||||
async function hole(weg) {
|
||||
if (!panelBereit()) {
|
||||
return { ok: false, grund: 'nicht_eingerichtet' };
|
||||
}
|
||||
|
||||
const steuerung = new AbortController();
|
||||
const uhr = setTimeout(() => steuerung.abort(), 8000);
|
||||
|
||||
try {
|
||||
const antwort = await fetch(`${BASIS}/bot/${weg}`, {
|
||||
headers: { 'X-Bot-Token': TOKEN },
|
||||
signal: steuerung.signal,
|
||||
});
|
||||
|
||||
// 404 IST AUCH DIE ANTWORT AUF EINEN FALSCHEN SCHLÜSSEL — das Panel ist
|
||||
// so gebaut, damit von außen nicht erkennbar ist, dass es einen
|
||||
// Maschinenzugang gibt. Für uns heißt es deshalb nicht „Weg gibt es
|
||||
// nicht", sondern „wir kommen nicht durch".
|
||||
if (antwort.status === 404) {
|
||||
return { ok: false, grund: 'abgewiesen_oder_unbekannter_weg' };
|
||||
}
|
||||
|
||||
if (!antwort.ok) {
|
||||
return { ok: false, grund: `http_${antwort.status}` };
|
||||
}
|
||||
|
||||
return await antwort.json();
|
||||
} catch (fehler) {
|
||||
return {
|
||||
ok: false,
|
||||
grund: fehler?.name === 'AbortError'
|
||||
? 'zeitueberschreitung'
|
||||
: 'panel_nicht_erreichbar',
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(uhr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alles, was der Spielserver über eine Discord-Kennung weiß.
|
||||
*
|
||||
* Zu jeder Quelle kommt ein Flag mit, ob sie überhaupt geantwortet hat. Eine
|
||||
* fehlende Quelle als „nichts gefunden" zu lesen wäre der Fehler, den diese
|
||||
* Flags verhindern.
|
||||
*/
|
||||
export async function person(discordId, recht) {
|
||||
// `recht` ist optional. Wird eines mitgegeben, prueft der Spielserver es
|
||||
// gleich mit und legt `darf` in die Antwort - eine Frage, eine Runde.
|
||||
const zusatz = recht ? `&recht=${encodeURIComponent(recht)}` : '';
|
||||
const a = await hole(`person?discord=${encodeURIComponent(discordId)}${zusatz}`);
|
||||
|
||||
if (!a.ok) return a;
|
||||
|
||||
return { ok: true, ...a.person };
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf diese Discord-Kennung die Whitelist verwalten?
|
||||
*
|
||||
* DREI AUSGÄNGE, NICHT ZWEI: erlaubt, verboten, und „ich weiß es nicht".
|
||||
* Der dritte ist der wichtige — ein Bot, der bei einer Störung „verboten"
|
||||
* sagt, sperrt das Team aus; einer, der „erlaubt" sagt, öffnet es für alle.
|
||||
* Beides wäre geraten, also wird es unterschieden.
|
||||
*/
|
||||
export async function darfWhitelist(discordId) {
|
||||
const p = await person(discordId, 'd4rk.whitelist');
|
||||
|
||||
if (!p.ok) return { bekannt: false, grund: p.grund };
|
||||
if (p.rolleGelesen === false) return { bekannt: false, grund: 'rechtesystem_stumm' };
|
||||
if (!p.rolle) return { bekannt: true, darf: false, grund: 'keine_rolle' };
|
||||
if (p.rechtGelesen === false) return { bekannt: false, grund: 'recht_nicht_lesbar' };
|
||||
|
||||
return { bekannt: true, darf: p.darf === true, rolle: p.rolle };
|
||||
}
|
||||
|
||||
/**
|
||||
* Die Einstellungen, die im Panel eingetragen sind.
|
||||
*
|
||||
* WARUM DER BOT SIE NICHT SELBST FUEHRT: sonst stuende die Whitelist-Rolle an
|
||||
* zwei Orten — hier in der Umgebung und dort auf der Seite. Zwei Orte fuer
|
||||
* dieselbe Wahrheit heisst, dass eine Aenderung eine der beiden vergisst, und
|
||||
* der Fehler sieht dann aus wie „die Rolle wird nicht erkannt".
|
||||
*
|
||||
* Kurz gepuffert, damit nicht jeder Befehl eine Runde ueber das Netz macht.
|
||||
* Ein Fehlschlag wird NICHT gepuffert — sonst bliebe eine Stoerung eine Minute
|
||||
* lang stehen, nachdem sie vorbei ist.
|
||||
*/
|
||||
let konfigStand = null;
|
||||
let konfigZeit = 0;
|
||||
|
||||
export async function konfig() {
|
||||
if (konfigStand && Date.now() - konfigZeit < 30000) return konfigStand;
|
||||
|
||||
const a = await hole('config');
|
||||
|
||||
if (!a.ok) return a;
|
||||
|
||||
konfigStand = a;
|
||||
konfigZeit = Date.now();
|
||||
|
||||
return a;
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
// Die eine Stelle, an der der Bot mit dem Spielserver redet.
|
||||
//
|
||||
// ============================================================================
|
||||
// WARUM DER BOT DEN SPIELSERVER FRAGT UND NICHT SELBST ENTSCHEIDET
|
||||
// ============================================================================
|
||||
// Wer die Whitelist verwalten darf, steht in der Rechtematrix — dort, wo es
|
||||
// auch für das Adminmenü und das Browserpanel steht. Ein Bot, der stattdessen
|
||||
// Discord-Berechtigungen prüft, wäre eine zweite Rechteverwaltung: sie würde
|
||||
// nichts von einer Änderung in der Matrix erfahren, und irgendwann darf
|
||||
// jemand in Discord etwas, das ihm im Spiel längst genommen wurde.
|
||||
//
|
||||
// Das ist dieselbe Regel, die ADR-0012 für das Panel setzt: Discord-Kennung →
|
||||
// vorhandenes Recht, keine zweite Verwaltung.
|
||||
//
|
||||
// ============================================================================
|
||||
// DAS GEHEIMNIS IST DER ZWEITE HALTER — UND DAS IST EINE ENTSCHEIDUNG
|
||||
// ============================================================================
|
||||
// Bisher kannte nur das Adminpanel den Token für d4rk_web. Der Bot ist jetzt
|
||||
// der zweite. Beide sind eigene Container auf demselben Host, beide reden über
|
||||
// das interne Netz — aber es bleibt eine Verdopplung, und sie steht deshalb
|
||||
// hier und nicht in einer Fußnote.
|
||||
//
|
||||
// Ohne Token macht dieser Baustein NICHTS und sagt es. Ein Bot, der bei
|
||||
// fehlender Konfiguration einfach durchwinkt, wäre schlimmer als einer, der
|
||||
// gar nicht antwortet.
|
||||
//
|
||||
// ============================================================================
|
||||
// EIN FEHLSCHLAG IST EINE ANTWORT, KEINE AUSNAHME
|
||||
// ============================================================================
|
||||
// Jede Funktion hier gibt `{ ok: false, grund }` zurück statt zu werfen. Ein
|
||||
// Discord-Befehl, der mit einem Stacktrace endet, zeigt dem Benutzer nichts —
|
||||
// der Grund gehört in die Antwort, damit der Mensch davor weiß, was los ist.
|
||||
|
||||
const BASIS = process.env.SPIELSERVER_URL || '';
|
||||
const TOKEN = process.env.SPIELSERVER_TOKEN || '';
|
||||
|
||||
/** Ist der Kanal überhaupt eingerichtet? Für Befehle, die das anzeigen wollen. */
|
||||
export function spielserverBereit() {
|
||||
return Boolean(BASIS && TOKEN);
|
||||
}
|
||||
|
||||
async function hole(weg) {
|
||||
if (!BASIS || !TOKEN) {
|
||||
return { ok: false, grund: 'nicht_eingerichtet' };
|
||||
}
|
||||
|
||||
const steuerung = new AbortController();
|
||||
const uhr = setTimeout(() => steuerung.abort(), 8000);
|
||||
|
||||
try {
|
||||
const antwort = await fetch(`${BASIS}/${weg}`, {
|
||||
headers: { 'X-D4rk-Token': TOKEN },
|
||||
signal: steuerung.signal,
|
||||
});
|
||||
|
||||
// 404 IST DIE ANTWORT AUF EIN FALSCHES GEHEIMNIS — so ist der Endpunkt
|
||||
// gebaut, damit er von außen nicht als vorhanden erkennbar ist. Für uns
|
||||
// heißt es also nicht „Weg gibt es nicht", sondern „wir kommen nicht
|
||||
// durch", und genau das soll die Meldung sagen.
|
||||
if (antwort.status === 404) {
|
||||
return { ok: false, grund: 'abgewiesen_oder_unbekannter_weg' };
|
||||
}
|
||||
|
||||
if (!antwort.ok) {
|
||||
return { ok: false, grund: `http_${antwort.status}` };
|
||||
}
|
||||
|
||||
return await antwort.json();
|
||||
} catch (fehler) {
|
||||
return {
|
||||
ok: false,
|
||||
grund: fehler?.name === 'AbortError'
|
||||
? 'zeitueberschreitung'
|
||||
: 'spielserver_nicht_erreichbar',
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(uhr);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alles, was der Spielserver über eine Discord-Kennung weiß.
|
||||
*
|
||||
* Gibt `person` zurück mit: rolle, rang, darfWhitelist, konto — und zu jeder
|
||||
* Quelle ein Flag, ob sie überhaupt geantwortet hat. Eine fehlende Quelle als
|
||||
* „nichts gefunden" zu lesen wäre der Fehler, den diese Flags verhindern.
|
||||
*/
|
||||
export async function person(discordId) {
|
||||
const a = await hole(`person?discord=${encodeURIComponent(discordId)}`);
|
||||
|
||||
if (!a.ok) return a;
|
||||
|
||||
return { ok: true, ...a.person };
|
||||
}
|
||||
|
||||
/**
|
||||
* Darf diese Discord-Kennung die Whitelist verwalten?
|
||||
*
|
||||
* DREI AUSGÄNGE, NICHT ZWEI: erlaubt, verboten, und „ich weiß es nicht".
|
||||
* Der dritte ist der wichtige — ein Bot, der bei einer Störung „verboten"
|
||||
* sagt, sperrt das Team aus; einer, der „erlaubt" sagt, öffnet es für alle.
|
||||
* Beides wäre geraten, also wird es unterschieden.
|
||||
*/
|
||||
export async function darfWhitelist(discordId) {
|
||||
const p = await person(discordId);
|
||||
|
||||
if (!p.ok) return { bekannt: false, grund: p.grund };
|
||||
if (p.rolleGelesen === false) return { bekannt: false, grund: 'rechtesystem_stumm' };
|
||||
if (!p.rolle) return { bekannt: true, darf: false, grund: 'keine_rolle' };
|
||||
if (p.rechtGelesen === false) return { bekannt: false, grund: 'recht_nicht_lesbar' };
|
||||
|
||||
return { bekannt: true, darf: p.darfWhitelist === true, rolle: p.rolle };
|
||||
}
|
||||
Reference in New Issue
Block a user