import type { Database } from 'bun:sqlite'; import { isGroupId } from '../utils/whatsapp'; import { toIsoUTC } from '../utils/datetime'; import { AllowedGroups } from '../services/allowed-groups'; import { ResponseQueue } from '../services/response-queue'; // ── Env helpers ────────────────────────────────────────────── const DEFAULT_TTL_DAYS = 14; function isEnvFlagEnabled(envKey: string, defaultVal = false): boolean { const raw = String(process.env[envKey] || String(defaultVal)).toLowerCase(); return ['true', '1', 'yes', 'on'].includes(raw); } function envNumber(envKey: string, fallback: number): number { const n = Number(process.env[envKey]); return Number.isFinite(n) && n > 0 ? n : fallback; } function envString(envKey: string, fallback: string): string { const v = (process.env[envKey] ?? '').trim(); return v || fallback; } // ── DB helpers ─────────────────────────────────────────────── interface TaskOrigin { chat_id?: string; message_id?: string; created_at?: string; participant?: string | null; from_me?: number | boolean | null; } /** Query con fallback: si la columna participant/from_me no existe aún (schema antiguo), reintenta sin ellas. */ function getTaskOrigin(db: Database, taskId: number): TaskOrigin | null { try { const row = db.prepare(` SELECT chat_id, message_id, created_at, participant, from_me FROM task_origins WHERE task_id = ? `).get(taskId) as TaskOrigin | undefined; return row ?? null; } catch { const row = db.prepare(` SELECT chat_id, message_id, created_at FROM task_origins WHERE task_id = ? `).get(taskId) as TaskOrigin | undefined; return row ?? null; } } // ── Eligibility checks ─────────────────────────────────────── function isScopeEligible(chatId: string): boolean { const scope = envString('REACTIONS_SCOPE', 'groups').toLowerCase(); return scope === 'all' || isGroupId(chatId); } function isWithinTtl(origin: TaskOrigin): boolean { const ttlDays = envNumber('REACTIONS_TTL_DAYS', DEFAULT_TTL_DAYS); const maxAgeMs = ttlDays * 24 * 60 * 60 * 1000; const createdMs = Date.parse(toIsoUTC(String(origin.created_at || ''))); return Number.isFinite(createdMs) && (Date.now() - createdMs <= maxAgeMs); } function isGatingAllowed(chatId: string): boolean { if (!isGroupId(chatId)) return true; const mode = envString('GROUP_GATING_MODE', 'off').toLowerCase(); if (mode !== 'enforce') return true; try { return AllowedGroups.isAllowed(chatId); } catch { return true; // fail open } } // ── Reaction options builder ───────────────────────────────── function buildReactionOpts(origin: TaskOrigin): { participant?: string; fromMe?: boolean } { const participant = origin.participant ? String(origin.participant) : undefined; const fromMe = (origin.from_me === 1 || origin.from_me === true) ? true : undefined; const opts: { participant?: string; fromMe?: boolean } = {}; if (participant !== undefined) opts.participant = participant; if (typeof fromMe === 'boolean') opts.fromMe = fromMe; return Object.keys(opts).length > 0 ? opts : {}; } // ── Public API ─────────────────────────────────────────────── /** * Publica una reacción ✅ al mensaje origen de la tarea si: * - REACTIONS_ENABLED está activado, * - scope permite (all o solo grupos), * - está dentro del TTL configurado (por defecto 14 días), * - y pasa el gating de grupos en modo 'enforce'. * * No lanza errores (no debe bloquear el flujo de completado). */ export function enqueueCompletionReactionIfEligible(db: Database, taskId: number): void { try { if (!isEnvFlagEnabled('REACTIONS_ENABLED')) return; const origin = getTaskOrigin(db, taskId); if (!origin?.chat_id || !origin.message_id) return; const chatId = String(origin.chat_id); if (!isScopeEligible(chatId)) return; if (!isWithinTtl(origin)) return; if (!isGatingAllowed(chatId)) return; ResponseQueue.enqueueReaction( chatId, String(origin.message_id), '✅', buildReactionOpts(origin), ).catch(() => {}); } catch { // no-op: nunca bloquear el flujo de completado } }