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.
		
		
		
		
		
			
		
			
				
	
	
	
		
			9.1 KiB
		
	
	
	
			
		
		
	
	
			9.1 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 (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_ATTEMPTSpara jobs de reacción.
 
- Detectar 
- src/server.ts (WebhookServer)
- Capturar messageId = data.key.id.
- Pasar messageIden elCommandContext.
- 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 handleWithOutcomeen CommandService que devuelve{ responses, ok: boolean, createdTaskIds?: number[] }(implementado).
- Encolar job con emoji = ok ? 🤖 : ⚠️, chatId=remoteJid,messageId.
- Idempotencia: consulta previa antes de insertar.
 
 
- Capturar 
- src/services/command.ts
- Ampliar CommandContextconmessageId: string.
- En la rama /t nueva, tras crear la tarea:- Si isGroupId(context.groupId)ycontext.messageId, insertar fila entask_origins (task_id, chat_id, message_id).
 
- Si 
- (Recomendado) Añadir handleWithOutcomepara clasificar ok/error sin depender del texto.
 
- Ampliar 
- src/db/migrations/index.ts
- Añadir migración v17 con task_originse índices.
 
- Añadir migración v17 con 
Fase 2 — Reacción tardía (✅) al completar
- src/tasks/service.ts
- En completeTask, cuandostatus === 'updated':- Buscar task_originsportaskId.
- Si no existe, salir.
- Comprobar TTL: now - created_at <= REACTIONS_TTL_DAYS.
- Flags/política: REACTIONS_ENABLEDy, siREACTIONS_SCOPE=groups, quechat_idtermine 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.
 
- Buscar 
 
- En 
6) Flujo E2E (grupo permitido)
- Usuario envía mensaje con /t nueva …en un grupo.
- WebhookServer:
- Obtiene remoteJid,messageId.
- Construye CommandContextconsender,groupId,message,mentions,messageId.
 
- Obtiene 
- CommandService:
- Procesa el comando.
- Si crea tarea: inserta fila en task_origins.
 
- WebhookServer:
- Clasifica outcome (ok/err).
- Si aplica, encola una reacción (🤖 o ⚠️) usando ResponseQueue.
 
- Más tarde, alguien completa la tarea:
- TaskService.completeTask → si dentro del TTL, encola ✅ apuntando al messageIdoriginal.
 
- TaskService.completeTask → si dentro del TTL, encola ✅ apuntando al 
- ResponseQueue:
- Consume jobs kind:'reaction'y llama a Evolution/message/sendReaction.
 
- Consume jobs 
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).
 
- No duplicar reacciones para el mismo (chatId, messageId, emoji) gracias a la consulta previa en 
- Gating:
- Respetar GROUP_GATING_MODE='enforce': no reaccionar en grupos no permitidos.
- No reaccionar en DMs salvo REACTIONS_SCOPE=all.
 
- Respetar 
- 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 failedsin reintentos excesivos.
- Errores de red/5xx → reintentos con backoff hasta RQ_REACTIONS_MAX_ATTEMPTS(si definido) o los globales.
- Falta de messageIden el evento → omitir reacciones ytask_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 xsin IDs) → se encola ⚠️.
- DM con REACTIONS_SCOPE=groups→ no se encola.
- REACTIONS_ENABLED=false→ no se encola.
 
- Grupo allowed, 
- task_origins:
- Tras /t nuevaen grupo, existetask_origins(task_id, chat_id, message_id).
 
- Tras 
- Completar → ✅:
- Dentro de TTL → se encola ✅ con el messageIdde origen.
- Fuera de TTL → no se encola.
- Completar dos veces → solo 1 job ✅ (idempotencia).
 
- Dentro de TTL → se encola ✅ con el 
- ResponseQueue:
- Jobs kind:'reaction'llaman a/message/sendReaction…(no a sendText).
- Manejo de 4xx/5xx conforme a política de reintentos.
 
- Jobs 
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_ENABLEDgradualmente 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.