You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
taskbot/docs/plan-reacciones-bot.md

230 lines
9.1 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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 (714 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” (⏳) >12s 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.