Refactor moderation actions to include channel and source metadata
- Updated moderation action functions to include `channelId` and `source` in the metadata for better tracking of actions. - Introduced an `auditFrom` function to streamline the addition of audit information across various commands. - Removed `prisma/migrations` from .gitignore to ensure migration files are tracked. - Enhanced the `CreateCaseInput` type to accommodate new metadata structure.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,4 +8,3 @@ pnpm-lock.yaml
|
|||||||
apps/*/dist
|
apps/*/dist
|
||||||
apps/*/.next
|
apps/*/.next
|
||||||
packages/*/dist
|
packages/*/dist
|
||||||
prisma/migrations
|
|
||||||
|
|||||||
@@ -43,7 +43,12 @@ export async function executeBan(
|
|||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
action: 'BAN',
|
action: 'BAN',
|
||||||
reason: payload.reason,
|
reason: payload.reason,
|
||||||
metadata: { confirmed: true }
|
channelId: interaction.channelId ?? undefined,
|
||||||
|
source: 'button_confirmation',
|
||||||
|
metadata: {
|
||||||
|
confirmed: true,
|
||||||
|
duration: payload.duration
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (payload.duration) {
|
if (payload.duration) {
|
||||||
@@ -98,11 +103,21 @@ export async function executePurge(
|
|||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
action: 'PURGE',
|
action: 'PURGE',
|
||||||
reason: `Deleted ${deletedCount} messages`,
|
reason: `Deleted ${deletedCount} messages`,
|
||||||
|
channelId: payload.channelId,
|
||||||
|
source: 'button_confirmation',
|
||||||
metadata: {
|
metadata: {
|
||||||
confirmed: true,
|
confirmed: true,
|
||||||
channelId: payload.channelId,
|
purge: {
|
||||||
requestedAmount: payload.amount,
|
requestedAmount: payload.amount,
|
||||||
deletedCount
|
deletedCount,
|
||||||
|
filters: {
|
||||||
|
targetUserId: payload.targetUserId,
|
||||||
|
botsOnly: payload.botsOnly,
|
||||||
|
linksOnly: payload.linksOnly,
|
||||||
|
attachmentsOnly: payload.attachmentsOnly,
|
||||||
|
regex: payload.regex
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -147,7 +162,13 @@ export async function executeCaseDelete(
|
|||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
action: 'CASE_DELETE',
|
action: 'CASE_DELETE',
|
||||||
reason: `Soft-deleted case #${payload.caseNumber}`,
|
reason: `Soft-deleted case #${payload.caseNumber}`,
|
||||||
metadata: { confirmed: true, deletedCaseId: existing.id }
|
channelId: interaction.channelId ?? undefined,
|
||||||
|
source: 'button_confirmation',
|
||||||
|
metadata: {
|
||||||
|
confirmed: true,
|
||||||
|
deletedCaseId: existing.id,
|
||||||
|
deletedCaseNumber: payload.caseNumber
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await interaction.update({
|
await interaction.update({
|
||||||
|
|||||||
@@ -18,6 +18,13 @@ function reasonFrom(i: ChatInputCommandInteraction, locale: 'de' | 'en'): string
|
|||||||
return i.options.getString('reason') ?? t(locale, 'moderation.reason.none');
|
return i.options.getString('reason') ?? t(locale, 'moderation.reason.none');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function auditFrom(interaction: ChatInputCommandInteraction) {
|
||||||
|
return {
|
||||||
|
channelId: interaction.channelId ?? undefined,
|
||||||
|
source: 'slash_command' as const
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureMod(
|
async function ensureMod(
|
||||||
interaction: ChatInputCommandInteraction,
|
interaction: ChatInputCommandInteraction,
|
||||||
permission: bigint,
|
permission: bigint,
|
||||||
@@ -70,6 +77,7 @@ const unbanCommand: SlashCommand = {
|
|||||||
guildId: interaction.guildId!,
|
guildId: interaction.guildId!,
|
||||||
targetUserId: userId,
|
targetUserId: userId,
|
||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
|
...auditFrom(interaction),
|
||||||
action: 'UNBAN',
|
action: 'UNBAN',
|
||||||
reason
|
reason
|
||||||
});
|
});
|
||||||
@@ -94,6 +102,7 @@ const kickCommand: SlashCommand = {
|
|||||||
guildId: interaction.guildId!,
|
guildId: interaction.guildId!,
|
||||||
targetUserId: user.id,
|
targetUserId: user.id,
|
||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
|
...auditFrom(interaction),
|
||||||
action: 'KICK',
|
action: 'KICK',
|
||||||
reason
|
reason
|
||||||
});
|
});
|
||||||
@@ -122,6 +131,7 @@ const timeoutCommand: SlashCommand = {
|
|||||||
guildId: interaction.guildId!,
|
guildId: interaction.guildId!,
|
||||||
targetUserId: user.id,
|
targetUserId: user.id,
|
||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
|
...auditFrom(interaction),
|
||||||
action: 'TIMEOUT',
|
action: 'TIMEOUT',
|
||||||
reason,
|
reason,
|
||||||
metadata: { durationMs: duration }
|
metadata: { durationMs: duration }
|
||||||
@@ -147,6 +157,7 @@ const untimeoutCommand: SlashCommand = {
|
|||||||
guildId: interaction.guildId!,
|
guildId: interaction.guildId!,
|
||||||
targetUserId: user.id,
|
targetUserId: user.id,
|
||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
|
...auditFrom(interaction),
|
||||||
action: 'UN_TIMEOUT',
|
action: 'UN_TIMEOUT',
|
||||||
reason
|
reason
|
||||||
});
|
});
|
||||||
@@ -369,6 +380,7 @@ const warnCommand: SlashCommand = {
|
|||||||
guildId: interaction.guildId!,
|
guildId: interaction.guildId!,
|
||||||
targetUserId: user.id,
|
targetUserId: user.id,
|
||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
|
...auditFrom(interaction),
|
||||||
action: 'WARN_CLEAR',
|
action: 'WARN_CLEAR',
|
||||||
reason: 'Warnings cleared'
|
reason: 'Warnings cleared'
|
||||||
});
|
});
|
||||||
@@ -446,6 +458,7 @@ const slowmodeCommand: SlashCommand = {
|
|||||||
guildId: interaction.guildId!,
|
guildId: interaction.guildId!,
|
||||||
targetUserId: interaction.user.id,
|
targetUserId: interaction.user.id,
|
||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
|
...auditFrom(interaction),
|
||||||
action: 'SLOWMODE',
|
action: 'SLOWMODE',
|
||||||
reason: `Set slowmode to ${seconds}s`
|
reason: `Set slowmode to ${seconds}s`
|
||||||
});
|
});
|
||||||
@@ -467,6 +480,7 @@ const lockCommand: SlashCommand = {
|
|||||||
guildId: interaction.guildId!,
|
guildId: interaction.guildId!,
|
||||||
targetUserId: interaction.user.id,
|
targetUserId: interaction.user.id,
|
||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
|
...auditFrom(interaction),
|
||||||
action: 'LOCK',
|
action: 'LOCK',
|
||||||
reason: 'Channel locked'
|
reason: 'Channel locked'
|
||||||
});
|
});
|
||||||
@@ -488,6 +502,7 @@ const unlockCommand: SlashCommand = {
|
|||||||
guildId: interaction.guildId!,
|
guildId: interaction.guildId!,
|
||||||
targetUserId: interaction.user.id,
|
targetUserId: interaction.user.id,
|
||||||
moderatorId: interaction.user.id,
|
moderatorId: interaction.user.id,
|
||||||
|
...auditFrom(interaction),
|
||||||
action: 'UNLOCK',
|
action: 'UNLOCK',
|
||||||
reason: 'Channel unlocked'
|
reason: 'Channel unlocked'
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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';
|
import type { Prisma } from '@prisma/client';
|
||||||
|
import { buildCaseMetadata, type CaseAuditSource } from '@nexumi/shared';
|
||||||
|
|
||||||
type CreateCaseInput = {
|
type CreateCaseInput = {
|
||||||
guildId: string;
|
guildId: string;
|
||||||
@@ -9,7 +10,9 @@ type CreateCaseInput = {
|
|||||||
moderatorId: string;
|
moderatorId: string;
|
||||||
action: string;
|
action: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
metadata?: Prisma.InputJsonValue;
|
source?: CaseAuditSource;
|
||||||
|
channelId?: string;
|
||||||
|
metadata?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function createCase(context: BotContext, input: CreateCaseInput) {
|
export async function createCase(context: BotContext, input: CreateCaseInput) {
|
||||||
@@ -24,6 +27,12 @@ export async function createCase(context: BotContext, input: CreateCaseInput) {
|
|||||||
orderBy: { caseNumber: 'desc' }
|
orderBy: { caseNumber: 'desc' }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const metadata = buildCaseMetadata({
|
||||||
|
source: input.source ?? 'slash_command',
|
||||||
|
channelId: input.channelId,
|
||||||
|
extras: input.metadata
|
||||||
|
});
|
||||||
|
|
||||||
return context.prisma.case.create({
|
return context.prisma.case.create({
|
||||||
data: {
|
data: {
|
||||||
guildId: input.guildId,
|
guildId: input.guildId,
|
||||||
@@ -32,7 +41,7 @@ export async function createCase(context: BotContext, input: CreateCaseInput) {
|
|||||||
moderatorId: input.moderatorId,
|
moderatorId: input.moderatorId,
|
||||||
action: input.action,
|
action: input.action,
|
||||||
reason: input.reason,
|
reason: input.reason,
|
||||||
metadata: input.metadata
|
metadata: metadata as Prisma.InputJsonValue
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -94,6 +103,8 @@ export async function applyWarnEscalation(
|
|||||||
moderatorId,
|
moderatorId,
|
||||||
action: 'TIMEOUT',
|
action: 'TIMEOUT',
|
||||||
reason,
|
reason,
|
||||||
|
channelId: interaction.channelId ?? undefined,
|
||||||
|
source: 'escalation',
|
||||||
metadata: { escalation: true, durationMs: rule.durationMs, warningCount }
|
metadata: { escalation: true, durationMs: rule.durationMs, warningCount }
|
||||||
});
|
});
|
||||||
return `Auto escalation applied: timeout (${rule.durationMs} ms).`;
|
return `Auto escalation applied: timeout (${rule.durationMs} ms).`;
|
||||||
@@ -110,6 +121,8 @@ export async function applyWarnEscalation(
|
|||||||
moderatorId,
|
moderatorId,
|
||||||
action: 'KICK',
|
action: 'KICK',
|
||||||
reason,
|
reason,
|
||||||
|
channelId: interaction.channelId ?? undefined,
|
||||||
|
source: 'escalation',
|
||||||
metadata: { escalation: true, warningCount }
|
metadata: { escalation: true, warningCount }
|
||||||
});
|
});
|
||||||
return 'Auto escalation applied: kick.';
|
return 'Auto escalation applied: kick.';
|
||||||
@@ -125,6 +138,8 @@ export async function applyWarnEscalation(
|
|||||||
moderatorId,
|
moderatorId,
|
||||||
action: 'BAN',
|
action: 'BAN',
|
||||||
reason,
|
reason,
|
||||||
|
channelId: interaction.channelId ?? undefined,
|
||||||
|
source: 'escalation',
|
||||||
metadata: { escalation: true, warningCount }
|
metadata: { escalation: true, warningCount }
|
||||||
});
|
});
|
||||||
return 'Auto escalation applied: ban.';
|
return 'Auto escalation applied: ban.';
|
||||||
|
|||||||
24
packages/shared/src/audit.test.ts
Normal file
24
packages/shared/src/audit.test.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { buildCaseMetadata } from './audit.js';
|
||||||
|
|
||||||
|
describe('buildCaseMetadata', () => {
|
||||||
|
it('builds standardized purge metadata', () => {
|
||||||
|
const metadata = buildCaseMetadata({
|
||||||
|
source: 'button_confirmation',
|
||||||
|
channelId: '123',
|
||||||
|
extras: {
|
||||||
|
confirmed: true,
|
||||||
|
purge: {
|
||||||
|
requestedAmount: 10,
|
||||||
|
deletedCount: 8,
|
||||||
|
filters: { botsOnly: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(metadata.source).toBe('button_confirmation');
|
||||||
|
expect(metadata.channelId).toBe('123');
|
||||||
|
expect(metadata.purge?.deletedCount).toBe(8);
|
||||||
|
expect(metadata.recordedAt).toBeTypeOf('string');
|
||||||
|
});
|
||||||
|
});
|
||||||
56
packages/shared/src/audit.ts
Normal file
56
packages/shared/src/audit.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const CaseAuditSourceSchema = z.enum([
|
||||||
|
'slash_command',
|
||||||
|
'button_confirmation',
|
||||||
|
'escalation',
|
||||||
|
'job'
|
||||||
|
]);
|
||||||
|
|
||||||
|
export type CaseAuditSource = z.infer<typeof CaseAuditSourceSchema>;
|
||||||
|
|
||||||
|
export const PurgeAuditFiltersSchema = z.object({
|
||||||
|
targetUserId: z.string().nullable().optional(),
|
||||||
|
botsOnly: z.boolean().optional(),
|
||||||
|
linksOnly: z.boolean().optional(),
|
||||||
|
attachmentsOnly: z.boolean().optional(),
|
||||||
|
regex: z.string().nullable().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CaseAuditMetadataSchema = z
|
||||||
|
.object({
|
||||||
|
source: CaseAuditSourceSchema,
|
||||||
|
recordedAt: z.string().datetime(),
|
||||||
|
channelId: z.string().optional(),
|
||||||
|
confirmed: z.boolean().optional(),
|
||||||
|
escalation: z.boolean().optional(),
|
||||||
|
warningCount: z.number().int().positive().optional(),
|
||||||
|
durationMs: z.number().int().positive().optional(),
|
||||||
|
deletedCaseId: z.string().optional(),
|
||||||
|
deletedCaseNumber: z.number().int().positive().optional(),
|
||||||
|
purge: z
|
||||||
|
.object({
|
||||||
|
requestedAmount: z.number().int().positive(),
|
||||||
|
deletedCount: z.number().int().nonnegative(),
|
||||||
|
filters: PurgeAuditFiltersSchema.optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
})
|
||||||
|
.passthrough();
|
||||||
|
|
||||||
|
export type CaseAuditMetadata = z.infer<typeof CaseAuditMetadataSchema>;
|
||||||
|
|
||||||
|
type BuildCaseMetadataInput = {
|
||||||
|
source: CaseAuditSource;
|
||||||
|
channelId?: string;
|
||||||
|
extras?: Record<string, unknown>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildCaseMetadata(input: BuildCaseMetadataInput): CaseAuditMetadata {
|
||||||
|
return CaseAuditMetadataSchema.parse({
|
||||||
|
source: input.source,
|
||||||
|
recordedAt: new Date().toISOString(),
|
||||||
|
channelId: input.channelId,
|
||||||
|
...input.extras
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export * from './audit.js';
|
||||||
|
|
||||||
export const LocaleSchema = z.enum(['de', 'en']);
|
export const LocaleSchema = z.enum(['de', 'en']);
|
||||||
export type Locale = z.infer<typeof LocaleSchema>;
|
export type Locale = z.infer<typeof LocaleSchema>;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user