From 91397d8a44a0a5ed49ceb32318bf65178aa25a97 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Sat, 1 Aug 2026 04:32:39 +0200 Subject: [PATCH] Raid-Sperre von Hand setzen, aufheben oder nachsehen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der fehlende Befehl zum Raid-Schutz. Drei Unterbefehle: status ist gerade gesperrt, und wie lange noch an jetzt sperren — fuer die Welle, die man kommen sieht aus sofort aufheben — fuer den Fehlalarm um drei Uhr nachts Rechte: ManageGuild, weil die Sperre genau das anfasst — Verifizierung und Invites. Antworten sind ephemeral. Die Sperr-Massnahmen sind in dichtmachen() herausgezogen, damit automatisch und von Hand denselben Weg nehmen und sich nicht auseinanderentwickeln. Die Meldung sagt jeweils dazu, wodurch sie ausgeloest wurde und wer es war. Ist der Raid-Schutz ausgeschaltet, verweist der Befehl aufs Panel statt stillschweigend nichts zu tun. Dazu: der Befehl steht auf der oeffentlichen Befehls-Seite, in beiden Sprachen, und die Zahl auf der Produktseite geht von 16 auf 17. Geprueft: 17 Befehle laden, keine doppelten Namen, Rechte-Bitmaske 32 (ManageGuild), alle neuen Exporte vorhanden, kein Sprachschluessel fehlt. Co-Authored-By: Claude Fable 5 --- frontend/src/locales/de.js | 1 + frontend/src/locales/en.js | 1 + frontend/src/pages/BotCommands.jsx | 1 + frontend/src/pages/BotLanding.jsx | 2 +- src/bot/anti-raid.js | 56 ++++++++++++++++++++++------ src/bot/client.js | 2 + src/bot/commands/raid.js | 60 ++++++++++++++++++++++++++++++ 7 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 src/bot/commands/raid.js diff --git a/frontend/src/locales/de.js b/frontend/src/locales/de.js index c9f421b..2e933a7 100644 --- a/frontend/src/locales/de.js +++ b/frontend/src/locales/de.js @@ -253,6 +253,7 @@ export default { 'commands.warns': 'Zeigt die Verwarnungs-Historie.', 'commands.timeout': 'Setzt eine Auszeit.', 'commands.purge': 'Löscht mehrere Nachrichten auf einmal.', + 'commands.raid': 'Zeigt die Raid-Sperre an, setzt sie von Hand oder hebt sie sofort auf.', 'commands.giveaway': 'Startet eine Verlosung mit Teilnahme-Knopf und automatischer Ziehung.', 'commands.ticketSetup': 'Postet den Knopf, über den Support-Tickets als private Threads entstehen.', 'commands.playtesterSetup': 'Postet den Bewerbungs-Knopf für das Playtester-Programm.', diff --git a/frontend/src/locales/en.js b/frontend/src/locales/en.js index 4f131ab..a27a8d4 100644 --- a/frontend/src/locales/en.js +++ b/frontend/src/locales/en.js @@ -336,6 +336,7 @@ export default { 'commands.warns': 'Shows the warning history.', 'commands.timeout': 'Puts someone in timeout.', 'commands.purge': 'Deletes several messages at once.', + 'commands.raid': 'Shows the raid lockdown, sets it by hand or lifts it right away.', 'commands.giveaway': 'Starts a giveaway with an entry button and an automatic draw.', 'commands.ticketSetup': 'Posts the button that turns support tickets into private threads.', 'commands.playtesterSetup': 'Posts the application button for the playtester programme.', diff --git a/frontend/src/pages/BotCommands.jsx b/frontend/src/pages/BotCommands.jsx index 63a219a..9c09c9d 100644 --- a/frontend/src/pages/BotCommands.jsx +++ b/frontend/src/pages/BotCommands.jsx @@ -22,6 +22,7 @@ const GROUPS = [ ['/purge anzahl:…', 'commands.purge'], ]], ['commands.g.setup', true, [ + ['/raid status | an | aus', 'commands.raid'], ['/giveaway preis:… dauer:… gewinner:…', 'commands.giveaway'], ['/ticket-setup', 'commands.ticketSetup'], ['/playtester-setup', 'commands.playtesterSetup'], diff --git a/frontend/src/pages/BotLanding.jsx b/frontend/src/pages/BotLanding.jsx index 1092efa..ee9fe15 100644 --- a/frontend/src/pages/BotLanding.jsx +++ b/frontend/src/pages/BotLanding.jsx @@ -67,7 +67,7 @@ export default function BotLanding() { )}
- 16 + 17 {t('bot.statCommands')}
diff --git a/src/bot/anti-raid.js b/src/bot/anti-raid.js index 5cc861d..0c68a51 100644 --- a/src/bot/anti-raid.js +++ b/src/bot/anti-raid.js @@ -50,18 +50,26 @@ async function melden(client, titel, text) { } export function istGesperrt() { - const bis = getSetting(BIS); - return Boolean(bis) && new Date(bis).getTime() > jetzt(); + return sperrStand().gesperrt; } -/** Server dichtmachen. Gibt zurück, was tatsächlich griff. */ -async function sperren(client, guild, anzahl, fenster) { +/** Läuft gerade eine Sperre, und bis wann? */ +export function sperrStand() { + const bis = getSetting(BIS); + const ms = bis ? new Date(bis).getTime() : 0; + return { gesperrt: ms > jetzt(), bis: ms || null }; +} + +/** + * Die eigentlichen Maßnahmen. Von Hand und automatisch nehmen denselben Weg, + * damit sich die beiden nicht auseinanderentwickeln. + * @returns {Promise<{getan: string[], minuten: number}>} + */ +async function dichtmachen(client, guild, grund) { const minuten = tuning('raid_lockdown'); const vorher = guild.verificationLevel; const getan = []; - const grund = `Anti-Raid: ${anzahl} Beitritte in ${fenster} s`; - if (vorher < GuildVerificationLevel.High) { const ok = await guild.setVerificationLevel(GuildVerificationLevel.High, grund) .then(() => true) @@ -81,20 +89,45 @@ async function sperren(client, guild, anzahl, fenster) { setSetting(VORHER, String(vorher)); setSetting(BIS, new Date(jetzt() + minuten * 60000).toISOString()); planeAufhebung(client, minuten * 60000); + return { getan, minuten }; +} + +/** Automatisch, weil das Beitritts-Fenster übergelaufen ist */ +async function sperren(client, guild, anzahl, fenster) { + const { getan, minuten } = await dichtmachen(client, guild, `Anti-Raid: ${anzahl} Beitritte in ${fenster} s`); await melden( client, '🚨 Raid-Verdacht — Server gesperrt', `**${anzahl} Beitritte in ${fenster} Sekunden.**\n\n` + (getan.length ? `${getan.map((g) => `• ${g}`).join('\n')}\n\n` : '_Keine Maßnahme griff — fehlen dem Bot Rechte?_\n\n') - + `Wird in **${minuten} Minuten** automatisch aufgehoben.\n` + + `Wird in **${minuten} Minuten** automatisch aufgehoben, ` + + 'vorher mit `/raid aus`.\n' + 'Es wurde niemand gekickt oder gebannt.' ); console.warn(`[antiraid] Sperre aktiv: ${anzahl} Beitritte in ${fenster}s`); } -/** Sperre zurücknehmen — auch beim Start, falls über einen Neustart hinweg */ -export async function entsperren(client, { melde = true } = {}) { +/** Von Hand über /raid an — für die Welle, die man kommen sieht */ +export async function sperrenVonHand(client, guild, durch) { + const { getan, minuten } = await dichtmachen(client, guild, `Raid-Sperre von Hand durch ${durch}`); + + await melden( + client, + '🔒 Server von Hand gesperrt', + `Ausgelöst von **${durch}**.\n\n` + + (getan.length ? `${getan.map((g) => `• ${g}`).join('\n')}\n\n` : '_Keine Maßnahme griff — fehlen dem Bot Rechte?_\n\n') + + `Läuft nach **${minuten} Minuten** aus, vorher mit \`/raid aus\`.` + ); + console.warn(`[antiraid] Sperre von Hand durch ${durch}`); + return { getan, minuten }; +} + +/** + * Sperre zurücknehmen — vom Timer, von /raid aus, oder beim Start, wenn eine + * Sperre einen Neustart überlebt hat. + */ +export async function entsperren(client, { melde = true, durch = null } = {}) { if (!getSetting(BIS)) return false; const guild = client.guilds.cache.first(); @@ -120,9 +153,10 @@ export async function entsperren(client, { melde = true } = {}) { if (melde) { await melden(client, '✅ Sperre aufgehoben', - getan.length ? getan.map((g) => `• ${g}`).join('\n') : 'Nichts zurückzunehmen.'); + (durch ? `Von Hand durch **${durch}**.\n\n` : '') + + (getan.length ? getan.map((g) => `• ${g}`).join('\n') : 'Nichts zurückzunehmen.')); } - console.log('[antiraid] Sperre aufgehoben'); + console.log(`[antiraid] Sperre aufgehoben${durch ? ` (durch ${durch})` : ''}`); return true; } diff --git a/src/bot/client.js b/src/bot/client.js index 6eae950..c8ee40f 100644 --- a/src/bot/client.js +++ b/src/bot/client.js @@ -37,11 +37,13 @@ import * as rank from './commands/rank.js'; import * as tag from './commands/tag.js'; import * as remind from './commands/remind.js'; import * as geburtstag from './commands/geburtstag.js'; +import * as raid from './commands/raid.js'; // Alle Commands hier eintragen — jedes Modul exportiert { data, execute } const commandModules = [ ping, devlogBackfill, bug, playtesterSetup, galerieBackfill, wunsch, giveaway, ticketSetup, warn, warns, timeout, purge, rank, tag, remind, geburtstag, + raid, ]; export async function startBot() { diff --git a/src/bot/commands/raid.js b/src/bot/commands/raid.js new file mode 100644 index 0000000..3be95e3 --- /dev/null +++ b/src/bot/commands/raid.js @@ -0,0 +1,60 @@ +// /raid — Raid-Sperre von Hand setzen, aufheben oder nachsehen. +// +// Der Bot sperrt bei einer Beitritts-Welle selbst, aber zwei Fälle braucht +// man von Hand: der Fehlalarm um drei Uhr nachts, und die Welle, die man +// kommen sieht, bevor die Schwelle reißt. +import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; +import { sperrenVonHand, entsperren, sperrStand } from '../anti-raid.js'; +import { moduleEnabled } from '../../modules.js'; +import { tuning } from '../../tuning.js'; + +export const data = new SlashCommandBuilder() + .setName('raid') + .setDescription('Raid-Sperre setzen, aufheben oder Stand ansehen') + // ManageGuild, weil die Sperre genau das anfasst: Verifizierung und Invites + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild) + .addSubcommand((s) => s.setName('status').setDescription('Ist gerade gesperrt?')) + .addSubcommand((s) => s.setName('an').setDescription('Jetzt sperren')) + .addSubcommand((s) => s.setName('aus').setDescription('Sperre sofort aufheben')); + +export async function execute(interaction) { + const was = interaction.options.getSubcommand(); + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + if (!moduleEnabled('anti_raid')) { + await interaction.editReply( + '⚠️ Der Raid-Schutz ist ausgeschaltet. Einschalten im Panel unter **Support → Raid-Schutz**.' + ); + return; + } + + const stand = sperrStand(); + + if (was === 'status') { + await interaction.editReply(stand.gesperrt + ? `🔒 **Gesperrt** — läuft ab.` + : `🔓 **Offen.** Sperre greift ab **${tuning('raid_joins')} Beitritten** in ` + + `**${tuning('raid_window')} Sekunden**.`); + return; + } + + if (was === 'an') { + if (stand.gesperrt) { + await interaction.editReply(`🔒 Ist schon gesperrt — läuft ab.`); + return; + } + const erg = await sperrenVonHand(interaction.client, interaction.guild, interaction.user.username); + await interaction.editReply(erg.getan.length + ? `🔒 Gesperrt für **${erg.minuten} Minuten**:\n${erg.getan.map((g) => `• ${g}`).join('\n')}` + : '❌ Keine Maßnahme griff — fehlt dem Bot das Recht „Server verwalten"?'); + return; + } + + // aus + if (!stand.gesperrt) { + await interaction.editReply('🔓 Es ist gerade nichts gesperrt.'); + return; + } + await entsperren(interaction.client, { durch: interaction.user.username }); + await interaction.editReply('🔓 Sperre aufgehoben.'); +}