Merge pull request 'FEAT: Criação do endpoint POST para envio de media' (#1) from feat/send-media into dev
All checks were successful
Deploy Dev — Backend / Deploy para VM Dev (push) Successful in 7s
All checks were successful
Deploy Dev — Backend / Deploy para VM Dev (push) Successful in 7s
Reviewed-on: #1
This commit is contained in:
commit
b1ee393bc2
@ -1,4 +1,23 @@
|
||||
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';
|
||||
|
||||
const ALLOWED_MEDIA_TYPES = new Set([
|
||||
'image/jpeg', 'image/png', 'image/webp', 'image/gif',
|
||||
'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/wav', 'audio/aac', 'audio/amr',
|
||||
'video/mp4', 'video/3gpp',
|
||||
'application/pdf',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/msword',
|
||||
'application/vnd.ms-excel',
|
||||
]);
|
||||
|
||||
const MAX_SIZE_MB: Record<string, number> = {
|
||||
image: 5,
|
||||
audio: 16,
|
||||
video: 16,
|
||||
document: 100,
|
||||
};
|
||||
import { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { WhatsappService } from './whatsapp.service';
|
||||
import { AttendanceAssignmentService } from '../attendance/attendance-assignment.service';
|
||||
@ -64,6 +83,40 @@ 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 },
|
||||
fileFilter: (_req, file, cb) => {
|
||||
if (ALLOWED_MEDIA_TYPES.has(file.mimetype)) {
|
||||
cb(null, true);
|
||||
} else {
|
||||
cb(new BadRequestException(`Tipo de arquivo não suportado: ${file.mimetype}`), false);
|
||||
}
|
||||
},
|
||||
}))
|
||||
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.');
|
||||
|
||||
const mediaType = file.mimetype.startsWith('image/') ? 'image'
|
||||
: file.mimetype.startsWith('audio/') ? 'audio'
|
||||
: file.mimetype.startsWith('video/') ? 'video'
|
||||
: 'document';
|
||||
const limitMB = MAX_SIZE_MB[mediaType];
|
||||
if (file.size > limitMB * 1024 * 1024) {
|
||||
throw new BadRequestException(`Arquivo muito grande. Limite para ${mediaType}: ${limitMB} MB.`);
|
||||
}
|
||||
|
||||
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