FEAT: Adiciona endpoint POST /whatsapp/send-media para envio de mídia
- Upload do arquivo para Meta API (multipart) para obter media_id - Armazenamento local via MediaStorageService antes do envio - Suporte a image, audio, video e document via mimeTypeToMediaType - Salva mensagem no banco com media_path e media_mime_type - Emite via Socket.IO para atualizar o chat em tempo real
This commit is contained in:
parent
98df003cd7
commit
30ec6e4b5e
@ -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')
|
||||
|
||||
@ -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<any> {
|
||||
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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user