Author SHA1 Message Date
D4rkst3randClaude Opus 5 00e4774a5f feat(whitelist): /whitelist add und remove
Deploy / deploy (push) Canceled after 0s
Deploy / check (push) Canceled after 0s
Der lesende Weg ist am 04.09.2026 mit echten Daten durchgelaufen
("ja (2 von 2 Zutrittsrollen)"), also kommt jetzt die schreibende
Haelfte - in dieser Reihenfolge und nicht umgekehrt.

Welche Rolle vergeben wird
--------------------------
d4rk_discord:grantrole, eine EIGENE Convar in secrets.cfg. Nicht das
Adminpanel: eine Rollen-ID, die man an einer zweiten Stelle pflegt,
laeuft von der ersten weg - genau daran ist heute frueh schon das
Eingabefeld gescheitert. Die Rollenpolitik liegt vollstaendig in
secrets.cfg.

SIE MUSS EINE DER ZUTRITTSROLLEN SEIN. d4rk_lib prueft das und meldet
`vergabeGueltig`; ist sie es nicht, verweigert der Befehl. Eine Rolle
zu vergeben, die niemanden hereinlaesst, waere ein Knopf, der nichts
tut und dabei "erledigt" meldet - und das faellt erst auf, wenn jemand
vor der Tuer steht.

NICHT GESETZT IST KEIN FEHLER, sondern der Zustand im geschlossenen
Test. Der Befehl sagt das dann auch so.

Was geprueft wird, bevor etwas passiert
---------------------------------------
Rechtesystem erreichbar - Aufrufer hat d4rk.whitelist - Rollen lesbar -
Vergaberolle gesetzt - Vergaberolle oeffnet Zutritt - Person ist auf
dem Discord - Rolle existiert - Bot hat "Rollen verwalten" - Rolle
steht unter der hoechsten Rolle des Bots.

Die Hierarchiepruefung ist keine Kosmetik: ohne sie kommt eine nackte
HTTP-50013, und die liest sich wie "der Bot ist kaputt" statt wie "die
Rolle steht zu weit oben".

"Hat sie schon" und "hat sie gar nicht" sind Antworten, keine
Fehlschlaege.

Wo es landet
------------
Im Devlog-Kanal (neuer Helfer melden.js:inDevlog, wirft nie - ein
fehlgeschlagener Logeintrag darf den Vorgang nicht mitreissen, ueber
den er berichtet). NICHT im Serverprotokoll: der Weg dorthin ginge nur
ueber einen Schreibzugang fuer den Bot, und der wuerde die absichtlich
schmale Tuer aufmachen. Die Freischaltung steht ausserdem in Discords
Audit-Log, und beim Verbinden protokolliert d4rk_lib die Zulassung mit
Namen.

Gemessen
--------
Befehlsdefinition gebaut: whitelist status(person*) add(person*,grund)
remove(person*,grund), keine Ueberschreitung der Discord-Grenzen. Die
Grenzpruefung hat eine Kontrolle mit bekanntem Ausgang, die anschlaegt -
sonst waere "nichts ueber der Grenze" von "Pruefung ist blind" nicht zu
unterscheiden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 12:23:23 +02:00
2 changed files with 200 additions and 6 deletions
+168 -6
View File
@@ -14,17 +14,36 @@
// 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, panelBereit, darfWhitelist, konfig } from '../../panel.js'; import { person, panelBereit, darfWhitelist, konfig } from '../../panel.js';
import { inDevlog } from '../../melden.js';
/* /*
DIE ROLLEN WERDEN GELESEN, NICHT GEPFLEGT. DIE ROLLEN WERDEN GELESEN, NICHT GEPFLEGT.
@@ -71,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) {
@@ -106,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) {
@@ -174,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.`
);
}
+32
View File
@@ -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;
}
}