Enhance purge command with additional filtering options

- Added user, bots, links, attachments, and regex options to the purge command for more granular message deletion.
- Updated the command execution logic to filter messages based on the new options before bulk deletion.
- Modified CreateCaseInput type to use Prisma.InputJsonValue for metadata.
This commit is contained in:
smueller
2026-07-22 11:18:33 +02:00
parent dd3b132f73
commit b0e3409793
4 changed files with 77 additions and 3 deletions

View File

@@ -356,13 +356,49 @@ const purgeCommand: SlashCommand = {
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
.setName('purge') .setName('purge')
.setDescription('Bulk delete messages') .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) { async execute(interaction, context) {
if (!(await ensureMod(interaction, PermissionFlagsBits.ManageMessages))) return; if (!(await ensureMod(interaction, PermissionFlagsBits.ManageMessages))) return;
const amount = interaction.options.getInteger('amount', true); 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; let deletedCount = 0;
if (interaction.channel instanceof TextChannel) { 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; deletedCount = deleted.size;
} }
await createCase(context, { await createCase(context, {

View File

@@ -1,6 +1,7 @@
import { moderationQueue } from '../../jobs.js'; import { moderationQueue } from '../../jobs.js';
import type { BotContext } from '../../types.js'; import type { BotContext } from '../../types.js';
import { PermissionFlagsBits, type ChatInputCommandInteraction } from 'discord.js'; import { PermissionFlagsBits, type ChatInputCommandInteraction } from 'discord.js';
import type { Prisma } from '@prisma/client';
type CreateCaseInput = { type CreateCaseInput = {
guildId: string; guildId: string;
@@ -8,7 +9,7 @@ type CreateCaseInput = {
moderatorId: string; moderatorId: string;
action: string; action: string;
reason?: string; reason?: string;
metadata?: Record<string, unknown>; metadata?: Prisma.InputJsonValue;
}; };
export async function createCase(context: BotContext, input: CreateCaseInput) { export async function createCase(context: BotContext, input: CreateCaseInput) {

BIN
docs/logo/nexumi-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

37
docs/logo/nexumi-logo.svg Normal file
View File

@@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-label="Nexumi logo">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#181C2B"/>
<stop offset="1" stop-color="#0C0E15"/>
</linearGradient>
<linearGradient id="mark" x1="0" y1="1" x2="1" y2="0">
<stop offset="0" stop-color="#6366F1"/>
<stop offset="1" stop-color="#8B5CF6"/>
</linearGradient>
</defs>
<!-- App-Icon-Grundflaeche -->
<rect width="512" height="512" rx="116" fill="url(#bg)"/>
<rect x="1.5" y="1.5" width="509" height="509" rx="114.5" fill="none" stroke="#FFFFFF" stroke-opacity="0.06" stroke-width="3"/>
<!-- Glow: gestaffelte Deckkraft statt Filter, rendersicher -->
<g fill="none" stroke="url(#mark)" stroke-linecap="round" stroke-linejoin="round">
<path d="M170 362 V150 L342 362 V150" stroke-width="78" stroke-opacity="0.10"/>
<path d="M170 362 V150 L342 362 V150" stroke-width="52" stroke-opacity="0.16"/>
</g>
<!-- Kanten des N -->
<path d="M170 362 V150 L342 362 V150" fill="none" stroke="url(#mark)"
stroke-width="30" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Knoten -->
<g fill="url(#mark)">
<circle cx="170" cy="150" r="34"/>
<circle cx="170" cy="362" r="34"/>
<circle cx="342" cy="362" r="34"/>
<circle cx="342" cy="150" r="34"/>
</g>
<!-- Aktiver Knoten oben rechts -->
<circle cx="342" cy="150" r="14" fill="#C7D2FE"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB