import { TextChannel, type ButtonInteraction } from 'discord.js'; import { type Locale, t, tf } from '@nexumi/shared'; import type { BotContext } from '../../types.js'; import { assertBannableForConfirm } from '../../hierarchy.js'; import { createCase, notifyModerationTargetDm, scheduleTempBanExpire } from './service.js'; import { tryParseDuration } from './duration.js'; type BanPayload = { userId: string; reason: string; duration: string | null; }; type PurgePayload = { channelId: string; amount: number; targetUserId: string | null; botsOnly: boolean; linksOnly: boolean; attachmentsOnly: boolean; regex: string | null; }; type CaseDeletePayload = { caseNumber: number; }; export async function executeBan( interaction: ButtonInteraction, context: BotContext, locale: Locale, payload: BanPayload ): Promise { const guild = interaction.guild; if (!guild) { await interaction.update({ content: t(locale, 'generic.guildOnly'), components: [] }); return; } if (!(await assertBannableForConfirm(interaction, locale, payload.userId))) { return; } let durationMs: number | null = null; if (payload.duration) { durationMs = tryParseDuration(payload.duration); if (durationMs === null || durationMs <= 0) { await interaction.update({ content: t(locale, 'moderation.duration.invalid'), components: [] }); return; } } await interaction.deferUpdate(); const targetUser = await context.client.users.fetch(payload.userId).catch(() => null); if (targetUser) { await notifyModerationTargetDm({ user: targetUser, guildName: guild.name, reason: payload.reason, locale, action: 'ban', duration: payload.duration }); } await guild.members.ban(payload.userId, { reason: payload.reason }); const modCase = await createCase(context, { guildId: guild.id, targetUserId: payload.userId, moderatorId: interaction.user.id, action: 'BAN', reason: payload.reason, channelId: interaction.channelId ?? undefined, source: 'button_confirmation', metadata: { confirmed: true, duration: payload.duration, durationMs } }); if (durationMs !== null) { await scheduleTempBanExpire(guild.id, payload.userId, durationMs, payload.reason); } await interaction.editReply({ content: tf(locale, 'moderation.success.caseWithReason', { caseNumber: modCase.caseNumber, reason: payload.reason }), components: [] }); } export async function executePurge( interaction: ButtonInteraction, context: BotContext, locale: Locale, payload: PurgePayload ): Promise { const guild = interaction.guild; if (!guild) { await interaction.update({ content: t(locale, 'generic.guildOnly'), components: [] }); return; } await interaction.deferUpdate(); const channel = await guild.channels.fetch(payload.channelId); let deletedCount = 0; const regex = payload.regex ? new RegExp(payload.regex, 'i') : null; if (channel instanceof TextChannel) { const fetched = await channel.messages.fetch({ limit: 100 }); const filtered = fetched.filter((message) => { if (payload.targetUserId && message.author.id !== payload.targetUserId) return false; if (payload.botsOnly && !message.author.bot) return false; if (payload.linksOnly && !/(https?:\/\/|discord\.gg\/)/i.test(message.content)) return false; if (payload.attachmentsOnly && message.attachments.size === 0) return false; if (regex && !regex.test(message.content)) return false; return true; }); const toDelete = filtered.first(payload.amount); const deleted = await channel.bulkDelete(toDelete, true); deletedCount = deleted.size; } const modCase = await createCase(context, { guildId: guild.id, targetUserId: interaction.user.id, moderatorId: interaction.user.id, action: 'PURGE', reason: `Deleted ${deletedCount} messages`, channelId: payload.channelId, source: 'button_confirmation', metadata: { confirmed: true, purge: { requestedAmount: payload.amount, deletedCount, filters: { targetUserId: payload.targetUserId, botsOnly: payload.botsOnly, linksOnly: payload.linksOnly, attachmentsOnly: payload.attachmentsOnly, regex: payload.regex } } } }); await interaction.editReply({ content: tf(locale, 'moderation.purge.done', { deletedCount, caseNumber: modCase.caseNumber }), components: [] }); } export async function executeCaseDelete( interaction: ButtonInteraction, context: BotContext, locale: Locale, payload: CaseDeletePayload ): Promise { const guildId = interaction.guildId; if (!guildId) { await interaction.update({ content: t(locale, 'generic.guildOnly'), components: [] }); return; } const existing = await context.prisma.case.findUnique({ where: { guildId_caseNumber: { guildId, caseNumber: payload.caseNumber } } }); if (!existing || existing.deletedAt) { await interaction.update({ content: t(locale, 'moderation.case.notFound'), components: [] }); return; } await context.prisma.case.update({ where: { guildId_caseNumber: { guildId, caseNumber: payload.caseNumber } }, data: { deletedAt: new Date() } }); const modCase = await createCase(context, { guildId, targetUserId: existing.targetUserId, moderatorId: interaction.user.id, action: 'CASE_DELETE', reason: `Soft-deleted case #${payload.caseNumber}`, channelId: interaction.channelId ?? undefined, source: 'button_confirmation', metadata: { confirmed: true, deletedCaseId: existing.id, deletedCaseNumber: payload.caseNumber } }); await interaction.update({ content: tf(locale, 'moderation.success.case', { caseNumber: modCase.caseNumber }), components: [] }); }