diff --git a/docs/plan-reacciones-bot.md b/docs/plan-reacciones-bot.md index 1a84a8f..cf783d2 100644 --- a/docs/plan-reacciones-bot.md +++ b/docs/plan-reacciones-bot.md @@ -81,7 +81,7 @@ Envío (Evolution API): - Body: ``` { - "key": { "remoteJid": "", "fromMe": true, "id": "" }, + "key": { "remoteJid": "", "fromMe": false, "id": "" }, "reaction": "" } ``` diff --git a/src/server.ts b/src/server.ts index a4c0294..bbf8bdc 100644 --- a/src/server.ts +++ b/src/server.ts @@ -478,20 +478,21 @@ export class WebhookServer { // Delegar el manejo del comando const messageId = typeof data?.key?.id === 'string' ? data.key.id : null; - const responses = await CommandService.handle({ + const outcome = await CommandService.handleWithOutcome({ sender: normalizedSenderId, groupId: data.key.remoteJid, message: messageText, mentions, messageId: messageId || undefined }); + const responses = outcome.responses; // Encolar respuestas si las hay if (responses.length > 0) { await ResponseQueue.add(responses); } - // Reaccionar al mensaje del comando (Fase 1) + // Reaccionar al mensaje del comando con outcome explícito try { const reactionsEnabled = String(process.env.REACTIONS_ENABLED || 'false').toLowerCase(); const enabled = ['true','1','yes','on'].includes(reactionsEnabled); @@ -514,20 +515,7 @@ export class WebhookServer { } catch {} } - // Heurística de outcome: si alguna respuesta sugiere error → ⚠️ - const anyError = (responses || []).some(r => { - const m = String(r?.message || '').toLowerCase(); - return m.startsWith('ℹ️ uso:'.toLowerCase()) - || m.includes('uso:'.toLowerCase()) - || m.includes('no puedes') - || m.includes('no permitido') - || m.includes('no encontrada') - || m.includes('comando no reconocido') - || (m.includes('acción') && m.includes('no reconocida')) - || m.includes('⚠️'.toLowerCase()); - }); - const emoji = anyError ? '⚠️' : '🤖'; - + const emoji = outcome.ok ? '🤖' : '⚠️'; await ResponseQueue.enqueueReaction(data.key.remoteJid, messageId, emoji); } catch (e) { // No romper el flujo por errores de reacción diff --git a/src/services/command.ts b/src/services/command.ts index a65dc01..8114512 100644 --- a/src/services/command.ts +++ b/src/services/command.ts @@ -26,6 +26,12 @@ export type CommandResponse = { mentions?: string[]; // full JIDs to mention in the outgoing message }; +export type CommandOutcome = { + responses: CommandResponse[]; + ok: boolean; + createdTaskIds?: number[]; +}; + export class CommandService { static dbInstance: Database = db; @@ -1306,12 +1312,17 @@ export class CommandService { } static async handle(context: CommandContext): Promise { + const outcome = await this.handleWithOutcome(context); + return outcome.responses; + } + + static async handleWithOutcome(context: CommandContext): Promise { const msg = (context.message || '').trim(); if (!/^\/(tarea|t)\b/i.test(msg)) { - return []; + return { responses: [], ok: true }; } - // Gating de grupos en modo 'enforce' (Etapa 3) cuando CommandService se invoca directamente + // Gating de grupos en modo 'enforce' (cuando CommandService se invoca directamente) if (isGroupId(context.groupId)) { try { (AllowedGroups as any).dbInstance = this.dbInstance; } catch {} const mode = String(process.env.GROUP_GATING_MODE || 'off').toLowerCase(); @@ -1319,7 +1330,7 @@ export class CommandService { try { if (!AllowedGroups.isAllowed(context.groupId)) { try { Metrics.inc('commands_blocked_total'); } catch {} - return []; + return { responses: [], ok: true }; } } catch { // Si falla el check, ser permisivos @@ -1328,12 +1339,79 @@ export class CommandService { } try { - return await this.processTareaCommand(context); + const responses = await this.processTareaCommand(context); + + // Clasificación explícita del outcome (evita lógica en server) + const tokens = msg.split(/\s+/); + const rawAction = (tokens[1] || '').toLowerCase(); + const ACTION_ALIASES: Record = { + 'n': 'nueva', + 'nueva': 'nueva', + 'crear': 'nueva', + '+': 'nueva', + 'ver': 'ver', + 'mostrar': 'ver', + 'listar': 'ver', + 'ls': 'ver', + 'x': 'completar', + 'hecho': 'completar', + 'completar': 'completar', + 'done': 'completar', + 'tomar': 'tomar', + 'claim': 'tomar', + 'asumir': 'tomar', + 'asumo': 'tomar', + 'soltar': 'soltar', + 'unassign': 'soltar', + 'dejar': 'soltar', + 'liberar': 'soltar', + 'renunciar': 'soltar', + 'ayuda': 'ayuda', + 'help': 'ayuda', + '?': 'ayuda', + 'config': 'configurar', + 'configurar': 'configurar', + 'web': 'web' + }; + const action = ACTION_ALIASES[rawAction] || rawAction; + + // Casos explícitos considerados éxito + if (!action || action === 'ayuda' || action === 'web') { + return { responses, ok: true }; + } + + const lowerMsgs = (responses || []).map(r => String(r?.message || '').toLowerCase()); + + const isOkException = (m: string) => + m.includes('ya estaba completada') || + m.includes('ya la tenías') || + m.includes('no la tenías'); + + const isErrorMsg = (m: string) => + m.startsWith('ℹ️ uso:'.toLowerCase()) || + m.includes('uso:') || + m.includes('no puedes') || + m.includes('no permitido') || + m.includes('no encontrada') || + m.includes('comando no reconocido'); + + let hasError = false; + for (const m of lowerMsgs) { + if (isErrorMsg(m) && !isOkException(m)) { + hasError = true; + break; + } + } + + return { responses, ok: !hasError }; } catch (error) { - return [{ - recipient: context.sender, - message: 'Error processing command' - }]; + return { + responses: [{ + recipient: context.sender, + message: 'Error processing command' + }], + ok: false + }; } } } diff --git a/src/services/response-queue.ts b/src/services/response-queue.ts index d18c533..adde0eb 100644 --- a/src/services/response-queue.ts +++ b/src/services/response-queue.ts @@ -115,6 +115,7 @@ export const ResponseQueue = { // Construir JSON canónico const metaObj = { kind: 'reaction', emoji, chatId, messageId }; const metadata = JSON.stringify(metaObj); + const emojiLabel = emoji === '✅' ? 'check' : (emoji === '🤖' ? 'robot' : (emoji === '⚠️' ? 'warn' : 'other')); // Ventana de 24h const cutoff = this.futureIso(-24 * 60 * 60 * 1000); @@ -137,6 +138,7 @@ export const ResponseQueue = { INSERT INTO response_queue (recipient, message, metadata, next_attempt_at) VALUES (?, ?, ?, ?) `).run(chatId, '', metadata, this.nowIso()); + try { Metrics.inc('reactions_enqueued_total', 1, { emoji: emojiLabel }); } catch {} } catch (err) { console.error('Failed to enqueue reaction:', err); throw err; diff --git a/src/tasks/service.ts b/src/tasks/service.ts index fb7a2a9..de4fc12 100644 --- a/src/tasks/service.ts +++ b/src/tasks/service.ts @@ -318,7 +318,6 @@ export class TaskService { if (allowed) { // Encolar reacción ✅ con idempotencia; no bloquear si falla ResponseQueue.enqueueReaction(chatId, String(origin.message_id), '✅') - .then(() => { try { Metrics.inc('reactions_enqueued_total', 1, { emoji: 'check' }); } catch {} }) .catch(() => {}); } }