skailan-conversations
Version:
Servicio de conversaciones y mensajería para Skailan
137 lines • 7.18 kB
JavaScript
import crypto from 'crypto';
import { StartConversation } from '../../app/use-cases/StartConversation';
import { CreateMessage } from '../../app/use-cases/CreateMessage';
import { ChannelPrismaRepository } from '../../infra/database/prisma/ChannelPrismaRepository';
import { ConversationPrismaRepository } from '../../infra/database/prisma/ConversationPrismaRepository';
import { MessagePrismaRepository } from '../../infra/database/prisma/MessagePrismaRepository';
// Helper para verificar la firma del webhook de Meta
const verifySignature = (req, res, next) => {
const signature = req.headers['x-hub-signature-256'];
if (!signature) {
console.warn('Webhook received without signature.');
return res.status(400).send('Signature missing');
}
const [algorithm, hash] = signature.toString().split('=');
if (algorithm !== 'sha256') {
console.warn('Unsupported signature algorithm.');
return res.status(400).send('Unsupported signature algorithm');
}
const hmac = crypto.createHmac('sha256', process.env.WHATSAPP_APP_SECRET || '');
hmac.update(JSON.stringify(req.body));
const digest = hmac.digest('hex');
if (digest !== hash) {
console.warn('Invalid signature.');
return res.status(403).send('Invalid signature');
}
next();
};
export const handleWhatsappWebhook = async (req, res) => {
const organizationId = req.organization?.id;
if (!organizationId) {
return res.status(400).json({ error: 'Organization ID not found in request.' });
}
const prisma = req.tenantPrisma;
if (!prisma) {
return res.status(500).json({ error: 'Prisma client not initialized for tenant.' });
}
const channelRepository = new ChannelPrismaRepository(prisma);
const conversationRepository = new ConversationPrismaRepository(prisma);
const messageRepository = new MessagePrismaRepository(prisma);
// Meta envía un array de entradas
for (const entry of req.body.entry) {
for (const change of entry.changes) {
if (change.field === 'messages') {
for (const message of change.value.messages) {
// Ignorar mensajes de estado o mensajes enviados por la propia aplicación
if (message.type === 'status' || message.from === process.env.WHATSAPP_PHONE_NUMBER_ID) {
continue;
}
try {
// 1. Identificar o crear el contacto y la conversación
const startConversationUseCase = new StartConversation(conversationRepository, channelRepository);
// Asumimos que el channelId se puede obtener del config del webhook o de alguna manera
// Por ahora, lo hardcodearemos o lo buscaremos por tipo
const whatsappChannel = await channelRepository.findByType('whatsapp', organizationId);
if (!whatsappChannel) {
console.error('WhatsApp channel not found for organization', organizationId);
continue;
}
const conversation = await startConversationUseCase.execute({
organizationId,
channelId: whatsappChannel.id,
contactExternalId: message.from, // Número de teléfono del remitente
// Puedes intentar obtener el nombre del contacto si está disponible en el payload
// contactName: message.contacts ? message.contacts[0].profile.name : undefined,
});
// 2. Crear el mensaje
const createMessageUseCase = new CreateMessage(messageRepository, conversationRepository);
let messageContent = '';
let messageType = message.type;
let mediaUrl;
switch (message.type) {
case 'text':
messageContent = message.text.body;
break;
case 'image':
messageContent = message.image.caption || '(Imagen)';
mediaUrl = message.image.id; // Esto es un ID de Meta, necesitará ser descargado
break;
case 'video':
messageContent = message.video.caption || '(Video)';
mediaUrl = message.video.id;
break;
case 'audio':
messageContent = '(Audio)';
mediaUrl = message.audio.id;
break;
case 'document':
messageContent = message.document.filename || '(Documento)';
mediaUrl = message.document.id;
break;
// Añadir más tipos de mensajes según sea necesario
default:
messageContent = `(Mensaje de tipo no soportado: ${message.type})`;
break;
}
await createMessageUseCase.execute({
organizationId,
conversationId: conversation.id,
senderType: 'contact',
senderId: message.from, // O el ID del contacto si ya lo tienes
type: messageType,
content: messageContent,
mediaUrl: mediaUrl,
externalMessageId: message.id,
timestamp: new Date(message.timestamp * 1000), // Convertir a milisegundos
});
}
catch (error) {
console.error('Error processing WhatsApp message:', error.message);
// Dependiendo de la política, podrías enviar una respuesta de error a Meta
}
}
}
}
}
res.status(200).send('EVENT_RECEIVED');
};
// Endpoint para la verificación de webhooks de Meta (GET request)
export const verifyWhatsappWebhook = (req, res) => {
const VERIFY_TOKEN = process.env.WHATSAPP_VERIFY_TOKEN;
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode && token) {
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
console.log('WEBHOOK_VERIFIED');
res.status(200).send(challenge);
}
else {
res.status(403).send('Verification failed');
}
}
else {
res.status(400).send('Missing parameters');
}
};
//# sourceMappingURL=whatsappWebhookController.js.map