import { createHash } from 'crypto'; export type SpamReason = 'emoji_only' | 'short_greeting' | 'link_only' | 'forward_flood' | 'duplicate_hash'; export interface SpamResult { isSpam: boolean; reason?: SpamReason; contentHash: string; } const GREETING_PHRASES = [ 'good morning', 'good evening', 'good night', 'good afternoon', 'gm', 'gn', 'subah ki shayari', 'शुभ प्रभात', 'शुभ रात्रि', 'happy', 'jai hind', 'jai shree ram', 'jai mata di', 'forwarded as received', 'please share', 'must share', ]; const EMOJI_RE = /^[\p{Emoji}\p{Emoji_Presentation}\s]+$/u; const URL_RE = /https?:\/\/\S+/gi; const TOKEN_MIN = 4; export function hashContent(content: string): string { return createHash('sha256').update(content.trim().toLowerCase()).digest('hex').slice(0, 16); } export function detectSpam(content: string): SpamResult { const trimmed = content.trim(); const contentHash = hashContent(trimmed); if (EMOJI_RE.test(trimmed)) { return { isSpam: true, reason: 'emoji_only', contentHash }; } const lower = trimmed.toLowerCase(); const withoutUrls = lower.replace(URL_RE, '').trim(); const tokens = withoutUrls.split(/\s+/).filter(Boolean); if (tokens.length < TOKEN_MIN) { if (GREETING_PHRASES.some((phrase) => lower.includes(phrase))) { return { isSpam: true, reason: 'short_greeting', contentHash }; } } const urls = trimmed.match(URL_RE) ?? []; if (urls.length > 0 && tokens.length === 0) { return { isSpam: true, reason: 'link_only', contentHash }; } return { isSpam: false, contentHash }; } export async function checkDuplicateHash( contentHash: string, tenantId: string, sourceGroupId: string, windowMs: number, prisma: any, ): Promise { const since = new Date(Date.now() - windowMs); const count = await prisma.message.count({ where: { tenantId, sourceGroupId, content: { startsWith: '' }, createdAt: { gte: since }, }, }); if (count === 0) return false; const recent = await prisma.message.findMany({ where: { tenantId, sourceGroupId, createdAt: { gte: since } }, select: { content: true }, take: 100, }); return recent.some((m: { content: string }) => hashContent(m.content) === contentHash); }