FEAT: Implementa download e armazenamento de mídia recebida via webhook
- Detecta mensagens de mídia (image, audio, video, document, sticker) - downloadMetaMedia() busca URL temporária na Meta e baixa o binário - Salva via MediaStorageService antes de persistir e emitir - Socket.IO passa mediaUrl e mediaMimeType ao invés de media: null - Falha no download loga erro mas não bloqueia o salvamento da mensagem
This commit is contained in:
parent
c988a84f97
commit
3f1535b960
@ -5,6 +5,9 @@ import { ContactsService } from '../contacts/contacts.service';
|
|||||||
import { WhatsappTemplateService } from './whatsapp-template.service';
|
import { WhatsappTemplateService } from './whatsapp-template.service';
|
||||||
import { MensagensRepository } from './repositories/mensagens.repository';
|
import { MensagensRepository } from './repositories/mensagens.repository';
|
||||||
import { WhatsappConfigService } from './whatsapp-config.service';
|
import { WhatsappConfigService } from './whatsapp-config.service';
|
||||||
|
import { MediaStorageService } from './media/media-storage.service';
|
||||||
|
|
||||||
|
const MEDIA_TYPES = ['image', 'audio', 'video', 'document', 'sticker'] as const;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WhatsappService {
|
export class WhatsappService {
|
||||||
@ -19,6 +22,7 @@ export class WhatsappService {
|
|||||||
private readonly whatsappTemplateService: WhatsappTemplateService,
|
private readonly whatsappTemplateService: WhatsappTemplateService,
|
||||||
private readonly mensagensRepository: MensagensRepository,
|
private readonly mensagensRepository: MensagensRepository,
|
||||||
private readonly configService: WhatsappConfigService,
|
private readonly configService: WhatsappConfigService,
|
||||||
|
private readonly mediaStorageService: MediaStorageService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private async enqueueIncomingRoute(remoteJid: string, msg: any, messageBody: string, messageId: string) {
|
private async enqueueIncomingRoute(remoteJid: string, msg: any, messageBody: string, messageId: string) {
|
||||||
@ -310,6 +314,7 @@ export class WhatsappService {
|
|||||||
const from: string = message.from;
|
const from: string = message.from;
|
||||||
const messageId: string = message.id;
|
const messageId: string = message.id;
|
||||||
const timestamp: number = parseInt(message.timestamp, 10);
|
const timestamp: number = parseInt(message.timestamp, 10);
|
||||||
|
const messageType: string = message.type ?? 'text';
|
||||||
const messageBody: string = message.text?.body?.trim() ?? '';
|
const messageBody: string = message.text?.body?.trim() ?? '';
|
||||||
|
|
||||||
const contact = contacts.find((c) => c.wa_id === from);
|
const contact = contacts.find((c) => c.wa_id === from);
|
||||||
@ -322,16 +327,35 @@ export class WhatsappService {
|
|||||||
this.processedIncomingMessages.delete(oldest);
|
this.processedIncomingMessages.delete(oldest);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`[Meta] Mensagem recebida de ${from} (${contactName}): ${messageBody || '[mídia]'}`);
|
this.logger.log(`[Meta] Mensagem recebida de ${from} (${contactName}): ${messageBody || `[${messageType}]`}`);
|
||||||
|
|
||||||
|
let mediaPath: string | undefined;
|
||||||
|
let mediaMimeType: string | undefined;
|
||||||
|
|
||||||
|
if ((MEDIA_TYPES as readonly string[]).includes(messageType)) {
|
||||||
|
const mediaObj = message[messageType];
|
||||||
|
if (mediaObj?.id) {
|
||||||
|
try {
|
||||||
|
const { buffer, mimeType } = await this.downloadMetaMedia(mediaObj.id, mediaObj.mime_type);
|
||||||
|
mediaPath = await this.mediaStorageService.upload(buffer, mimeType);
|
||||||
|
mediaMimeType = mimeType;
|
||||||
|
this.logger.log(`[Meta] Mídia salva: ${mediaPath}`);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`[Meta] Falha ao baixar mídia ${mediaObj.id} de ${from}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.mensagensRepository.save({
|
await this.mensagensRepository.save({
|
||||||
wamid: messageId,
|
wamid: messageId,
|
||||||
chatId: from,
|
chatId: from,
|
||||||
fromMe: false,
|
fromMe: false,
|
||||||
type: message.type ?? 'text',
|
type: messageType,
|
||||||
body: messageBody || undefined,
|
body: messageBody || undefined,
|
||||||
senderName: contactName,
|
senderName: contactName,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
mediaPath,
|
||||||
|
mediaMimeType,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.gateway.emitNewMessage({
|
this.gateway.emitNewMessage({
|
||||||
@ -342,16 +366,17 @@ export class WhatsappService {
|
|||||||
id: messageId,
|
id: messageId,
|
||||||
fromMe: false,
|
fromMe: false,
|
||||||
notifyName: contactName,
|
notifyName: contactName,
|
||||||
hasMedia: message.type !== 'text',
|
hasMedia: !!mediaPath,
|
||||||
media: null,
|
mediaUrl: mediaPath ? this.mediaStorageService.getUrl(mediaPath) : null,
|
||||||
|
mediaMimeType: mediaMimeType ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (messageBody) {
|
if (messageBody || mediaPath) {
|
||||||
await this.assignmentService.markCustomerReplied(from);
|
await this.assignmentService.markCustomerReplied(from);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!messageBody) {
|
if (!messageBody) {
|
||||||
this.logger.log(`[Meta] Triagem ignorada para ${from}: mensagem sem texto.`);
|
this.logger.log(`[Meta] Triagem ignorada para ${from}: mensagem de mídia sem texto.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,4 +390,34 @@ export class WhatsappService {
|
|||||||
await this.enqueueIncomingRoute(from, fakeMsg, messageBody, messageId);
|
await this.enqueueIncomingRoute(from, fakeMsg, messageBody, messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async downloadMetaMedia(mediaId: string, webhookMimeType?: string): Promise<{ buffer: Buffer; mimeType: string }> {
|
||||||
|
const { access_token: token, api_version: apiVersion } = await this.configService.getConfig();
|
||||||
|
|
||||||
|
if (!token) throw new Error('Token do WhatsApp não configurado.');
|
||||||
|
|
||||||
|
const metaResponse = await fetch(
|
||||||
|
`https://graph.facebook.com/${apiVersion}/${mediaId}`,
|
||||||
|
{ headers: { Authorization: `Bearer ${token}` } },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!metaResponse.ok) {
|
||||||
|
throw new Error(`Meta API erro ao buscar URL de mídia ${mediaId}: ${metaResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaData = await metaResponse.json() as any;
|
||||||
|
const downloadUrl: string = metaData.url;
|
||||||
|
const mimeType: string = webhookMimeType || metaData.mime_type || 'application/octet-stream';
|
||||||
|
|
||||||
|
const fileResponse = await fetch(downloadUrl, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fileResponse.ok) {
|
||||||
|
throw new Error(`Falha ao baixar arquivo de mídia: ${fileResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arrayBuffer = await fileResponse.arrayBuffer();
|
||||||
|
return { buffer: Buffer.from(arrayBuffer), mimeType };
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user