You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

150 lines
4.7 KiB
TypeScript

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<string, CacheEntry>();
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);
}
}
}