diff --git a/apps/bot/src/modules/moderation/commands.ts b/apps/bot/src/modules/moderation/commands.ts index 0c204d3..b56e8de 100644 --- a/apps/bot/src/modules/moderation/commands.ts +++ b/apps/bot/src/modules/moderation/commands.ts @@ -356,13 +356,49 @@ const purgeCommand: SlashCommand = { data: new SlashCommandBuilder() .setName('purge') .setDescription('Bulk delete messages') - .addIntegerOption((o) => o.setName('amount').setDescription('2-100').setRequired(true).setMinValue(2).setMaxValue(100)), + .addIntegerOption((o) => + o.setName('amount').setDescription('2-100').setRequired(true).setMinValue(2).setMaxValue(100) + ) + .addUserOption((o) => o.setName('user').setDescription('Only purge this user messages')) + .addBooleanOption((o) => o.setName('bots').setDescription('Only purge bot messages')) + .addBooleanOption((o) => o.setName('links').setDescription('Only purge messages containing links')) + .addBooleanOption((o) => + o.setName('attachments').setDescription('Only purge messages with attachments') + ) + .addStringOption((o) => + o.setName('regex').setDescription('Only purge messages matching regex') + ), async execute(interaction, context) { if (!(await ensureMod(interaction, PermissionFlagsBits.ManageMessages))) return; const amount = interaction.options.getInteger('amount', true); + const targetUser = interaction.options.getUser('user'); + const botsOnly = interaction.options.getBoolean('bots') ?? false; + 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'); + } catch { + await interaction.reply({ content: 'Invalid regex pattern.', ephemeral: true }); + return; + } + } + let deletedCount = 0; if (interaction.channel instanceof TextChannel) { - const deleted = await interaction.channel.bulkDelete(amount, true); + 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; } await createCase(context, { diff --git a/apps/bot/src/modules/moderation/service.ts b/apps/bot/src/modules/moderation/service.ts index ad3d743..fdf7e23 100644 --- a/apps/bot/src/modules/moderation/service.ts +++ b/apps/bot/src/modules/moderation/service.ts @@ -1,6 +1,7 @@ import { moderationQueue } from '../../jobs.js'; import type { BotContext } from '../../types.js'; import { PermissionFlagsBits, type ChatInputCommandInteraction } from 'discord.js'; +import type { Prisma } from '@prisma/client'; type CreateCaseInput = { guildId: string; @@ -8,7 +9,7 @@ type CreateCaseInput = { moderatorId: string; action: string; reason?: string; - metadata?: Record; + metadata?: Prisma.InputJsonValue; }; export async function createCase(context: BotContext, input: CreateCaseInput) { diff --git a/docs/logo/nexumi-logo.png b/docs/logo/nexumi-logo.png new file mode 100644 index 0000000..4756961 Binary files /dev/null and b/docs/logo/nexumi-logo.png differ diff --git a/docs/logo/nexumi-logo.svg b/docs/logo/nexumi-logo.svg new file mode 100644 index 0000000..210c5a6 --- /dev/null +++ b/docs/logo/nexumi-logo.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +