From bc256c49999e44250565c564bd727a3bfbc46f3c Mon Sep 17 00:00:00 2001 From: brobert Date: Tue, 21 Oct 2025 00:57:12 +0200 Subject: [PATCH] feat: incluir participant y fromMe en key de reacciones para grupos Co-authored-by: aider (openrouter/openai/gpt-5) --- src/db/migrations/index.ts | 18 ++++++++++++++++++ src/server.ts | 12 ++++++++++-- src/services/command.ts | 13 ++++++++++++- src/services/response-queue.ts | 15 +++++++++++---- src/tasks/service.ts | 15 +++++++++++++-- 5 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/db/migrations/index.ts b/src/db/migrations/index.ts index 009f326..e1540a0 100644 --- a/src/db/migrations/index.ts +++ b/src/db/migrations/index.ts @@ -472,5 +472,23 @@ export const migrations: Migration[] = [ db.exec(`CREATE INDEX IF NOT EXISTS idx_task_origins_task ON task_origins (task_id);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_task_origins_chat_msg ON task_origins (chat_id, message_id);`); } + }, + { + version: 18, + name: 'task-origins-participant-fromme', + checksum: 'v18-task-origins-participant-fromme-2025-10-21', + up: (db: Database) => { + try { + const cols = db.query(`PRAGMA table_info(task_origins)`).all() as any[]; + const hasParticipant = Array.isArray(cols) && cols.some((c: any) => String(c.name) === 'participant'); + if (!hasParticipant) { + db.exec(`ALTER TABLE task_origins ADD COLUMN participant TEXT NULL;`); + } + const hasFromMe = Array.isArray(cols) && cols.some((c: any) => String(c.name) === 'from_me'); + if (!hasFromMe) { + db.exec(`ALTER TABLE task_origins ADD COLUMN from_me INTEGER NULL;`); + } + } catch {} + } } ]; diff --git a/src/server.ts b/src/server.ts index f7cd34b..6d833ae 100644 --- a/src/server.ts +++ b/src/server.ts @@ -478,12 +478,17 @@ export class WebhookServer { // Delegar el manejo del comando const messageId = typeof data?.key?.id === 'string' ? data.key.id : null; + const participantForKey = typeof data?.key?.participantAlt === 'string' + ? data.key.participantAlt + : (typeof data?.key?.participant === 'string' ? data.key.participant : null); const outcome = await CommandService.handleWithOutcome({ sender: normalizedSenderId, groupId: data.key.remoteJid, message: messageText, mentions, - messageId: messageId || undefined + messageId: messageId || undefined, + participant: participantForKey || undefined, + fromMe: !!data?.key?.fromMe }); const responses = outcome.responses; @@ -516,7 +521,10 @@ export class WebhookServer { } const emoji = outcome.ok ? '🤖' : '⚠️'; - await ResponseQueue.enqueueReaction(data.key.remoteJid, messageId, emoji); + const participant = typeof data?.key?.participantAlt === 'string' + ? data.key.participantAlt + : (typeof data?.key?.participant === 'string' ? data.key.participant : undefined); + await ResponseQueue.enqueueReaction(data.key.remoteJid, messageId, emoji, { participant, fromMe: !!data?.key?.fromMe }); } catch (e) { // No romper el flujo por errores de reacción if (process.env.NODE_ENV !== 'test') { diff --git a/src/services/command.ts b/src/services/command.ts index 8114512..751c86a 100644 --- a/src/services/command.ts +++ b/src/services/command.ts @@ -18,6 +18,8 @@ type CommandContext = { message: string; // raw message text mentions: string[]; // array of raw JIDs mentioned messageId?: string; // id del mensaje origen (para task_origins y reacciones) + participant?: string; // JID del autor del mensaje origen (en grupos) + fromMe?: boolean; // si el mensaje origen fue enviado por la instancia }; export type CommandResponse = { @@ -1216,10 +1218,19 @@ export class CommandService { // Registrar origen del comando para esta tarea (Fase 1) try { if (groupIdToUse && isGroupId(groupIdToUse) && context.messageId) { - this.dbInstance.prepare(` + const participant = typeof context.participant === 'string' ? context.participant : null; + const fromMe = typeof context.fromMe === 'boolean' ? (context.fromMe ? 1 : 0) : null; + try { + this.dbInstance.prepare(` + INSERT OR IGNORE INTO task_origins (task_id, chat_id, message_id, participant, from_me) + VALUES (?, ?, ?, ?, ?) + `).run(taskId, groupIdToUse, context.messageId, participant, fromMe); + } catch { + this.dbInstance.prepare(` INSERT OR IGNORE INTO task_origins (task_id, chat_id, message_id) VALUES (?, ?, ?) `).run(taskId, groupIdToUse, context.messageId); + } } } catch {} diff --git a/src/services/response-queue.ts b/src/services/response-queue.ts index adde0eb..11442bf 100644 --- a/src/services/response-queue.ts +++ b/src/services/response-queue.ts @@ -108,12 +108,14 @@ export const ResponseQueue = { }, // Encolar una reacción con idempotencia (24h) usando metadata canónica - async enqueueReaction(chatId: string, messageId: string, emoji: string): Promise { + async enqueueReaction(chatId: string, messageId: string, emoji: string, opts?: { participant?: string; fromMe?: boolean }): Promise { try { if (!chatId || !messageId || !emoji) return; - // Construir JSON canónico - const metaObj = { kind: 'reaction', emoji, chatId, messageId }; + // Construir JSON canónico (incluir participant/fromMe si están disponibles) + const metaObj: any = { kind: 'reaction', emoji, chatId, messageId }; + if (typeof opts?.fromMe === 'boolean') metaObj.fromMe = !!opts.fromMe; + if (opts?.participant) metaObj.participant = opts.participant; const metadata = JSON.stringify(metaObj); const emojiLabel = emoji === '✅' ? 'check' : (emoji === '🤖' ? 'robot' : (emoji === '⚠️' ? 'warn' : 'other')); @@ -173,8 +175,13 @@ export const ResponseQueue = { if (!chatId || !messageId || !emoji) { return { ok: false, error: 'invalid_reaction_metadata' }; } + const fromMe = !!meta.fromMe; + const key: any = { remoteJid: chatId, fromMe, id: messageId }; + if (meta.participant) { + key.participant = String(meta.participant); + } const payload = { - key: { remoteJid: chatId, fromMe: false, id: messageId }, + key, reaction: emoji }; try { diff --git a/src/tasks/service.ts b/src/tasks/service.ts index de4fc12..b3014c4 100644 --- a/src/tasks/service.ts +++ b/src/tasks/service.ts @@ -285,11 +285,20 @@ export class TaskService { const rxEnabled = String(process.env.REACTIONS_ENABLED || 'false').toLowerCase(); const enabled = ['true','1','yes','on'].includes(rxEnabled); if (enabled) { - const origin = this.dbInstance.prepare(` + let origin: any = null; + try { + origin = this.dbInstance.prepare(` + SELECT chat_id, message_id, created_at, participant, from_me + FROM task_origins + WHERE task_id = ? + `).get(taskId) as any; + } catch { + origin = this.dbInstance.prepare(` SELECT chat_id, message_id, created_at FROM task_origins WHERE task_id = ? `).get(taskId) as any; + } if (origin && origin.chat_id && origin.message_id) { const chatId = String(origin.chat_id); @@ -317,7 +326,9 @@ export class TaskService { } if (allowed) { // Encolar reacción ✅ con idempotencia; no bloquear si falla - ResponseQueue.enqueueReaction(chatId, String(origin.message_id), '✅') + const participant = origin && origin.participant ? String(origin.participant) : undefined; + const fromMe = (origin && (origin.from_me === 1 || origin.from_me === true)) ? true : undefined; + ResponseQueue.enqueueReaction(chatId, String(origin.message_id), '✅', { participant, fromMe }) .catch(() => {}); } }