diff --git a/src/modules/whatsapp/whatsapp.controller.ts b/src/modules/whatsapp/whatsapp.controller.ts index 4866310..8b4c1f7 100644 --- a/src/modules/whatsapp/whatsapp.controller.ts +++ b/src/modules/whatsapp/whatsapp.controller.ts @@ -1,4 +1,5 @@ -import { Controller, Get, Post, Body, Delete, Param, ForbiddenException, Query } from '@nestjs/common'; +import { Controller, Get, Post, Body, Delete, Param, ForbiddenException, Query, UseInterceptors, UploadedFile, BadRequestException } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { WhatsappService } from './whatsapp.service'; import { AttendanceAssignmentService } from '../attendance/attendance-assignment.service'; @@ -64,6 +65,21 @@ export class WhatsappController { return this.whatsappService.sendMessage(body.to, body.message, body.media, body.senderName); } + @ApiOperation({ summary: 'Envia mídia no WhatsApp' }) + @ApiResponse({ status: 201, description: 'Mídia enviada.' }) + @Post('send-media') + @UseInterceptors(FileInterceptor('file', { limits: { fileSize: 100 * 1024 * 1024 } })) + async sendMediaMessage( + @UploadedFile() file: Express.Multer.File, + @Body('to') to: string, + @Body('senderName') senderName?: string, + @Body('caption') caption?: string, + ) { + if (!file) throw new BadRequestException('Arquivo não enviado.'); + if (!to) throw new BadRequestException('Destinatário (to) obrigatório.'); + return this.whatsappService.sendMediaMessage(to, file.buffer, file.mimetype, senderName, caption); + } + @ApiOperation({ summary: 'Inicia atendimento ativo' }) @ApiResponse({ status: 201, description: 'Atendimento ativo iniciado.' }) @Post('start-attendance') diff --git a/src/modules/whatsapp/whatsapp.service.ts b/src/modules/whatsapp/whatsapp.service.ts index ea7dce2..c94e433 100644 --- a/src/modules/whatsapp/whatsapp.service.ts +++ b/src/modules/whatsapp/whatsapp.service.ts @@ -100,6 +100,109 @@ export class WhatsappService { throw new NotImplementedException('Busca de mídia não disponível na Meta API ainda.'); } + async sendMediaMessage( + to: string, + buffer: Buffer, + mimeType: string, + senderName?: string, + caption?: string, + ): Promise { + const { access_token: token, phone_number_id: phoneNumberId, api_version: apiVersion } = + await this.configService.getConfig(); + + if (!token || !phoneNumberId) { + throw new Error('Token e Phone Number ID do WhatsApp não configurados. Acesse Admin → Canais e Integração → WhatsApp → Configurar.'); + } + + const mediaPath = await this.mediaStorageService.upload(buffer, mimeType); + const mediaType = this.mimeTypeToMediaType(mimeType); + + const form = new FormData(); + form.append('file', new Blob([buffer], { type: mimeType }), `media`); + form.append('type', mimeType); + form.append('messaging_product', 'whatsapp'); + + const uploadResponse = await fetch( + `https://graph.facebook.com/${apiVersion}/${phoneNumberId}/media`, + { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body: form }, + ); + + if (!uploadResponse.ok) { + const error = await uploadResponse.json().catch(() => ({})); + throw new Error(`Meta API erro ao fazer upload de mídia: ${JSON.stringify(error)}`); + } + + const { id: mediaId } = await uploadResponse.json() as any; + + const mediaPayload: any = { id: mediaId }; + if (caption && mediaType !== 'audio') mediaPayload.caption = senderName ? `*Atendente: ${senderName}*\n\n${caption}` : caption; + + const messageResponse = await fetch( + `https://graph.facebook.com/${apiVersion}/${phoneNumberId}/messages`, + { + method: 'POST', + headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messaging_product: 'whatsapp', + recipient_type: 'individual', + to, + type: mediaType, + [mediaType]: mediaPayload, + }), + }, + ); + + if (!messageResponse.ok) { + const error = await messageResponse.json().catch(() => ({})); + throw new Error(`Meta API erro ao enviar mídia para ${to}: ${JSON.stringify(error)}`); + } + + const messageResult = await messageResponse.json() as any; + const wamid: string | undefined = messageResult?.messages?.[0]?.id; + this.logger.log(`[Meta] Mídia (${mediaType}) enviada para ${to}: ${wamid}`); + + const senderLabel = senderName ?? 'Sistema'; + const bodyText = caption ? (senderName ? `*Atendente: ${senderLabel}*\n\n${caption}` : caption) : `[${mediaType}]`; + const timestamp = Math.floor(Date.now() / 1000); + + if (wamid) { + await this.mensagensRepository.save({ + wamid, + chatId: to, + fromMe: true, + type: mediaType, + body: bodyText, + senderName: senderLabel, + status: 'sent', + timestamp, + mediaPath, + mediaMimeType: mimeType, + }); + } + + this.gateway.emitNewMessage({ + from: to, + body: bodyText, + timestamp, + isGroupMsg: false, + id: wamid || '', + fromMe: true, + notifyName: senderLabel, + hasMedia: true, + mediaUrl: this.mediaStorageService.getUrl(mediaPath), + mediaMimeType: mimeType, + }); + + return messageResult; + } + + private mimeTypeToMediaType(mimeType: string): string { + if (mimeType.startsWith('image/')) return 'image'; + if (mimeType.startsWith('audio/')) return 'audio'; + if (mimeType.startsWith('video/')) return 'video'; + return 'document'; + } + private async getCustomerContact(chatId: string) { try { const contact = await this.contactsService.getContact(chatId);