feat(bot): der Bot fragt das Panel, nicht den Spielserver
Betreiber am 04.09.2026: "koennen wir das nicht mit in die webseite einbauen, das er sich das ueber das acp holt?" - das ist der bessere Entwurf, und der Grund ist das geringste Recht. WAS DER ERSTE ANLAUF FALSCH GEMACHT HAETTE Er haette dem Bot das Geheimnis von d4rk_web gegeben. Dann haetten ZWEI Dienste den Schluessel zu allem gehabt: Rechtematrix, Spielerliste, Protokoll, Schreibweg. Ein Bot, der nur wissen soll, ob jemand die Whitelist verwalten darf, braucht davon nichts. Jetzt bleibt das Panel der einzige, der mit dem Spielserver redet, und der Bot bekommt einen eigenen Schluessel (BOT_TOKEN), der genau eine Tuer oeffnet: /bot/person. EIGENER PFAD, EIGENER WAECHTER /bot/ statt /api/bot/, weil der Sitzungswaechter dort Discord-Anmeldung und Rolle prueft - beides hat eine Maschine nicht. Der Botwaechter vergleicht den Schluessel zeichenweise (dieselbe Ueberlegung wie in d4rk_web) und antwortet bei fehlendem oder falschem Schluessel mit 404, nicht 401: wer von aussen probiert, soll nicht erfahren, dass es hier einen Maschinenzugang gibt. Unter 32 Zeichen gibt es den Weg gar nicht - eine halbe Konfiguration darf keine Tuer aufmachen. DURCHGEREICHT, NICHT AUSGEWERTET. Das Panel entscheidet an dieser Stelle nichts; sonst waere es die zweite Rechteverwaltung, die ADR-0012 ausschliesst. NEBENBEI ZWEI FALLEN VERMIEDEN Dem Bot fehlt extra_hosts - host.docker.internal haette gar nicht aufgeloest, und der Fehler haette wie "Spielserver antwortet nicht" ausgesehen. Ueber das oeffentliche Panel braucht er es nicht. Und das Panel nennt seine zwei Werte D4RK_WEB_URL/D4RK_WEB_TOKEN; mein Botmodul hiess erst SPIELSERVER_*. Zwei Vokabeln fuer eine Sache bedeuten, dass man beim Umzug an zwei Stellen sucht und eine vergisst. Die Datei heisst jetzt panel.js - ein Modul namens spielserver.js, das mit dem Panel redet, waere ein Name, der luegt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,7 +24,7 @@
|
||||
// gemessen hat, ist ein Schreibbefehl ins Ungewisse.
|
||||
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
|
||||
|
||||
import { person, spielserverBereit, darfWhitelist } from '../../spielserver.js';
|
||||
import { person, panelBereit, darfWhitelist } 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
|
||||
@@ -34,10 +34,10 @@ const WHITELIST_ROLLE = 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 +58,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);
|
||||
}
|
||||
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// 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) {
|
||||
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 };
|
||||
}
|
||||
@@ -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