From 6c9f744f1fadd03253ace7a0fea86ac008d21020 Mon Sep 17 00:00:00 2001 From: borja Date: Fri, 5 Sep 2025 23:54:12 +0200 Subject: [PATCH] feat: habilita menciones en mensajes y limpia descripciones de tareas Co-authored-by: aider (openrouter/openai/gpt-5) --- src/db.ts | 11 +++++++ src/server.ts | 5 ++- src/services/command.ts | 37 ++++++++++++++++------ src/services/response-queue.ts | 36 ++++++++++++++++----- tests/unit/services/response-queue.test.ts | 12 +++++++ 5 files changed, 83 insertions(+), 18 deletions(-) diff --git a/src/db.ts b/src/db.ts index 54428a3..293eb49 100644 --- a/src/db.ts +++ b/src/db.ts @@ -92,6 +92,17 @@ export function initializeDatabase(instance: Database) { CREATE INDEX IF NOT EXISTS idx_response_queue_status_created_at ON response_queue (status, created_at); `); + + // Migration: ensure 'metadata' column exists on response_queue for message options (e.g., mentions) + try { + const cols = instance.query(`PRAGMA table_info('response_queue')`).all() as any[]; + const hasMetadata = Array.isArray(cols) && cols.some((c: any) => c.name === 'metadata'); + if (!hasMetadata) { + instance.exec(`ALTER TABLE response_queue ADD COLUMN metadata TEXT NULL;`); + } + } catch (e) { + console.warn('[initializeDatabase] Skipped adding response_queue.metadata column:', e); + } } /** diff --git a/src/server.ts b/src/server.ts index cd73da1..d62d236 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,7 +6,7 @@ import { ResponseQueue } from './services/response-queue'; import { TaskService } from './tasks/service'; import { WebhookManager } from './services/webhook-manager'; import { normalizeWhatsAppId } from './utils/whatsapp'; -import { ensureUserExists, db } from './db'; +import { ensureUserExists, db, initializeDatabase } from './db'; // Bun is available globally when running under Bun runtime declare global { @@ -196,6 +196,9 @@ export class WebhookServer { static async start() { this.validateEnv(); + // Ensure database schema and migrations are applied + initializeDatabase(this.dbInstance); + const PORT = process.env.PORT || '3007'; console.log('✅ Environment variables validated'); diff --git a/src/services/command.ts b/src/services/command.ts index 9bcc821..7302fbc 100644 --- a/src/services/command.ts +++ b/src/services/command.ts @@ -14,12 +14,13 @@ type CommandContext = { export type CommandResponse = { recipient: string; message: string; + mentions?: string[]; // full JIDs to mention in the outgoing message }; export class CommandService { static dbInstance: Database = db; - private static parseNueva(message: string): { + private static parseNueva(message: string, mentionsNormalized: string[]): { action: string; description: string; dueDate: string | null; @@ -44,15 +45,23 @@ export class CommandService { let dueDate: string | null = null; let descriptionTokens: string[] = []; + const isMentionToken = (token: string) => token.startsWith('@'); + if (dateIndices.length > 0) { const last = dateIndices[dateIndices.length - 1]; dueDate = last.text; for (let i = 2; i < parts.length; i++) { if (i === last.index) continue; - descriptionTokens.push(parts[i]); + const token = parts[i]; + if (isMentionToken(token)) continue; // quitar @menciones del texto + descriptionTokens.push(token); } } else { - descriptionTokens = parts.slice(2); + for (let i = 2; i < parts.length; i++) { + const token = parts[i]; + if (isMentionToken(token)) continue; + descriptionTokens.push(token); + } } const description = descriptionTokens.join(' ').trim(); @@ -79,7 +88,14 @@ export class CommandService { } // Parseo específico de "nueva" - const { description, dueDate } = this.parseNueva(trimmed); + // Normalizar menciones del contexto para parseo y asignaciones + const mentionsNormalizedFromContext = Array.from(new Set( + (context.mentions || []) + .map(j => normalizeWhatsAppId(j)) + .filter((id): id is string => !!id) + )); + + const { description, dueDate } = this.parseNueva(trimmed, mentionsNormalizedFromContext); // Asegurar creador const createdBy = ensureUserExists(context.sender, this.dbInstance); @@ -90,9 +106,7 @@ export class CommandService { // Normalizar menciones y excluir duplicados y el número del bot const botNumber = process.env.CHATBOT_PHONE_NUMBER || ''; const assigneesNormalized = Array.from(new Set( - (context.mentions || []) - .map(j => normalizeWhatsAppId(j)) - .filter((id): id is string => !!id) + mentionsNormalizedFromContext .filter(id => !botNumber || id !== botNumber) )); @@ -123,7 +137,11 @@ export class CommandService { })) ); - const assignedList = assignmentUserIds.join(', '); + const mentionsForSending = ensuredAssignees.length > 0 + ? ensuredAssignees.map(uid => `${uid}@s.whatsapp.net`) + : []; + + const assignedList = assignmentUserIds.map(uid => `@${uid}`).join(' '); const resp = `✅ Tarea ${taskId} creada: "${description || '(sin descripción)'}"` + (dueDate ? ` (vence ${dueDate})` : '') + @@ -131,7 +149,8 @@ export class CommandService { return [{ recipient: createdBy, - message: resp + message: resp, + mentions: mentionsForSending.length > 0 ? mentionsForSending : undefined }]; } diff --git a/src/services/response-queue.ts b/src/services/response-queue.ts index c4bd4c8..a112952 100644 --- a/src/services/response-queue.ts +++ b/src/services/response-queue.ts @@ -4,12 +4,14 @@ import { db } from '../db'; type QueuedResponse = { recipient: string; message: string; + mentions?: string[]; // full JIDs to mention (e.g., '346xxx@s.whatsapp.net') }; type ClaimedItem = { id: number; recipient: string; message: string; + metadata?: string | null; // JSON-encoded metadata (e.g., { mentioned: [...] }) }; export const ResponseQueue = { @@ -40,13 +42,17 @@ export const ResponseQueue = { } const insert = this.dbInstance.prepare(` - INSERT INTO response_queue (recipient, message) - VALUES (?, ?) + INSERT INTO response_queue (recipient, message, metadata) + VALUES (?, ?, ?) `); this.dbInstance.transaction((rows: QueuedResponse[]) => { for (const r of rows) { - insert.run(r.recipient, r.message); + const metadata = + r.mentions && r.mentions.length > 0 + ? JSON.stringify({ mentioned: r.mentions }) + : null; + insert.run(r.recipient, r.message, metadata); } })(filtered); @@ -76,13 +82,27 @@ export const ResponseQueue = { const url = `${baseUrl}/message/sendText/${instance}`; try { + // Build payload, adding mentioned JIDs if present in metadata + const payload: any = { + number: item.recipient, + text: item.message, + }; + + if (item.metadata) { + try { + const parsed = JSON.parse(item.metadata); + if (parsed && Array.isArray(parsed.mentioned) && parsed.mentioned.length > 0) { + payload.mentioned = parsed.mentioned; + } + } catch { + // ignore bad metadata + } + } + const response = await fetch(url, { method: 'POST', headers: this.getHeaders(), - body: JSON.stringify({ - number: item.recipient, - text: item.message, - }), + body: JSON.stringify(payload), }); if (!response.ok) { @@ -110,7 +130,7 @@ export const ResponseQueue = { ORDER BY created_at, id LIMIT ? ) - RETURNING id, recipient, message + RETURNING id, recipient, message, metadata `).all(limit) as ClaimedItem[]; return rows || []; diff --git a/tests/unit/services/response-queue.test.ts b/tests/unit/services/response-queue.test.ts index 7d26ab3..48b6d91 100644 --- a/tests/unit/services/response-queue.test.ts +++ b/tests/unit/services/response-queue.test.ts @@ -82,6 +82,18 @@ describe('ResponseQueue (persistent add)', () => { expect(rows[0].message).toBe('ok'); }); + test('should persist mentions in metadata when provided', async () => { + await ResponseQueue.add([ + { recipient: '555', message: 'hola con menciones', mentions: ['111@s.whatsapp.net', '222@s.whatsapp.net'] }, + ]); + + const row = testDb.query("SELECT metadata FROM response_queue ORDER BY id DESC LIMIT 1").get() as any; + expect(row).toBeTruthy(); + const meta = JSON.parse(row.metadata); + expect(Array.isArray(meta.mentioned)).toBe(true); + expect(meta.mentioned).toEqual(['111@s.whatsapp.net', '222@s.whatsapp.net']); + }); + test('should throw if database error occurs (e.g., missing table)', async () => { // Provocar error: eliminar tabla testDb.exec('DROP TABLE response_queue');