feat(ai-moderation): add per-user moderation result cache for spammer deduplication
This commit is contained in:
@@ -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:<userId>:<sha256(content).slice(0,16)>
|
||||
* 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<string, unknown>;
|
||||
|
||||
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<void> {
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user