Implement confirmation flow for destructive moderation actions

- Added confirmation prompts for destructive commands such as `/ban`, `/purge`, and `/case delete` to enhance user safety.
- Integrated locale support for confirmation messages, ensuring clarity in both German and English.
- Refactored command logic to utilize the new confirmation system, improving user experience and reducing accidental actions.
This commit is contained in:
smueller
2026-07-22 11:26:49 +02:00
parent a17d161c06
commit 878478e4fc
6 changed files with 402 additions and 51 deletions

View File

@@ -9,9 +9,10 @@ import {
import { t, tf } from '@nexumi/shared';
import type { SlashCommand } from '../../types.js';
import { requirePermission } from '../../permissions.js';
import { applyWarnEscalation, createCase, scheduleTempBanExpire } from './service.js';
import { applyWarnEscalation, createCase } from './service.js';
import { parseDuration } from './duration.js';
import { getGuildLocale } from '../../i18n.js';
import { promptDestructiveConfirmation } from './confirmations.js';
function reasonFrom(i: ChatInputCommandInteraction, locale: 'de' | 'en'): string {
return i.options.getString('reason') ?? t(locale, 'moderation.reason.none');
@@ -41,19 +42,15 @@ const banCommand: SlashCommand = {
if (!(await ensureMod(interaction, PermissionFlagsBits.BanMembers, locale))) return;
const user = interaction.options.getUser('user', true);
const reason = reasonFrom(interaction, locale);
await interaction.guild!.members.ban(user.id, { reason });
await createCase(context, {
guildId: interaction.guildId!,
targetUserId: user.id,
moderatorId: interaction.user.id,
action: 'BAN',
reason
});
const duration = interaction.options.getString('duration');
if (duration) {
await scheduleTempBanExpire(interaction.guildId!, user.id, parseDuration(duration), reason);
}
await interaction.reply({ content: t(locale, 'moderation.success'), ephemeral: true });
await promptDestructiveConfirmation(interaction, locale, {
action: 'ban',
payload: {
userId: user.id,
reason,
duration
}
});
}
};
@@ -404,39 +401,32 @@ const purgeCommand: SlashCommand = {
const linksOnly = interaction.options.getBoolean('links') ?? false;
const attachmentsOnly = interaction.options.getBoolean('attachments') ?? false;
const regexInput = interaction.options.getString('regex');
let regex: RegExp | null = null;
if (regexInput) {
try {
regex = new RegExp(regexInput, 'i');
new RegExp(regexInput, 'i');
} catch {
await interaction.reply({ content: t(locale, 'generic.invalidRegex'), ephemeral: true });
return;
}
}
let deletedCount = 0;
if (interaction.channel instanceof TextChannel) {
const fetched = await interaction.channel.messages.fetch({ limit: 100 });
const filtered = fetched.filter((message) => {
if (targetUser && message.author.id !== targetUser.id) return false;
if (botsOnly && !message.author.bot) return false;
if (linksOnly && !/(https?:\/\/|discord\.gg\/)/i.test(message.content)) return false;
if (attachmentsOnly && message.attachments.size === 0) return false;
if (regex && !regex.test(message.content)) return false;
return true;
});
const toDelete = filtered.first(amount);
const deleted = await interaction.channel.bulkDelete(toDelete, true);
deletedCount = deleted.size;
if (!(interaction.channel instanceof TextChannel)) {
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
return;
}
await createCase(context, {
guildId: interaction.guildId!,
targetUserId: interaction.user.id,
moderatorId: interaction.user.id,
action: 'PURGE',
reason: `Deleted ${deletedCount} messages`
await promptDestructiveConfirmation(interaction, locale, {
action: 'purge',
payload: {
channelId: interaction.channel.id,
amount,
targetUserId: targetUser?.id ?? null,
botsOnly,
linksOnly,
attachmentsOnly,
regex: regexInput
}
});
await interaction.reply({ content: t(locale, 'moderation.success'), ephemeral: true });
}
};
@@ -593,11 +583,10 @@ const caseCommand: SlashCommand = {
await interaction.reply({ content: t(locale, 'moderation.success'), ephemeral: true });
return;
}
await context.prisma.case.update({
where: { guildId_caseNumber: { guildId: interaction.guildId!, caseNumber } },
data: { deletedAt: new Date() }
await promptDestructiveConfirmation(interaction, locale, {
action: 'case_delete',
payload: { caseNumber }
});
await interaction.reply({ content: t(locale, 'moderation.success'), ephemeral: true });
}
};