feat: add moderation module with case tracking database schema

This commit is contained in:
syntaxbullet
2025-12-24 20:55:56 +01:00
parent 37ac0ee934
commit 64cf47ee03
8 changed files with 1392 additions and 0 deletions

View File

@@ -0,0 +1,158 @@
import { moderationCases } from "@/db/schema";
import { eq, and, desc } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient";
import type { CreateCaseOptions, ClearCaseOptions, SearchCasesFilter, CaseType } from "./moderation.types";
export class ModerationService {
/**
* Generate the next sequential case ID
*/
private static async getNextCaseId(): Promise<string> {
const latestCase = await DrizzleClient.query.moderationCases.findFirst({
orderBy: [desc(moderationCases.id)],
});
if (!latestCase) {
return "CASE-0001";
}
// Extract number from case ID (e.g., "CASE-0042" -> 42)
const match = latestCase.caseId.match(/CASE-(\d+)/);
if (!match) {
return "CASE-0001";
}
const nextNumber = parseInt(match[1], 10) + 1;
return `CASE-${nextNumber.toString().padStart(4, '0')}`;
}
/**
* Create a new moderation case
*/
static async createCase(options: CreateCaseOptions) {
const caseId = await this.getNextCaseId();
const [newCase] = await DrizzleClient.insert(moderationCases).values({
caseId,
type: options.type,
userId: BigInt(options.userId),
username: options.username,
moderatorId: BigInt(options.moderatorId),
moderatorName: options.moderatorName,
reason: options.reason,
metadata: options.metadata || {},
active: options.type === 'warn' ? true : false, // Only warnings are "active" by default
}).returning();
return newCase;
}
/**
* Get a case by its case ID
*/
static async getCaseById(caseId: string) {
return await DrizzleClient.query.moderationCases.findFirst({
where: eq(moderationCases.caseId, caseId),
});
}
/**
* Get all cases for a specific user
*/
static async getUserCases(userId: string, activeOnly: boolean = false) {
const conditions = [eq(moderationCases.userId, BigInt(userId))];
if (activeOnly) {
conditions.push(eq(moderationCases.active, true));
}
return await DrizzleClient.query.moderationCases.findMany({
where: and(...conditions),
orderBy: [desc(moderationCases.createdAt)],
});
}
/**
* Get active warnings for a user
*/
static async getUserWarnings(userId: string) {
return await DrizzleClient.query.moderationCases.findMany({
where: and(
eq(moderationCases.userId, BigInt(userId)),
eq(moderationCases.type, 'warn'),
eq(moderationCases.active, true)
),
orderBy: [desc(moderationCases.createdAt)],
});
}
/**
* Get all notes for a user
*/
static async getUserNotes(userId: string) {
return await DrizzleClient.query.moderationCases.findMany({
where: and(
eq(moderationCases.userId, BigInt(userId)),
eq(moderationCases.type, 'note')
),
orderBy: [desc(moderationCases.createdAt)],
});
}
/**
* Clear/resolve a warning
*/
static async clearCase(options: ClearCaseOptions) {
const [updatedCase] = await DrizzleClient.update(moderationCases)
.set({
active: false,
resolvedAt: new Date(),
resolvedBy: BigInt(options.clearedBy),
resolvedReason: options.reason || 'Manually cleared',
})
.where(eq(moderationCases.caseId, options.caseId))
.returning();
return updatedCase;
}
/**
* Search cases with various filters
*/
static async searchCases(filter: SearchCasesFilter) {
const conditions = [];
if (filter.userId) {
conditions.push(eq(moderationCases.userId, BigInt(filter.userId)));
}
if (filter.moderatorId) {
conditions.push(eq(moderationCases.moderatorId, BigInt(filter.moderatorId)));
}
if (filter.type) {
conditions.push(eq(moderationCases.type, filter.type));
}
if (filter.active !== undefined) {
conditions.push(eq(moderationCases.active, filter.active));
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
return await DrizzleClient.query.moderationCases.findMany({
where: whereClause,
orderBy: [desc(moderationCases.createdAt)],
limit: filter.limit || 50,
offset: filter.offset || 0,
});
}
/**
* Get total count of active warnings for a user (useful for auto-timeout)
*/
static async getActiveWarningCount(userId: string): Promise<number> {
const warnings = await this.getUserWarnings(userId);
return warnings.length;
}
}