FEAT: Criação do endpoint POST para envio de media #1
@ -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 { ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { WhatsappService } from './whatsapp.service';
|
import { WhatsappService } from './whatsapp.service';
|
||||||
import { AttendanceAssignmentService } from '../attendance/attendance-assignment.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);
|
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' })
|
@ApiOperation({ summary: 'Inicia atendimento ativo' })
|
||||||
@ApiResponse({ status: 201, description: 'Atendimento ativo iniciado.' })
|
@ApiResponse({ status: 201, description: 'Atendimento ativo iniciado.' })
|
||||||
@Post('start-attendance')
|
@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.');
|
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) {
|
private async getCustomerContact(chatId: string) {
|
||||||
try {
|
try {
|
||||||
const contact = await this.contactsService.getContact(chatId);
|
const contact = await this.contactsService.getContact(chatId);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user