FEAT: Parser de eventos do webhook Meta (messages e statuses)

This commit is contained in:
Rafael Alves Lopes 2026-06-18 13:53:40 -03:00
parent c681814cca
commit 79b55be7ef
2 changed files with 76 additions and 2 deletions

View File

@ -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' };
}

View File

@ -578,4 +578,78 @@ export class WhatsappService implements OnModuleInit {
return `*Atendente: ${cleanSenderName}*\n\n${cleanMessage}`;
}
async handleMetaWebhookEvent(body: any): Promise<void> {
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<void> {
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);
}
}