import type { Database } from 'bun:sqlite'; import { db } from '../db'; type GroupStatus = 'pending' | 'allowed' | 'blocked'; type CacheEntry = { status: GroupStatus; label: string | null; }; export class AllowedGroups { static dbInstance: Database = db; // Caché en memoria: group_id (JID completo) -> { status, label } private static cache = new Map(); private static nowExpr = "strftime('%Y-%m-%d %H:%M:%f','now')"; static clearCache() { this.cache.clear(); } static resetForTests() { this.clearCache(); } private static getRow(groupId: string): { group_id: string; label: string | null; status: GroupStatus } | null { try { const row = this.dbInstance .prepare(`SELECT group_id, label, status FROM allowed_groups WHERE group_id = ?`) .get(groupId) as any; if (!row) return null; return { group_id: String(row.group_id), label: row.label != null ? String(row.label) : null, status: String(row.status) as GroupStatus, }; } catch { // Tabla podría no existir en contextos muy tempranos: degradar a null return null; } } static isAllowed(groupId: string | null | undefined): boolean { const gid = String(groupId || '').trim(); if (!gid) return false; const cached = this.cache.get(gid); if (cached) return cached.status === 'allowed'; const row = this.getRow(gid); const status = row?.status || 'pending'; const label = row?.label ?? null; this.cache.set(gid, { status, label }); return status === 'allowed'; } /** * Inserta un grupo como pending si no existe. No degrada estados existentes (allowed/blocked). * Actualiza label si se proporciona y cambió. */ static upsertPending(groupId: string, label?: string | null, discoveredBy?: string | null): void { const gid = String(groupId || '').trim(); if (!gid) return; const row = this.getRow(gid); if (!row) { // Insertar como pending this.dbInstance .prepare(` INSERT INTO allowed_groups (group_id, label, status, discovered_at, updated_at, discovered_by) VALUES (?, ?, 'pending', ${this.nowExpr}, ${this.nowExpr}, ?) `) .run(gid, label ?? null, discoveredBy ?? null); this.cache.set(gid, { status: 'pending', label: label ?? null }); return; } // No cambiar status existente. Solo actualizar label si se aporta y cambió. const newLabel = label ?? row.label; if (label != null && String(row.label ?? '') !== String(label)) { this.dbInstance .prepare(` UPDATE allowed_groups SET label = ?, updated_at = ${this.nowExpr} WHERE group_id = ? `) .run(newLabel, gid); } this.cache.set(gid, { status: row.status, label: newLabel ?? null }); } /** * Establece el estado de un grupo (upsert). Devuelve true si cambió algo (estado o label). */ static setStatus(groupId: string, status: GroupStatus, label?: string | null): boolean { const gid = String(groupId || '').trim(); if (!gid) return false; const before = this.getRow(gid); this.dbInstance .prepare(` INSERT INTO allowed_groups (group_id, label, status, discovered_at, updated_at) VALUES (?, ?, ?, ${this.nowExpr}, ${this.nowExpr}) ON CONFLICT(group_id) DO UPDATE SET status = excluded.status, label = COALESCE(excluded.label, allowed_groups.label), updated_at = excluded.updated_at `) .run(gid, label ?? null, status); const after = this.getRow(gid) || { group_id: gid, label: label ?? null, status }; this.cache.set(gid, { status: after.status, label: after.label }); return ( !before || before.status !== after.status || (label != null && String(before.label ?? '') !== String(label)) ); } static listByStatus(status: GroupStatus): Array<{ group_id: string; label: string | null }> { const rows = this.dbInstance .prepare( `SELECT group_id, label FROM allowed_groups WHERE status = ? ORDER BY group_id` ) .all(status) as Array<{ group_id: string; label: string | null }>; return rows.map(r => ({ group_id: String((r as any).group_id), label: (r as any).label != null ? String((r as any).label) : null, })); } /** * Marca como allowed todos los group_ids provistos en una cadena separada por comas. * Si env no se pasa, usa process.env.ALLOWED_GROUPS. */ static seedFromEnv(env?: string | null): void { const val = (env ?? process?.env?.ALLOWED_GROUPS ?? '').trim(); if (!val) return; const ids = val .split(',') .map(s => s.trim()) .filter(Boolean); for (const gid of ids) { this.setStatus(gid, 'allowed', null); } } }