Compare commits
5
Commits
6fab871225
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00e4774a5f | ||
|
|
90fefb2865 | ||
|
|
8b4efb2cb9 | ||
|
|
c840f6fc7e | ||
|
|
b1ec1a2a29 |
+14
-14
@@ -74,26 +74,26 @@ GITEA_API_TOKEN=
|
|||||||
# YTDLP_PATH=yt-dlp
|
# 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
|
# WOZU: der Bot fragt, WER die Whitelist verwalten darf. Die Antwort steht in
|
||||||
# Antwort steht in der Rechtematrix - derselben, die Adminmenue und Panel
|
# der Rechtematrix - derselben, die Adminmenue und Panel benutzen. Wuerde der
|
||||||
# benutzen. Wuerde der Bot stattdessen Discord-Berechtigungen pruefen, waere
|
# Bot stattdessen Discord-Berechtigungen pruefen, waere das eine zweite
|
||||||
# das eine zweite Rechteverwaltung, die von einer Aenderung in der Matrix nie
|
# Rechteverwaltung, die von einer Aenderung in der Matrix nie erfaehrt.
|
||||||
# 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
|
# OHNE DIESE ZWEI WERTE TUT /whitelist NICHTS und sagt es. Ein Bot, der bei
|
||||||
# fehlender Konfiguration durchwinkt, waere schlimmer als einer, der schweigt.
|
# 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
|
# Derselbe Wert wie BOT_TOKEN im Adminpanel. NICHT dessen D4RK_WEB_TOKEN -
|
||||||
# Weg soll nicht ueber das oeffentliche Netz laufen.
|
# das ist der Schluessel zum Spielserver und geht den Bot nichts an.
|
||||||
# SPIELSERVER_URL=http://host.docker.internal:30130/d4rk_web
|
# ACP_BOT_TOKEN=
|
||||||
#
|
|
||||||
# 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=
|
|
||||||
#
|
#
|
||||||
# Die Rolle, die den Zutritt oeffnet. Sie MUSS dieselbe sein, die der
|
# Die Rolle, die den Zutritt oeffnet. Sie MUSS dieselbe sein, die der
|
||||||
# Spielserver prueft (Convar d4rk_discord:role) - stimmen sie nicht ueberein,
|
# 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
|
# 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:
|
||||||
|
|||||||
+219
-24
@@ -14,30 +14,69 @@
|
|||||||
// Matrix.
|
// Matrix.
|
||||||
//
|
//
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// DIESER BEFEHL LIEST NUR
|
// DREI UNTERBEFEHLE, EINE RECHTEPRUEFUNG
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// `status` beantwortet drei Fragen an einem Ort: hat die Person die Rolle,
|
// `status` liest, `add` und `remove` schreiben. Alle drei fragen zuerst den
|
||||||
// kennt der Spielserver ihr Konto, und welche Teamrolle hat sie bei uns.
|
// Spielserver, ob der Aufrufer `d4rk.whitelist` hat.
|
||||||
//
|
//
|
||||||
// Das Vergeben und Entziehen kommt als eigener Schritt — erst wenn dieser
|
// Das Schreiben kam bewusst SPAETER als das Lesen: erst am 04.09.2026, nachdem
|
||||||
// Weg nachweislich durchläuft. Ein Schreibbefehl auf einem Kanal, den niemand
|
// der lesende Weg mit echten Daten durchgelaufen war („ja (2 von 2
|
||||||
// gemessen hat, ist ein Schreibbefehl ins Ungewisse.
|
// Zutrittsrollen)"). Ein Schreibbefehl auf einem Kanal, den niemand gemessen
|
||||||
|
// hat, ist ein Schreibbefehl ins Ungewisse.
|
||||||
|
//
|
||||||
|
// ============================================================================
|
||||||
|
// WAS DER BOT VERGIBT, ENTSCHEIDET DER SPIELSERVER
|
||||||
|
// ============================================================================
|
||||||
|
// Die Rolle steht in `d4rk_discord:grantrole` und MUSS eine der Rollen sein,
|
||||||
|
// die auch Zutritt oeffnen. Ist sie es nicht, verweigert dieser Befehl — eine
|
||||||
|
// Rolle zu vergeben, die niemanden hereinlaesst, waere ein Knopf, der nichts
|
||||||
|
// tut und dabei „erledigt" meldet.
|
||||||
|
//
|
||||||
|
// ============================================================================
|
||||||
|
// WO DAS LANDET
|
||||||
|
// ============================================================================
|
||||||
|
// Im Devlog-Kanal des Bots, nicht im Serverprotokoll. Der Weg dorthin ginge
|
||||||
|
// nur ueber einen Schreibzugang fuer den Bot, und der wuerde die absichtlich
|
||||||
|
// schmale Tuer aufmachen (siehe panel.js). Die Freischaltung selbst steht
|
||||||
|
// ausserdem in Discords eigenem Audit-Log, und wenn die Person verbindet,
|
||||||
|
// protokolliert d4rk_lib die Zulassung mit Namen.
|
||||||
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
|
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
|
||||||
|
|
||||||
import { person, spielserverBereit, darfWhitelist } from '../../spielserver.js';
|
import { person, panelBereit, darfWhitelist, konfig } from '../../panel.js';
|
||||||
|
import { inDevlog } from '../../melden.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
|
DIE ROLLEN WERDEN GELESEN, NICHT GEPFLEGT.
|
||||||
// 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.
|
Sie sind kein Geheimnis, aber sie müssen zu denen passen, die der
|
||||||
const WHITELIST_ROLLE = process.env.WHITELIST_ROLE_ID || '';
|
Spielserver prüft (`d4rk_discord:role`) — sonst zeigt dieser Befehl „hat die
|
||||||
|
Rolle" für eine Rolle, die den Zutritt gar nicht öffnet.
|
||||||
|
|
||||||
|
Am 04.09.2026 gemessen: die Convar enthielt ZWEI Rollen, eingetragen war
|
||||||
|
eine. Deshalb kommt die Menge jetzt vom Spielserver.
|
||||||
|
|
||||||
|
ES SIND MEHRERE, UND EINE REICHT. Wer eine davon hat, kommt herein — außer
|
||||||
|
er hat eine Sperrrolle, die schlägt alles. Das ist dieselbe Regel wie in
|
||||||
|
d4rk_lib; sie hier zu vereinfachen hieße, sie ein zweites Mal zu schreiben.
|
||||||
|
|
||||||
|
KEIN RÜCKFALL AUF EINE GESPEICHERTE ID. Eine veraltete Liste würde
|
||||||
|
selbstbewusst falsch antworten; „nicht prüfbar" ist die richtige Antwort auf
|
||||||
|
eine Frage, die man gerade nicht beantworten kann.
|
||||||
|
*/
|
||||||
|
async function zutritt() {
|
||||||
|
const k = await konfig();
|
||||||
|
|
||||||
|
if (!k.ok || k.zutrittGelesen !== true || !k.zutritt) return null;
|
||||||
|
|
||||||
|
return k.zutritt;
|
||||||
|
}
|
||||||
|
|
||||||
const GRUENDE = {
|
const GRUENDE = {
|
||||||
nicht_eingerichtet:
|
nicht_eingerichtet:
|
||||||
'Der Bot kennt den Spielserver nicht — `SPIELSERVER_URL` und `SPIELSERVER_TOKEN` fehlen.',
|
'Der Bot kennt das Adminpanel nicht — `ACP_URL` und `ACP_BOT_TOKEN` fehlen.',
|
||||||
spielserver_nicht_erreichbar: 'Der Spielserver antwortet gerade nicht.',
|
panel_nicht_erreichbar: 'Das Adminpanel antwortet gerade nicht.',
|
||||||
zeitueberschreitung: 'Der Spielserver hat zu lange gebraucht.',
|
zeitueberschreitung: 'Das Panel hat zu lange gebraucht.',
|
||||||
abgewiesen_oder_unbekannter_weg: 'Der Spielserver weist den Bot ab — stimmt das Geheimnis?',
|
abgewiesen_oder_unbekannter_weg: 'Das Panel weist den Bot ab — stimmt der Schlüssel?',
|
||||||
rechtesystem_stumm: 'Das Rechtesystem auf dem Spielserver antwortet nicht.',
|
rechtesystem_stumm: 'Das Rechtesystem auf dem Spielserver antwortet nicht.',
|
||||||
recht_nicht_lesbar: 'Die Rechteprüfung war nicht lesbar.',
|
recht_nicht_lesbar: 'Die Rechteprüfung war nicht lesbar.',
|
||||||
};
|
};
|
||||||
@@ -51,6 +90,26 @@ export const data = new SlashCommandBuilder()
|
|||||||
.addUserOption((o) =>
|
.addUserOption((o) =>
|
||||||
o.setName('person').setDescription('Wen nachschlagen?').setRequired(true)
|
o.setName('person').setDescription('Wen nachschlagen?').setRequired(true)
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
.addSubcommand((s) =>
|
||||||
|
s.setName('add')
|
||||||
|
.setDescription('Jemanden freischalten')
|
||||||
|
.addUserOption((o) =>
|
||||||
|
o.setName('person').setDescription('Wen freischalten?').setRequired(true)
|
||||||
|
)
|
||||||
|
.addStringOption((o) =>
|
||||||
|
o.setName('grund').setDescription('Warum? Steht später im Log.').setRequired(false)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.addSubcommand((s) =>
|
||||||
|
s.setName('remove')
|
||||||
|
.setDescription('Freischaltung zurücknehmen')
|
||||||
|
.addUserOption((o) =>
|
||||||
|
o.setName('person').setDescription('Wem?').setRequired(true)
|
||||||
|
)
|
||||||
|
.addStringOption((o) =>
|
||||||
|
o.setName('grund').setDescription('Warum? Steht später im Log.').setRequired(false)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
export async function execute(interaction) {
|
export async function execute(interaction) {
|
||||||
@@ -58,7 +117,7 @@ export async function execute(interaction) {
|
|||||||
// die gehören nicht in einen offenen Kanal.
|
// die gehören nicht in einen offenen Kanal.
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||||
|
|
||||||
if (!spielserverBereit()) {
|
if (!panelBereit()) {
|
||||||
return interaction.editReply(GRUENDE.nicht_eingerichtet);
|
return interaction.editReply(GRUENDE.nicht_eingerichtet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +145,12 @@ export async function execute(interaction) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ziel = interaction.options.getUser('person', true);
|
const ziel = interaction.options.getUser('person', true);
|
||||||
|
const was = interaction.options.getSubcommand();
|
||||||
|
|
||||||
|
if (was === 'add' || was === 'remove') {
|
||||||
|
return schreiben(interaction, ziel, was, erlaubnis.rolle);
|
||||||
|
}
|
||||||
|
|
||||||
const p = await person(ziel.id);
|
const p = await person(ziel.id);
|
||||||
|
|
||||||
if (!p.ok) {
|
if (!p.ok) {
|
||||||
@@ -99,18 +164,31 @@ export async function execute(interaction) {
|
|||||||
Verbindung. Wer sie hier vom Server erwartet, bekommt eine Antwort, die
|
Verbindung. Wer sie hier vom Server erwartet, bekommt eine Antwort, die
|
||||||
geraten wäre.
|
geraten wäre.
|
||||||
*/
|
*/
|
||||||
let rolleText = 'nicht prüfbar — `WHITELIST_ROLE_ID` ist nicht gesetzt';
|
const z = await zutritt();
|
||||||
|
|
||||||
if (WHITELIST_ROLLE) {
|
let rolleText = 'nicht prüfbar — der Spielserver sagt gerade nicht, welche Rollen gelten';
|
||||||
|
|
||||||
|
if (z && z.rollen.length === 0) {
|
||||||
|
rolleText = 'keine Zutrittsrolle konfiguriert — die Whitelist ist offen';
|
||||||
|
} else if (z) {
|
||||||
const mitglied = await interaction.guild?.members
|
const mitglied = await interaction.guild?.members
|
||||||
.fetch(ziel.id)
|
.fetch(ziel.id)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
|
|
||||||
rolleText = !mitglied
|
if (!mitglied) {
|
||||||
? 'nicht auf diesem Discord'
|
rolleText = 'nicht auf diesem Discord';
|
||||||
: mitglied.roles.cache.has(WHITELIST_ROLLE)
|
} else {
|
||||||
? '**ja**'
|
const hat = z.rollen.filter((r) => mitglied.roles.cache.has(r));
|
||||||
: '**nein**';
|
const gesperrt = (z.sperrrollen ?? []).some((r) => mitglied.roles.cache.has(r));
|
||||||
|
|
||||||
|
// DIE SPERRE ZUERST: sie schlägt die Zutrittsrolle. Sie danach zu
|
||||||
|
// nennen ließe „ja" stehen für jemanden, der nicht hereinkommt.
|
||||||
|
rolleText = gesperrt
|
||||||
|
? '**gesperrt** — eine Sperrrolle schlägt jede Zutrittsrolle'
|
||||||
|
: hat.length > 0
|
||||||
|
? `**ja** (${hat.length} von ${z.rollen.length} Zutrittsrollen)`
|
||||||
|
: `**nein** (keine von ${z.rollen.length})`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const k = p.konto;
|
const k = p.konto;
|
||||||
@@ -141,3 +219,120 @@ export async function execute(interaction) {
|
|||||||
|
|
||||||
return interaction.editReply(zeilen.filter(Boolean).join('\n'));
|
return interaction.editReply(zeilen.filter(Boolean).join('\n'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Freischalten oder zurücknehmen.
|
||||||
|
*
|
||||||
|
* SIEBEN PRÜFUNGEN, BEVOR ETWAS PASSIERT. Jede davon hat einen eigenen Satz —
|
||||||
|
* „geht nicht" ist keine Auskunft, mit der jemand weiterkommt.
|
||||||
|
*/
|
||||||
|
async function schreiben(interaction, ziel, was, aufruferRolle) {
|
||||||
|
const z = await zutritt();
|
||||||
|
|
||||||
|
if (!z) {
|
||||||
|
return interaction.editReply(
|
||||||
|
'Der Spielserver sagt gerade nicht, welche Rollen gelten — ich schalte nichts frei, was ich nicht prüfen kann.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!z.vergaberolle) {
|
||||||
|
return interaction.editReply(
|
||||||
|
'Es ist keine Rolle zum Vergeben eingetragen (`d4rk_discord:grantrole` in `secrets.cfg`). Bis dahin kann ich niemanden freischalten.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
DIE ROLLE MUSS AUCH ZUTRITT OEFFNEN.
|
||||||
|
|
||||||
|
Sonst vergibt der Befehl etwas Wirkungsloses und meldet Erfolg — die
|
||||||
|
unangenehmste Sorte Fehler, weil sie erst auffaellt, wenn jemand vor der
|
||||||
|
Tuer steht.
|
||||||
|
*/
|
||||||
|
if (!z.vergabeGueltig) {
|
||||||
|
return interaction.editReply(
|
||||||
|
`Die eingetragene Vergaberolle (\`${z.vergaberolle}\`) ist **keine** der Rollen, die Zutritt öffnen. ` +
|
||||||
|
'Ich vergebe sie nicht — sie würde niemanden hereinlassen.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mitglied = await interaction.guild?.members.fetch(ziel.id).catch(() => null);
|
||||||
|
|
||||||
|
if (!mitglied) {
|
||||||
|
return interaction.editReply(`${ziel.tag} ist nicht auf diesem Discord.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
KANN ICH DIE ROLLE UEBERHAUPT VERGEBEN?
|
||||||
|
|
||||||
|
Discord laesst einen Bot nur Rollen UNTERHALB seiner eigenen hoechsten
|
||||||
|
vergeben. Ohne diese Pruefung kommt eine nackte HTTP-50013, und die liest
|
||||||
|
sich wie „der Bot ist kaputt" statt wie „die Rolle steht zu weit oben".
|
||||||
|
*/
|
||||||
|
const rolle = interaction.guild.roles.cache.get(z.vergaberolle)
|
||||||
|
?? await interaction.guild.roles.fetch(z.vergaberolle).catch(() => null);
|
||||||
|
|
||||||
|
if (!rolle) {
|
||||||
|
return interaction.editReply(
|
||||||
|
`Die Rolle \`${z.vergaberolle}\` gibt es auf diesem Discord nicht. Steht in \`secrets.cfg\` die richtige ID?`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ich = interaction.guild.members.me;
|
||||||
|
|
||||||
|
if (!ich?.permissions.has('ManageRoles')) {
|
||||||
|
return interaction.editReply('Mir fehlt das Recht **Rollen verwalten** auf diesem Discord.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rolle.position >= ich.roles.highest.position) {
|
||||||
|
return interaction.editReply(
|
||||||
|
`**${rolle.name}** steht in der Rollenliste über meiner höchsten Rolle — Discord lässt mich sie deshalb nicht vergeben. ` +
|
||||||
|
'Schieb meine Rolle darüber.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hatte = mitglied.roles.cache.has(rolle.id);
|
||||||
|
|
||||||
|
// NICHTS ZU TUN IST EINE ANTWORT, KEIN FEHLSCHLAG.
|
||||||
|
if (was === 'add' && hatte) {
|
||||||
|
return interaction.editReply(`${ziel.tag} hat **${rolle.name}** bereits.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (was === 'remove' && !hatte) {
|
||||||
|
return interaction.editReply(`${ziel.tag} hat **${rolle.name}** gar nicht.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const grund = interaction.options.getString('grund') || null;
|
||||||
|
const spur = `${interaction.user.tag} (${aufruferRolle})${grund ? ` — ${grund}` : ''}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (was === 'add') await mitglied.roles.add(rolle, spur);
|
||||||
|
else await mitglied.roles.remove(rolle, spur);
|
||||||
|
} catch (fehler) {
|
||||||
|
return interaction.editReply(
|
||||||
|
`Discord hat es abgelehnt: ${fehler?.message ?? 'unbekannt'}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ERST HANDELN, DANN MELDEN - und die Meldung darf nicht mitreissen.
|
||||||
|
|
||||||
|
Ein fehlgeschlagener Logeintrag waere ein schlechter Grund, eine bereits
|
||||||
|
vergebene Rolle als Fehler darzustellen.
|
||||||
|
*/
|
||||||
|
await inDevlog(interaction.client, {
|
||||||
|
title: was === 'add' ? 'Whitelist: freigeschaltet' : 'Whitelist: zurückgenommen',
|
||||||
|
description:
|
||||||
|
`**${ziel.tag}** (\`${ziel.id}\`)\n` +
|
||||||
|
`Rolle: **${rolle.name}**\n` +
|
||||||
|
`Durch: **${interaction.user.tag}** (${aufruferRolle})` +
|
||||||
|
(grund ? `\nGrund: ${grund}` : ''),
|
||||||
|
color: was === 'add' ? 0x3fb950 : 0xd29922,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return interaction.editReply(
|
||||||
|
was === 'add'
|
||||||
|
? `${ziel.tag} hat jetzt **${rolle.name}** und kommt auf den Server.`
|
||||||
|
: `${ziel.tag} hat **${rolle.name}** nicht mehr.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,3 +55,35 @@ export async function dmAdminEinmalig(client, schluessel, zustand, embedBauen =
|
|||||||
setSetting(merker, zustand);
|
setSetting(merker, zustand);
|
||||||
return dmAdmin(client, embedBauen());
|
return dmAdmin(client, embedBauen());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein Embed in den Devlog-Kanal.
|
||||||
|
*
|
||||||
|
* WARUM NICHT ALS DM: eine DM erreicht genau eine Person. Was das Team tut,
|
||||||
|
* gehört dorthin, wo das Team es sieht — sonst ist es protokolliert und
|
||||||
|
* trotzdem unsichtbar.
|
||||||
|
*
|
||||||
|
* WIRFT NIE. Ein fehlgeschlagener Logeintrag darf den Vorgang nicht
|
||||||
|
* mitreißen, über den er berichtet — dieselbe Regel wie bei `dmAdmin`. Der
|
||||||
|
* Rückgabewert sagt, ob es geklappt hat; wer es wissen muss, fragt ihn.
|
||||||
|
*
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export async function inDevlog(client, embed) {
|
||||||
|
if (!config.devlogChannelId || !client) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const kanal = await client.channels.fetch(config.devlogChannelId).catch(() => null);
|
||||||
|
|
||||||
|
// Kein Textkanal ist kein Absturz: der Kanal kann gelöscht oder in
|
||||||
|
// eine Kategorie umgewandelt worden sein, und der Bot soll dann
|
||||||
|
// weiterarbeiten statt zu scheitern.
|
||||||
|
if (!kanal?.isTextBased?.()) return false;
|
||||||
|
|
||||||
|
await kanal.send({ embeds: [embed] });
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
// 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. Genau daran ist die erste Fassung gescheitert — am 04.09.2026 an
|
||||||
|
* der Serverkonsole gemessen:
|
||||||
|
*
|
||||||
|
* d4rk_discord:role = "1439019849664696460,1544029550802108437"
|
||||||
|
*
|
||||||
|
* ZWEI Rollen; eingetragen war eine. Wer nur die andere hatte, kam auf den
|
||||||
|
* Server und haette hier trotzdem „Whitelist-Rolle: nein" gelesen.
|
||||||
|
*
|
||||||
|
* Der Wert kommt deshalb aus der Convar des Spielservers, durchgereicht vom
|
||||||
|
* Panel — dieselbe Quelle, die auch ueber den Zutritt entscheidet.
|
||||||
|
*
|
||||||
|
* 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