From c1e79dca233d3e2d0b5e37b42a48d0601e2acc99 Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Thu, 4 Jun 2026 16:51:08 +0700 Subject: [PATCH] feat(ai-moderation): add per-user moderation result cache for spammer deduplication --- .../modules/ai-moderation/textCacheStore.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/services/discord-gateway/src/modules/ai-moderation/textCacheStore.ts b/services/discord-gateway/src/modules/ai-moderation/textCacheStore.ts index 19ffe7a..241de36 100644 --- a/services/discord-gateway/src/modules/ai-moderation/textCacheStore.ts +++ b/services/discord-gateway/src/modules/ai-moderation/textCacheStore.ts @@ -240,6 +240,117 @@ export async function upsertCachedMediaAnalysis( } } +// --------------------------------------------------------------------------- +// Per-user moderation result cache (for spammer deduplication) +// --------------------------------------------------------------------------- + +/** + * Generate a deterministic cache key for a per-user moderation result. + * + * Format: user_mod:: + * Two users sending the same text get separate cache entries so that + * per-user action history (e.g. repeated spam) can be tracked later. + */ +export function makeUserModerationCacheKey( + userId: string, + content: string, +): string { + const hash = createHash("sha256") + .update(content) + .digest("hex") + .slice(0, 16); + return `user_mod:${userId}:${hash}`; +} + +/** + * Lookup a cached moderation result for a (user, content) pair. + * Returns the stored result fields or null. + */ +export async function getCachedUserModeration( + cacheKey: string, +): Promise<{ + flags: string[]; + score: number; + analysis: string; + categories: string[]; + severity: string; + confidence: number; + recommendedAction: string; +} | null> { + try { + const row = await executeGet( + `SELECT flags, source, analyzed_at, expires_at, hit_count + FROM text_analysis_cache + WHERE text = $1 AND expires_at > $2`, + [cacheKey, Date.now()], + ); + + if (!row) return null; + + const parsed = JSON.parse(row.flags) as Record; + + return { + flags: (parsed.flags as string[]) ?? [], + score: (parsed.score as number) ?? 0, + analysis: (parsed.analysis as string) ?? "", + categories: (parsed.categories as string[]) ?? [], + severity: (parsed.severity as string) ?? "none", + confidence: (parsed.confidence as number) ?? 0, + recommendedAction: (parsed.recommendedAction as string) ?? "none", + }; + } catch (error) { + logger.error( + { error: error instanceof Error ? error.message : String(error) }, + "Failed to get cached user moderation", + ); + return null; + } +} + +/** + * Store a moderation result for a (user, content) pair. + * The `flags` field stores the full result object as JSON. + */ +export async function setCachedUserModeration( + cacheKey: string, + result: { + flags: string[]; + score: number; + analysis: string; + categories: string[]; + severity: string; + confidence: number; + recommendedAction: string; + }, +): Promise { + const now = Date.now(); + const USER_MOD_CACHE_TTL_MS = 24 * 60 * 60 * 1000; + + try { + await executeAll( + `INSERT INTO text_analysis_cache (text, flags, source, analyzed_at, expires_at, hit_count) + VALUES ($1, $2, $3, $4, $5, 0) + ON CONFLICT (text) DO UPDATE SET + flags = EXCLUDED.flags, + source = EXCLUDED.source, + analyzed_at = EXCLUDED.analyzed_at, + expires_at = EXCLUDED.expires_at`, + [ + cacheKey, + JSON.stringify(result), + "user_moderation", + now, + now + USER_MOD_CACHE_TTL_MS, + ], + ); + } catch (error) { + logger.error( + { error: error instanceof Error ? error.message : String(error) }, + "Failed to set cached user moderation", + ); + } +} + // --------------------------------------------------------------------------- // Perceptual hash helpers for image deduplication // ---------------------------------------------------------------------------