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