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

9.3 KiB

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 714).

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 (admisible 714)
  • 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:
        • Recomendado: usar un wrapper handleWithOutcome en CommandService que devuelva { responses, ok: boolean, createdTaskIds?: number[] }.
        • Alternativa mínima (temporal): heurística sobre texto (mensajes que empiezan con “ Uso: …”, “No puedes …”, “no encontrada”, “Acción … no reconocida”, “⚠️ …” → error).
      • 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.