# Plan de reacciones del bot de tareas (WhatsApp) Objetivo: añadir un “ack” visual de bajo ruido en grupos, usando reacciones a los mensajes con comandos `/t`. Alcance inicial: - Reaccionar 1 sola vez por comando: - Éxito (comando procesado): 🤖 - Error (uso inválido, permisos, no encontrada…): ⚠️ - Plus opcional sin mucha complejidad: si el comando creó una tarea y esta se completa dentro de un TTL (7–14 días), reaccionar con ✅ al mensaje origen del comando. No se añaden mensajes al chat; solo reacciones. Por defecto solo en grupos. Todo detrás de “feature flags”. --- ## 1) UX y reglas - Ámbito: - Grupos permitidos (AllowedGroups) por defecto (REACTIONS_SCOPE=groups). - No reaccionar en DMs salvo que se configure explícitamente (REACTIONS_SCOPE=all). - Una reacción por comando (no usar “procesando”/“pensando”). - No borrar/reemplazar reacciones anteriores; simplemente añadir la correspondiente (🤖/⚠️) y, si aplica, luego ✅. - TTL para marcar ✅ tras completar: 14 días por defecto (configurable vía REACTIONS_TTL_DAYS). Emojis: - Éxito de procesamiento: 🤖 - Error: ⚠️ - Tarea completada (tardío): ✅ --- ## 2) Flags/entorno Añadir variables de entorno: - REACTIONS_ENABLED=true|false (default: false) - REACTIONS_TTL_DAYS=14 (configurable; sin clamp, por defecto 14) - REACTIONS_SCOPE=groups|all (default: groups) - (Opcional) RQ_REACTIONS_MAX_ATTEMPTS=3 para limitar reintentos de jobs de reacción Se reutilizan: - GROUP_GATING_MODE (off|discover|enforce) - AllowedGroups.isAllowed para coherencia con el gating. --- ## 3) Persistencia: nueva tabla `task_origins` (migración v17) Objetivo: vincular una tarea creada con el mensaje de WhatsApp que originó el comando para poder reaccionar con ✅ al completarse. Esquema: - task_id INTEGER PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE - chat_id TEXT NOT NULL // JID completo del grupo (p. ej. 123@g.us) - message_id TEXT NOT NULL // id del mensaje del comando - created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M:%f','now')) Índices: - CREATE INDEX IF NOT EXISTS idx_task_origins_task ON task_origins(task_id); - (Opcional) CREATE INDEX IF NOT EXISTS idx_task_origins_chat_msg ON task_origins(chat_id, message_id); Notas: - 1 fila por tarea (PK = task_id). Suficiente para nuestro caso. - No toca esquemas existentes. --- ## 4) Cola: soporte de “jobs de reacción” en ResponseQueue Formato del job (reutilizamos `response_queue`, sin cambiar esquema): - recipient: usar `chatId` (JID) para cumplir NOT NULL. - message: puede estar vacío (no se usa para reactions). - metadata (JSON): ``` { "kind": "reaction", "emoji": "🤖" | "⚠️" | "✅", "chatId": "", "messageId": "" } ``` Envío (Evolution API): - POST {EVOLUTION_API_URL}/message/sendReaction/{instance} - Headers: { apikey, Content-Type: application/json } - Body: ``` { "key": { "remoteJid": "", "fromMe": false, "id": "" }, "reaction": "" } ``` Reintentos: - Backoff existente. - Opcional: limitar reacciones con `RQ_REACTIONS_MAX_ATTEMPTS` (p. ej. 3). 4xx → fallo definitivo; 5xx/red → reintentos. Idempotencia: - Antes de insertar, consultar si ya existe (status IN queued|processing|sent) un job con metadata idéntica (mismo chatId, messageId, emoji) en las últimas 24h; si existe, no insertar otro. - Mantener JSON canónico (mismas claves/orden) al construir metadata para hacer la comparación fiable o parsear JSON en la consulta. --- ## 5) Cambios por fichero (implementación por fases) Fase 1 — Infra y reacción final por comando - src/services/response-queue.ts - Detectar `metadata.kind === 'reaction'`. - Construir y enviar POST a `/message/sendReaction/{instance}` con el payload anterior. - Opcional: `RQ_REACTIONS_MAX_ATTEMPTS` para jobs de reacción. - src/server.ts (WebhookServer) - Capturar `messageId = data.key.id`. - Pasar `messageId` en el `CommandContext`. - Tras ejecutar el comando, decidir emoji: - Si REACTIONS_ENABLED=false → no hacer nada. - Si REACTIONS_SCOPE=groups y no es grupo → no hacer nada. - Si GROUP_GATING_MODE='enforce' y el grupo no está allowed → no hacer nada. - Determinar outcome con `handleWithOutcome` en CommandService que devuelve `{ responses, ok: boolean, createdTaskIds?: number[] }` (implementado). - Encolar job con emoji = ok ? 🤖 : ⚠️, `chatId=remoteJid`, `messageId`. - Idempotencia: consulta previa antes de insertar. - src/services/command.ts - Ampliar `CommandContext` con `messageId: string`. - En la rama `/t nueva`, tras crear la tarea: - Si `isGroupId(context.groupId)` y `context.messageId`, insertar fila en `task_origins (task_id, chat_id, message_id)`. - (Recomendado) Añadir `handleWithOutcome` para clasificar ok/error sin depender del texto. - src/db/migrations/index.ts - Añadir migración v17 con `task_origins` e índices. Fase 2 — Reacción tardía (✅) al completar - src/tasks/service.ts - En `completeTask`, cuando `status === 'updated'`: - Buscar `task_origins` por `taskId`. - Si no existe, salir. - Comprobar TTL: `now - created_at <= REACTIONS_TTL_DAYS`. - Flags/política: `REACTIONS_ENABLED` y, si `REACTIONS_SCOPE=groups`, que `chat_id` termine en `@g.us`. - (Opcional) En modo enforce, verificar AllowedGroups.isAllowed(chat_id). - Encolar job `kind:'reaction', emoji:'✅', chatId, messageId`. - Idempotencia: mismo check previo antes de insertar. --- ## 6) Flujo E2E (grupo permitido) 1) Usuario envía mensaje con `/t nueva …` en un grupo. 2) WebhookServer: - Obtiene `remoteJid`, `messageId`. - Construye `CommandContext` con `sender`, `groupId`, `message`, `mentions`, `messageId`. 3) CommandService: - Procesa el comando. - Si crea tarea: inserta fila en `task_origins`. 4) WebhookServer: - Clasifica outcome (ok/err). - Si aplica, encola una reacción (🤖 o ⚠️) usando ResponseQueue. 5) Más tarde, alguien completa la tarea: - TaskService.completeTask → si dentro del TTL, encola ✅ apuntando al `messageId` original. 6) ResponseQueue: - Consume jobs `kind:'reaction'` y llama a Evolution `/message/sendReaction`. --- ## 7) Idempotencia, límites y gating - Idempotencia: - No duplicar reacciones para el mismo (chatId, messageId, emoji) gracias a la consulta previa en `response_queue`. - Completar varias veces → solo 1 job ✅ (misma idempotencia). - Gating: - Respetar `GROUP_GATING_MODE='enforce'`: no reaccionar en grupos no permitidos. - No reaccionar en DMs salvo `REACTIONS_SCOPE=all`. - Límites: - RateLimiter de comandos ya limita frecuencia. - Reintentos de reacciones limitados para evitar ruido prolongado. --- ## 8) Errores previstos y manejo - Mensaje borrado / bot expulsado / permisos → error 4xx → marcar `failed` sin reintentos excesivos. - Errores de red/5xx → reintentos con backoff hasta `RQ_REACTIONS_MAX_ATTEMPTS` (si definido) o los globales. - Falta de `messageId` en el evento → omitir reacciones y `task_origins` (no romper el flujo). --- ## 9) Pruebas a añadir Unitarias: - Reacción final: - Grupo allowed, `REACTIONS_ENABLED=true`, `/t nueva …` → se encola 🤖 (1 job con metadata.kind='reaction', emoji='🤖', chatId=grupo, messageId capturado). - Comando inválido (p. ej. `/t x` sin IDs) → se encola ⚠️. - DM con `REACTIONS_SCOPE=groups` → no se encola. - `REACTIONS_ENABLED=false` → no se encola. - task_origins: - Tras `/t nueva` en grupo, existe `task_origins(task_id, chat_id, message_id)`. - Completar → ✅: - Dentro de TTL → se encola ✅ con el `messageId` de origen. - Fuera de TTL → no se encola. - Completar dos veces → solo 1 job ✅ (idempotencia). - ResponseQueue: - Jobs `kind:'reaction'` llaman a `/message/sendReaction…` (no a sendText). - Manejo de 4xx/5xx conforme a política de reintentos. Integración simulada: - Flujo feliz: `/t nueva` → 🤖; `completeTask` → ✅. - Error: comando desconocido o “Uso:” → ⚠️. - Grupo bloqueado en enforce → no reacción. --- ## 10) Despliegue y configuración - Añadir flags al entorno: - `REACTIONS_ENABLED=false` (arranque en “off”). - `REACTIONS_TTL_DAYS=14`. - `REACTIONS_SCOPE=groups`. - (Opcional) `RQ_REACTIONS_MAX_ATTEMPTS=3`. - Aplicar migraciones (incluye v17: `task_origins`). - Activar `REACTIONS_ENABLED` gradualmente y monitorizar efectos. --- ## 11) Consideraciones - Notificaciones: algunos usuarios reciben notificación por reacciones; una sola por comando minimiza ruido. - Privacidad: no se envían datos nuevos; solo reacciones en el mismo chat. - Observabilidad: se puede añadir contadores de métricas (opcional): - `reactions_enqueued_total{emoji=…}`, `reactions_sent_total`, `reactions_failed_total`. --- ## 12) Trabajos futuros (opcional) - Debounce de “procesando” (⏳) >1–2s y reemplazo por 🤖. - Opt-out por grupo (preferencia guardada en DB). - Cambio de reacción previa (quitar ⚠️/🤖 y dejar solo ✅) — requiere leer/gestionar estado de reacciones y añade complejidad. - Reaccionar a otros comandos (tomar/soltar) con emojis específicos.