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.'); +}