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 { MensagensRepository } from './repositories/mensagens.repository';
|
||||
import { WhatsappConfigService } from './whatsapp-config.service';
|
||||
import { MediaStorageService } from './media/media-storage.service';
|
||||
|
||||
const MEDIA_TYPES = ['image', 'audio', 'video', 'document', 'sticker'] as const;
|
||||
|
||||
@Injectable()
|
||||
export class WhatsappService {
|
||||
@ -19,6 +22,7 @@ export class WhatsappService {
|
||||
private readonly whatsappTemplateService: WhatsappTemplateService,
|
||||
private readonly mensagensRepository: MensagensRepository,
|
||||
private readonly configService: WhatsappConfigService,
|
||||
private readonly mediaStorageService: MediaStorageService,
|
||||
) {}
|
||||
|
||||
private async enqueueIncomingRoute(remoteJid: string, msg: any, messageBody: string, messageId: string) {
|
||||
@ -310,6 +314,7 @@ export class WhatsappService {
|
||||
const from: string = message.from;
|
||||
const messageId: string = message.id;
|
||||
const timestamp: number = parseInt(message.timestamp, 10);
|
||||
const messageType: string = message.type ?? 'text';
|
||||
const messageBody: string = message.text?.body?.trim() ?? '';
|
||||
|
||||
const contact = contacts.find((c) => c.wa_id === from);
|
||||
@ -322,16 +327,35 @@ export class WhatsappService {
|
||||
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({
|
||||
wamid: messageId,
|
||||
chatId: from,
|
||||
fromMe: false,
|
||||
type: message.type ?? 'text',
|
||||
type: messageType,
|
||||
body: messageBody || undefined,
|
||||
senderName: contactName,
|
||||
timestamp,
|
||||
mediaPath,
|
||||
mediaMimeType,
|
||||
});
|
||||
|
||||
this.gateway.emitNewMessage({
|
||||
@ -342,16 +366,17 @@ export class WhatsappService {
|
||||
id: messageId,
|
||||
fromMe: false,
|
||||
notifyName: contactName,
|
||||
hasMedia: message.type !== 'text',
|
||||
media: null,
|
||||
hasMedia: !!mediaPath,
|
||||
mediaUrl: mediaPath ? this.mediaStorageService.getUrl(mediaPath) : null,
|
||||
mediaMimeType: mediaMimeType ?? null,
|
||||
});
|
||||
|
||||
if (messageBody) {
|
||||
if (messageBody || mediaPath) {
|
||||
await this.assignmentService.markCustomerReplied(from);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@ -365,4 +390,34 @@ export class WhatsappService {
|
||||
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