|  |  | # 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": "<jid>",
 | 
						
						
						
							|  |  |     "messageId": "<msg-id>"
 | 
						
						
						
							|  |  |   }
 | 
						
						
						
							|  |  |   ```
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | Envío (Evolution API):
 | 
						
						
						
							|  |  | - POST {EVOLUTION_API_URL}/message/sendReaction/{instance}
 | 
						
						
						
							|  |  | - Headers: { apikey, Content-Type: application/json }
 | 
						
						
						
							|  |  | - Body:
 | 
						
						
						
							|  |  |   ```
 | 
						
						
						
							|  |  |   {
 | 
						
						
						
							|  |  |     "key": { "remoteJid": "<jid>", "fromMe": false, "id": "<msg-id>" },
 | 
						
						
						
							|  |  |     "reaction": "<emoji>"
 | 
						
						
						
							|  |  |   }
 | 
						
						
						
							|  |  |   ```
 | 
						
						
						
							|  |  | 
 | 
						
						
						
							|  |  | 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.
 |