refactor: extraer display_code y complete-reaction; ajustar TaskService
Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>main
parent
a46b5dac68
commit
e3ec82037b
@ -0,0 +1,74 @@
|
||||
import type { Database } from 'bun:sqlite';
|
||||
import { isGroupId } from '../utils/whatsapp';
|
||||
import { AllowedGroups } from '../services/allowed-groups';
|
||||
import { ResponseQueue } from '../services/response-queue';
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
const rxEnabled = String(process.env.REACTIONS_ENABLED || 'false').toLowerCase();
|
||||
const enabled = ['true', '1', 'yes', 'on'].includes(rxEnabled);
|
||||
if (!enabled) return;
|
||||
|
||||
let origin: any = null;
|
||||
try {
|
||||
origin = db.prepare(`
|
||||
SELECT chat_id, message_id, created_at, participant, from_me
|
||||
FROM task_origins
|
||||
WHERE task_id = ?
|
||||
`).get(taskId) as { chat_id?: string; message_id?: string; created_at?: string; participant?: string | null; from_me?: number | boolean | null } | undefined;
|
||||
} catch {
|
||||
origin = db.prepare(`
|
||||
SELECT chat_id, message_id, created_at
|
||||
FROM task_origins
|
||||
WHERE task_id = ?
|
||||
`).get(taskId) as { chat_id?: string; message_id?: string; created_at?: string } | undefined;
|
||||
}
|
||||
|
||||
if (!origin || !origin.chat_id || !origin.message_id) return;
|
||||
|
||||
const chatId = String(origin.chat_id);
|
||||
const scope = String(process.env.REACTIONS_SCOPE || 'groups').toLowerCase();
|
||||
if (!(scope === 'all' || isGroupId(chatId))) return;
|
||||
|
||||
// TTL desde REACTIONS_TTL_DAYS (default 14 si inválido)
|
||||
const ttlDaysEnv = Number(process.env.REACTIONS_TTL_DAYS);
|
||||
const ttlDays = Number.isFinite(ttlDaysEnv) && ttlDaysEnv > 0 ? ttlDaysEnv : 14;
|
||||
const maxAgeMs = ttlDays * 24 * 60 * 60 * 1000;
|
||||
|
||||
const createdRaw = String(origin.created_at || '');
|
||||
const createdIso = createdRaw.includes('T') ? createdRaw : (createdRaw.replace(' ', 'T') + 'Z');
|
||||
const createdMs = Date.parse(createdIso);
|
||||
const withinTtl = Number.isFinite(createdMs) ? (Date.now() - createdMs <= maxAgeMs) : false;
|
||||
if (!withinTtl) return;
|
||||
|
||||
// Gating 'enforce' para grupos
|
||||
if (isGroupId(chatId)) {
|
||||
const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase();
|
||||
if (mode === 'enforce') {
|
||||
let allowed = true;
|
||||
try { allowed = AllowedGroups.isAllowed(chatId); } catch { allowed = true; }
|
||||
if (!allowed) return;
|
||||
}
|
||||
}
|
||||
|
||||
// Encolar reacción ✅ con idempotencia; no bloquear si falla
|
||||
const participant = origin && origin.participant ? String(origin.participant) : undefined;
|
||||
const fromMe = (origin && (origin.from_me === 1 || origin.from_me === true)) ? true : undefined;
|
||||
const rxOpts: { participant?: string; fromMe?: boolean } = {};
|
||||
if (participant !== undefined) rxOpts.participant = participant;
|
||||
if (typeof fromMe === 'boolean') rxOpts.fromMe = fromMe;
|
||||
|
||||
ResponseQueue.enqueueReaction(chatId, String(origin.message_id), '✅', rxOpts).catch(() => {});
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
import type { Database } from 'bun:sqlite';
|
||||
|
||||
/**
|
||||
* Calcula el siguiente display_code disponible entre tareas activas
|
||||
* (y tareas completadas en las últimas 24h), empezando en 1 hasta 9999,
|
||||
* respetando huecos.
|
||||
*/
|
||||
export function pickNextDisplayCode(db: Database): number {
|
||||
const MAX_DISPLAY_CODE = 9999;
|
||||
|
||||
const rows = db
|
||||
.prepare(`
|
||||
SELECT display_code
|
||||
FROM tasks
|
||||
WHERE display_code IS NOT NULL
|
||||
AND (
|
||||
COALESCE(completed, 0) = 0
|
||||
OR (completed_at IS NOT NULL AND completed_at >= strftime('%Y-%m-%d %H:%M:%f','now','-24 hours'))
|
||||
)
|
||||
ORDER BY display_code ASC
|
||||
`)
|
||||
.all() as Array<{ display_code: number }>;
|
||||
|
||||
let expect = 1;
|
||||
for (const r of rows) {
|
||||
const dc = Number(r.display_code || 0);
|
||||
if (dc < expect) continue;
|
||||
if (dc === expect) {
|
||||
expect++;
|
||||
if (expect > MAX_DISPLAY_CODE) break;
|
||||
continue;
|
||||
}
|
||||
// encontrado hueco
|
||||
break;
|
||||
}
|
||||
if (expect > MAX_DISPLAY_CODE) {
|
||||
throw new Error('No hay códigos disponibles (límite alcanzado)');
|
||||
}
|
||||
return expect;
|
||||
}
|
||||
Loading…
Reference in New Issue