From 79b55be7ef1abd61f138e7615778f7b9f059ed52 Mon Sep 17 00:00:00 2001 From: Rafael Lopes Date: Thu, 18 Jun 2026 13:53:40 -0300 Subject: [PATCH] FEAT: Parser de eventos do webhook Meta (messages e statuses) --- src/modules/whatsapp/whatsapp.controller.ts | 4 +- src/modules/whatsapp/whatsapp.service.ts | 74 +++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/modules/whatsapp/whatsapp.controller.ts b/src/modules/whatsapp/whatsapp.controller.ts index 4d6ed89..8478d1e 100644 --- a/src/modules/whatsapp/whatsapp.controller.ts +++ b/src/modules/whatsapp/whatsapp.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Body, Delete, Param, ForbiddenException, Query, RawBody } from '@nestjs/common'; +import { Controller, Get, Post, Body, Delete, Param, ForbiddenException, Query } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { WhatsappService } from './whatsapp.service'; import { AttendanceAssignmentService } from '../attendance/attendance-assignment.service'; @@ -291,7 +291,7 @@ export class WhatsappController { @ApiOperation({ summary: 'Recebe eventos do webhook Meta' }) @Post('webhook') async receiveWebhook(@Body() body: any) { - console.log('Webhook Meta:', JSON.stringify(body, null, 2)); + await this.whatsappService.handleMetaWebhookEvent(body); return { status: 'ok' }; } diff --git a/src/modules/whatsapp/whatsapp.service.ts b/src/modules/whatsapp/whatsapp.service.ts index f5277ad..fed9f75 100644 --- a/src/modules/whatsapp/whatsapp.service.ts +++ b/src/modules/whatsapp/whatsapp.service.ts @@ -578,4 +578,78 @@ export class WhatsappService implements OnModuleInit { return `*Atendente: ${cleanSenderName}*\n\n${cleanMessage}`; } + async handleMetaWebhookEvent(body: any): Promise { + if (body?.object !== 'whatsapp_business_account') return; + + for (const entry of body?.entry ?? []) { + for (const change of entry?.changes ?? []) { + if (change.field !== 'messages') continue; + + const value = change.value; + + if (value?.messages?.length) { + for (const message of value.messages) { + await this.handleMetaIncomingMessage(message, value.contacts ?? []); + } + } + + if (value?.statuses?.length) { + for (const status of value.statuses) { + this.logger.log(`[Meta] Status de mensagem: ${status.id} → ${status.status} (destinatário: ${status.recipient_id})`); + } + } + } + } + } + + private async handleMetaIncomingMessage(message: any, contacts: any[]): Promise { + const from: string = message.from; + const messageId: string = message.id; + const timestamp: number = parseInt(message.timestamp, 10); + const messageBody: string = message.text?.body?.trim() ?? ''; + + const contact = contacts.find((c) => c.wa_id === from); + const contactName: string = contact?.profile?.name || from; + + if (this.processedIncomingMessages.has(messageId)) return; + this.processedIncomingMessages.add(messageId); + if (this.processedIncomingMessages.size > 1000) { + const [oldest] = this.processedIncomingMessages; + this.processedIncomingMessages.delete(oldest); + } + + this.logger.log(`[Meta] Mensagem recebida de ${from} (${contactName}): ${messageBody || '[mídia]'}`); + + this.gateway.emitNewMessage({ + from, + body: messageBody, + timestamp, + isGroupMsg: false, + id: messageId, + fromMe: false, + notifyName: contactName, + hasMedia: message.type !== 'text', + media: null, + }); + + if (messageBody) { + await this.assignmentService.markCustomerReplied(from); + } + + if (!messageBody) { + this.logger.log(`[Meta] Triagem ignorada para ${from}: mensagem sem texto.`); + return; + } + + const fakeMsg = { + reply: async (text: string) => { + // TODO Fase 2: enviar via Meta Graph API + this.logger.warn(`[Meta] Bot reply pendente de implementação. Mensagem para ${from}: "${text}"`); + }, + _data: { notifyName: contactName }, + }; + + await this.enqueueIncomingRoute(from, fakeMsg, messageBody, messageId); + } + }