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:
@@ -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 });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user