Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b4efb2cb9 | ||
|
|
c840f6fc7e | ||
|
|
b1ec1a2a29 | ||
|
|
6fab871225 | ||
|
|
958b3b64f2 |
@@ -72,3 +72,30 @@ GITEA_API_TOKEN=
|
|||||||
# als "yt-dlp fehlt" erscheinen -- nicht als Stille im Sprachkanal.
|
# als "yt-dlp fehlt" erscheinen -- nicht als Stille im Sprachkanal.
|
||||||
# FFMPEG_PATH=ffmpeg
|
# FFMPEG_PATH=ffmpeg
|
||||||
# YTDLP_PATH=yt-dlp
|
# YTDLP_PATH=yt-dlp
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ZUGANG ZUM ADMINPANEL (fuer /whitelist)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
#
|
||||||
|
# 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,
|
||||||
|
# zeigt /whitelist "hat die Rolle" fuer eine Rolle, die gar nichts oeffnet.
|
||||||
|
# WHITELIST_ROLE_ID=
|
||||||
|
|||||||
@@ -37,6 +37,25 @@ services:
|
|||||||
# Lokale Zeitzone — wichtig für den Wochen-Rückblick (sonntags 20:00) und Datumsangaben
|
# Lokale Zeitzone — wichtig für den Wochen-Rückblick (sonntags 20:00) und Datumsangaben
|
||||||
TZ: ${TZ:-Europe/Berlin}
|
TZ: ${TZ:-Europe/Berlin}
|
||||||
DB_PATH: /app/data/ecobot.db
|
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
|
# HTTP-Endpoint für Gitea-Webhooks + Webinterface
|
||||||
# NPM leitet bot.d4rkst3r.de → host.docker.internal:3080
|
# NPM leitet bot.d4rkst3r.de → host.docker.internal:3080
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
+2
-1
@@ -51,12 +51,13 @@ import * as raid from './commands/raid.js';
|
|||||||
import * as umfrage from './commands/umfrage.js';
|
import * as umfrage from './commands/umfrage.js';
|
||||||
import * as radio from './commands/radio.js';
|
import * as radio from './commands/radio.js';
|
||||||
import * as musik from './commands/musik.js';
|
import * as musik from './commands/musik.js';
|
||||||
|
import * as whitelist from './commands/whitelist.js';
|
||||||
|
|
||||||
// Alle Commands hier eintragen — jedes Modul exportiert { data, execute }
|
// Alle Commands hier eintragen — jedes Modul exportiert { data, execute }
|
||||||
const commandModules = [
|
const commandModules = [
|
||||||
ping, devlogBackfill, bug, playtesterSetup, galerieBackfill,
|
ping, devlogBackfill, bug, playtesterSetup, galerieBackfill,
|
||||||
wunsch, wunschSetup, giveaway, ticketSetup, warn, warns, timeout, purge, rank, tag, remind, geburtstag,
|
wunsch, wunschSetup, giveaway, ticketSetup, warn, warns, timeout, purge, rank, tag, remind, geburtstag,
|
||||||
raid, umfrage, radio, musik,
|
raid, umfrage, radio, musik, whitelist,
|
||||||
];
|
];
|
||||||
|
|
||||||
export async function startBot() {
|
export async function startBot() {
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// /whitelist — Zutritt zum Spielserver, von Discord aus.
|
||||||
|
//
|
||||||
|
// ============================================================================
|
||||||
|
// DIE WHITELIST *IST* EINE DISCORD-ROLLE
|
||||||
|
// ============================================================================
|
||||||
|
// d4rk_lib prüft bei jeder Verbindung live, ob die Person eine bestimmte Rolle
|
||||||
|
// auf diesem Discord hat. „Jemanden auf die Whitelist setzen" heißt deshalb
|
||||||
|
// nichts anderes als: ihm die Rolle geben. Das kann nur der Bot — der
|
||||||
|
// Spielserver hat keinen Zugriff auf Discord.
|
||||||
|
//
|
||||||
|
// Umgekehrt weiß nur der Spielserver, WER das darf. Also fragt der Bot ihn,
|
||||||
|
// statt Discord-Berechtigungen zu prüfen: sonst gäbe es zwei
|
||||||
|
// Rechteverwaltungen, und die zweite erführe nie von einer Änderung in der
|
||||||
|
// Matrix.
|
||||||
|
//
|
||||||
|
// ============================================================================
|
||||||
|
// DIESER BEFEHL LIEST NUR
|
||||||
|
// ============================================================================
|
||||||
|
// `status` beantwortet drei Fragen an einem Ort: hat die Person die Rolle,
|
||||||
|
// kennt der Spielserver ihr Konto, und welche Teamrolle hat sie bei uns.
|
||||||
|
//
|
||||||
|
// Das Vergeben und Entziehen kommt als eigener Schritt — erst wenn dieser
|
||||||
|
// Weg nachweislich durchläuft. Ein Schreibbefehl auf einem Kanal, den niemand
|
||||||
|
// gemessen hat, ist ein Schreibbefehl ins Ungewisse.
|
||||||
|
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
|
||||||
|
|
||||||
|
import { person, panelBereit, darfWhitelist, konfig } from '../../panel.js';
|
||||||
|
|
||||||
|
/*
|
||||||
|
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 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.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const data = new SlashCommandBuilder()
|
||||||
|
.setName('whitelist')
|
||||||
|
.setDescription('Zutritt zum Spielserver')
|
||||||
|
.addSubcommand((s) =>
|
||||||
|
s.setName('status')
|
||||||
|
.setDescription('Was weiß der Server über diese Person?')
|
||||||
|
.addUserOption((o) =>
|
||||||
|
o.setName('person').setDescription('Wen nachschlagen?').setRequired(true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export async function execute(interaction) {
|
||||||
|
// Nur für den Aufrufer sichtbar: hier stehen Kennungen und Zeitpunkte, und
|
||||||
|
// die gehören nicht in einen offenen Kanal.
|
||||||
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||||
|
|
||||||
|
if (!panelBereit()) {
|
||||||
|
return interaction.editReply(GRUENDE.nicht_eingerichtet);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ZUERST: DARF DER AUFRUFER DAS ÜBERHAUPT?
|
||||||
|
|
||||||
|
Die Antwort kennt drei Ausgänge, und der dritte ist der wichtige: „ich
|
||||||
|
weiß es nicht". Bei einer Störung weder öffnen noch aussperren, sondern
|
||||||
|
sagen, dass die Auskunft fehlt.
|
||||||
|
*/
|
||||||
|
const erlaubnis = await darfWhitelist(interaction.user.id);
|
||||||
|
|
||||||
|
if (!erlaubnis.bekannt) {
|
||||||
|
return interaction.editReply(
|
||||||
|
`Das lässt sich gerade nicht feststellen: ${GRUENDE[erlaubnis.grund] ?? erlaubnis.grund}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!erlaubnis.darf) {
|
||||||
|
return interaction.editReply(
|
||||||
|
erlaubnis.grund === 'keine_rolle'
|
||||||
|
? 'Du stehst nicht im Team des Servers.'
|
||||||
|
: `Deine Rolle **${erlaubnis.rolle}** darf die Whitelist nicht verwalten.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ziel = interaction.options.getUser('person', true);
|
||||||
|
const p = await person(ziel.id);
|
||||||
|
|
||||||
|
if (!p.ok) {
|
||||||
|
return interaction.editReply(`Nachschlagen ging nicht: ${GRUENDE[p.grund] ?? p.grund}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
DIE DISCORD-ROLLE FRAGT DER BOT SELBST.
|
||||||
|
|
||||||
|
Der Spielserver weiß sie nicht — er sieht sie nur im Moment einer
|
||||||
|
Verbindung. Wer sie hier vom Server erwartet, bekommt eine Antwort, die
|
||||||
|
geraten wäre.
|
||||||
|
*/
|
||||||
|
const rolle = await whitelistRolle();
|
||||||
|
|
||||||
|
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(rolle)
|
||||||
|
? '**ja**'
|
||||||
|
: '**nein**';
|
||||||
|
}
|
||||||
|
|
||||||
|
const k = p.konto;
|
||||||
|
|
||||||
|
const zeilen = [
|
||||||
|
`**${ziel.tag}**`,
|
||||||
|
'',
|
||||||
|
`Whitelist-Rolle: ${rolleText}`,
|
||||||
|
// KEINE ROLLE IST EINE ANTWORT, kein Fehler: die meisten Spieler stehen
|
||||||
|
// nicht im Team.
|
||||||
|
`Teamrolle: ${p.rolle ? `**${p.rolle}**` : '—'}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (p.kontoGelesen === false) {
|
||||||
|
zeilen.push('Konto: *nicht lesbar* — das heißt **nicht**, dass es keins gibt.');
|
||||||
|
} else if (!k) {
|
||||||
|
// War nie auf dem Server, ODER war da, bevor der Server sich Konten
|
||||||
|
// merkt (seit 04.09.2026). Beides sagt derselbe Satz, weil der Bot es
|
||||||
|
// nicht unterscheiden kann.
|
||||||
|
zeilen.push('Konto: **unbekannt** — noch nie verbunden, oder zuletzt vor dem 04.09.2026.');
|
||||||
|
} else {
|
||||||
|
zeilen.push(
|
||||||
|
`Konto: **${k.name ?? '—'}** (${k.lizenzKurz ?? '—'})`,
|
||||||
|
`Charaktere: **${k.charaktere}** · zuletzt **${k.zuletzt}** · seit **${k.zuerst}**`,
|
||||||
|
k.online ? 'Gerade **online**.' : ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return interaction.editReply(zeilen.filter(Boolean).join('\n'));
|
||||||
|
}
|
||||||
+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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user