refactor: convert ModerationService and PruneService from classes to singleton objects

- Convert ModerationService class to moderationService singleton
- Convert PruneService class to pruneService singleton
- Update all command files to use new singleton imports
- Update web routes to use new singleton imports
- Update tests for singleton pattern
- Remove getNextCaseId from tests (now private module function)
This commit is contained in:
syntaxbullet
2026-02-13 13:33:58 +01:00
parent 55d2376ca1
commit 099601ce6d
13 changed files with 176 additions and 184 deletions

View File

@@ -10,34 +10,56 @@ export interface ModerationCaseConfig {
autoTimeoutThreshold?: number;
}
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)],
});
/**
* Generate the next sequential case ID
*/
async function 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 || !match[1]) {
return "CASE-0001";
}
const nextNumber = parseInt(match[1], 10) + 1;
return `CASE-${nextNumber.toString().padStart(4, '0')}`;
if (!latestCase) {
return "CASE-0001";
}
// Extract number from case ID (e.g., "CASE-0042" -> 42)
const match = latestCase.caseId.match(/CASE-(\d+)/);
if (!match || !match[1]) {
return "CASE-0001";
}
const nextNumber = parseInt(match[1], 10) + 1;
return `CASE-${nextNumber.toString().padStart(4, '0')}`;
}
/**
* Get active warnings for a user
*/
async function getUserWarnings(userId: string) {
return await DrizzleClient.query.moderationCases.findMany({
where: and(
eq(moderationCases.userId, BigInt(userId)),
eq(moderationCases.type, CaseType.WARN),
eq(moderationCases.active, true)
),
orderBy: [desc(moderationCases.createdAt)],
});
}
/**
* Get total count of active warnings for a user (useful for auto-timeout)
*/
async function getActiveWarningCount(userId: string): Promise<number> {
const warnings = await getUserWarnings(userId);
return warnings.length;
}
export const moderationService = {
/**
* Create a new moderation case
*/
static async createCase(options: CreateCaseOptions) {
const caseId = await this.getNextCaseId();
async createCase(options: CreateCaseOptions) {
const caseId = await getNextCaseId();
const [newCase] = await DrizzleClient.insert(moderationCases).values({
caseId,
@@ -52,12 +74,12 @@ export class ModerationService {
}).returning();
return newCase;
}
},
/**
* Issue a warning with DM and threshold logic
*/
static async issueWarning(options: {
async issueWarning(options: {
userId: string;
username: string;
moderatorId: string;
@@ -81,7 +103,7 @@ export class ModerationService {
throw new Error("Failed to create moderation case");
}
const warningCount = await this.getActiveWarningCount(options.userId);
const warningCount = await getActiveWarningCount(options.userId);
const config = options.config ?? {};
// Try to DM the user if configured
@@ -127,21 +149,21 @@ export class ModerationService {
}
return { moderationCase, warningCount, autoTimeoutIssued };
}
},
/**
* Get a case by its case ID
*/
static async getCaseById(caseId: string) {
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) {
async getUserCases(userId: string, activeOnly: boolean = false) {
const conditions = [eq(moderationCases.userId, BigInt(userId))];
if (activeOnly) {
@@ -152,26 +174,19 @@ export class ModerationService {
where: and(...conditions),
orderBy: [desc(moderationCases.createdAt)],
});
}
},
/**
* Get active warnings for a user
* Get active warnings for a user (public alias)
*/
static async getUserWarnings(userId: string) {
return await DrizzleClient.query.moderationCases.findMany({
where: and(
eq(moderationCases.userId, BigInt(userId)),
eq(moderationCases.type, CaseType.WARN),
eq(moderationCases.active, true)
),
orderBy: [desc(moderationCases.createdAt)],
});
}
async getUserWarnings(userId: string) {
return await getUserWarnings(userId);
},
/**
* Get all notes for a user
*/
static async getUserNotes(userId: string) {
async getUserNotes(userId: string) {
return await DrizzleClient.query.moderationCases.findMany({
where: and(
eq(moderationCases.userId, BigInt(userId)),
@@ -179,12 +194,12 @@ export class ModerationService {
),
orderBy: [desc(moderationCases.createdAt)],
});
}
},
/**
* Clear/resolve a warning
*/
static async clearCase(options: ClearCaseOptions) {
async clearCase(options: ClearCaseOptions) {
const [updatedCase] = await DrizzleClient.update(moderationCases)
.set({
active: false,
@@ -196,12 +211,12 @@ export class ModerationService {
.returning();
return updatedCase;
}
},
/**
* Search cases with various filters
*/
static async searchCases(filter: SearchCasesFilter) {
async searchCases(filter: SearchCasesFilter) {
const conditions = [];
if (filter.userId) {
@@ -228,13 +243,12 @@ export class ModerationService {
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;
}
}
async getActiveWarningCount(userId: string): Promise<number> {
return await getActiveWarningCount(userId);
},
};