feat: habilita menciones en mensajes y limpia descripciones de tareas

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
pull/1/head
borja 2 months ago
parent e85a9e47d1
commit 6c9f744f1f

@ -92,6 +92,17 @@ export function initializeDatabase(instance: Database) {
CREATE INDEX IF NOT EXISTS idx_response_queue_status_created_at CREATE INDEX IF NOT EXISTS idx_response_queue_status_created_at
ON 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);
}
} }
/** /**

@ -6,7 +6,7 @@ import { ResponseQueue } from './services/response-queue';
import { TaskService } from './tasks/service'; import { TaskService } from './tasks/service';
import { WebhookManager } from './services/webhook-manager'; import { WebhookManager } from './services/webhook-manager';
import { normalizeWhatsAppId } from './utils/whatsapp'; 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 // Bun is available globally when running under Bun runtime
declare global { declare global {
@ -196,6 +196,9 @@ export class WebhookServer {
static async start() { static async start() {
this.validateEnv(); this.validateEnv();
// Ensure database schema and migrations are applied
initializeDatabase(this.dbInstance);
const PORT = process.env.PORT || '3007'; const PORT = process.env.PORT || '3007';
console.log('✅ Environment variables validated'); console.log('✅ Environment variables validated');

@ -14,12 +14,13 @@ type CommandContext = {
export type CommandResponse = { export type CommandResponse = {
recipient: string; recipient: string;
message: string; message: string;
mentions?: string[]; // full JIDs to mention in the outgoing message
}; };
export class CommandService { export class CommandService {
static dbInstance: Database = db; static dbInstance: Database = db;
private static parseNueva(message: string): { private static parseNueva(message: string, mentionsNormalized: string[]): {
action: string; action: string;
description: string; description: string;
dueDate: string | null; dueDate: string | null;
@ -44,15 +45,23 @@ export class CommandService {
let dueDate: string | null = null; let dueDate: string | null = null;
let descriptionTokens: string[] = []; let descriptionTokens: string[] = [];
const isMentionToken = (token: string) => token.startsWith('@');
if (dateIndices.length > 0) { if (dateIndices.length > 0) {
const last = dateIndices[dateIndices.length - 1]; const last = dateIndices[dateIndices.length - 1];
dueDate = last.text; dueDate = last.text;
for (let i = 2; i < parts.length; i++) { for (let i = 2; i < parts.length; i++) {
if (i === last.index) continue; 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 { } 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(); const description = descriptionTokens.join(' ').trim();
@ -79,7 +88,14 @@ export class CommandService {
} }
// Parseo específico de "nueva" // 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 // Asegurar creador
const createdBy = ensureUserExists(context.sender, this.dbInstance); 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 // Normalizar menciones y excluir duplicados y el número del bot
const botNumber = process.env.CHATBOT_PHONE_NUMBER || ''; const botNumber = process.env.CHATBOT_PHONE_NUMBER || '';
const assigneesNormalized = Array.from(new Set( const assigneesNormalized = Array.from(new Set(
(context.mentions || []) mentionsNormalizedFromContext
.map(j => normalizeWhatsAppId(j))
.filter((id): id is string => !!id)
.filter(id => !botNumber || id !== botNumber) .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 = const resp =
`✅ Tarea ${taskId} creada: "${description || '(sin descripción)'}"` + `✅ Tarea ${taskId} creada: "${description || '(sin descripción)'}"` +
(dueDate ? ` (vence ${dueDate})` : '') + (dueDate ? ` (vence ${dueDate})` : '') +
@ -131,7 +149,8 @@ export class CommandService {
return [{ return [{
recipient: createdBy, recipient: createdBy,
message: resp message: resp,
mentions: mentionsForSending.length > 0 ? mentionsForSending : undefined
}]; }];
} }

@ -4,12 +4,14 @@ import { db } from '../db';
type QueuedResponse = { type QueuedResponse = {
recipient: string; recipient: string;
message: string; message: string;
mentions?: string[]; // full JIDs to mention (e.g., '346xxx@s.whatsapp.net')
}; };
type ClaimedItem = { type ClaimedItem = {
id: number; id: number;
recipient: string; recipient: string;
message: string; message: string;
metadata?: string | null; // JSON-encoded metadata (e.g., { mentioned: [...] })
}; };
export const ResponseQueue = { export const ResponseQueue = {
@ -40,13 +42,17 @@ export const ResponseQueue = {
} }
const insert = this.dbInstance.prepare(` const insert = this.dbInstance.prepare(`
INSERT INTO response_queue (recipient, message) INSERT INTO response_queue (recipient, message, metadata)
VALUES (?, ?) VALUES (?, ?, ?)
`); `);
this.dbInstance.transaction((rows: QueuedResponse[]) => { this.dbInstance.transaction((rows: QueuedResponse[]) => {
for (const r of rows) { 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); })(filtered);
@ -76,13 +82,27 @@ export const ResponseQueue = {
const url = `${baseUrl}/message/sendText/${instance}`; const url = `${baseUrl}/message/sendText/${instance}`;
try { 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, { const response = await fetch(url, {
method: 'POST', method: 'POST',
headers: this.getHeaders(), headers: this.getHeaders(),
body: JSON.stringify({ body: JSON.stringify(payload),
number: item.recipient,
text: item.message,
}),
}); });
if (!response.ok) { if (!response.ok) {
@ -110,7 +130,7 @@ export const ResponseQueue = {
ORDER BY created_at, id ORDER BY created_at, id
LIMIT ? LIMIT ?
) )
RETURNING id, recipient, message RETURNING id, recipient, message, metadata
`).all(limit) as ClaimedItem[]; `).all(limit) as ClaimedItem[];
return rows || []; return rows || [];

@ -82,6 +82,18 @@ describe('ResponseQueue (persistent add)', () => {
expect(rows[0].message).toBe('ok'); 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 () => { test('should throw if database error occurs (e.g., missing table)', async () => {
// Provocar error: eliminar tabla // Provocar error: eliminar tabla
testDb.exec('DROP TABLE response_queue'); testDb.exec('DROP TABLE response_queue');

Loading…
Cancel
Save