almeira-web/src/app/api/webhooks/whatsapp/route.ts

188 lines
8.6 KiB
TypeScript

import { NextResponse } from 'next/server';
import { ID, Query } from 'node-appwrite';
import { InputFile } from 'node-appwrite/file';
import { createAdminClient } from '@/lib/appwrite.server';
import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite';
import { processWhatsAppMessage } from '@/lib/gemini';
import { sendWhatsAppMessage, getBase64FromMediaMessage } from '@/lib/evolution';
// Grupo de publicaciones (tomado del test) o puedes dejarlo abierto a todos los grupos
const TARGET_GROUP_JID = '573134848203-1408546044@g.us';
// Función para obtener el último día del mes actual
function getLastDayOfMonth(): string {
const date = new Date();
const lastDay = new Date(date.getFullYear(), date.getMonth() + 1, 0);
return lastDay.toISOString().split('T')[0];
}
function generateSlug(titulo: string): string {
return titulo.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9\s-]/g, '').trim().replace(/\s+/g, '-').replace(/-+/g, '-').substring(0, 100);
}
function truncateString(str: string, length: number): string {
if (!str) return '';
return str.length > length ? str.substring(0, length - 3) + '...' : str;
}
export async function POST(req: Request) {
try {
const payload = await req.json();
const eventType = payload.event;
// Solo procesamos nuevos mensajes
if (eventType !== 'messages.upsert') {
return NextResponse.json({ success: true, message: 'Ignored event' }, { status: 200 });
}
const data = payload.data;
const remoteJid = data?.key?.remoteJid;
const fromMe = data?.key?.fromMe;
const messageData = data?.message || {};
const rawText = messageData.conversation || messageData.extendedTextMessage?.text || messageData.imageMessage?.caption || messageData.videoMessage?.caption || '';
// Ignorar mensajes generados por el bot para evitar bucle infinito,
// pero permitir tests que el administrador envíe desde el mismo celular del bot.
if (fromMe && (rawText.includes('📰 *Publicación Procesada') || rawText.includes('✅ La publicación ha sido aprobada') || rawText.includes('❌ Error al publicar'))) {
return NextResponse.json({ success: true, message: 'Ignored bot self-message loop' }, { status: 200 });
}
// Filtrar estrictamente para que SOLO procese mensajes del grupo 'Publicaciones Almeira'
// (A petición del usuario se desactiva para procesar todos los chats y filtrar en la fase de aprobación)
// if (remoteJid !== TARGET_GROUP_JID) {
// return NextResponse.json({ success: true, message: 'Ignored other chat' }, { status: 200 });
// }
// Extraer texto del mensaje (ya guardado en rawText, lo mantenemos como text para retrocompatibilidad interna)
const text = rawText;
const lowerText = text.trim().toLowerCase();
const contextInfo = messageData.extendedTextMessage?.contextInfo;
const isReply = !!contextInfo?.stanzaId;
const quotedText = contextInfo?.quotedMessage?.conversation || contextInfo?.quotedMessage?.extendedTextMessage?.text || '';
const { databases, storage } = await createAdminClient();
// LOGIC 1: APROBAR PUBLICACIÓN
if (isReply && lowerText.includes('aprobar')) {
// Extraer ID de la referencia en el mensaje del bot
const refMatch = quotedText.match(/Ref:\s*([a-zA-Z0-9_-]+)/);
if (refMatch && refMatch[1]) {
const postId = refMatch[1];
try {
// Actualizar Appwrite
await databases.updateDocument(
APPWRITE_ALMEIRA_CONFIG.databaseId,
APPWRITE_ALMEIRA_CONFIG.collections.posts,
postId,
{ estado: 'publicado' }
);
await sendWhatsAppMessage(remoteJid, `✅ La publicación ha sido aprobada y publicada exitosamente.\n(ID: ${postId})`);
return NextResponse.json({ success: true, message: 'Post aprobado' }, { status: 200 });
} catch (e: any) {
console.error("Error approving post:", e);
await sendWhatsAppMessage(remoteJid, `❌ Error al publicar: ${e.message}`);
return NextResponse.json({ success: false, error: e.message }, { status: 500 });
}
}
}
// LOGIC 2: PROCESAR NUEVO MENSAJE PARA GEMINI
// Evitámos tratar de procesar respuestas cortas o mensajes vacíos
if (!text && !messageData.imageMessage) {
return NextResponse.json({ success: true, message: 'Ignored empty message' }, { status: 200 });
}
// Verificamos si tiene multimedia
let base64Media = undefined;
let mimeType = undefined;
let imageId = undefined;
if (messageData.imageMessage || messageData.videoMessage || messageData.documentMessage) {
const media = await getBase64FromMediaMessage(data);
if (media) {
base64Media = media.base64;
mimeType = media.mimetype;
// Subimos a Appwrite Storage
const buffer = Buffer.from(base64Media, 'base64');
// Appwrite requiere un InputFile o similar. node-appwrite maneja InputFile.fromBuffer
const extension = mimeType.split('/')[1] || 'jpg';
const fileName = `wa_${Date.now()}.${extension}`;
const file = InputFile.fromBuffer(buffer, fileName);
const uploadedFile = await storage.createFile(
APPWRITE_ALMEIRA_CONFIG.mediaBucketId,
ID.unique(),
file
);
imageId = uploadedFile.$id;
}
}
// Llamar a Gemini
const parsed = await processWhatsAppMessage(text, base64Media, mimeType);
// Determinar fecha de vencimiento
let fechaVencimiento = parsed.fecha_vencimiento;
if ((parsed.tipo === 'evento' || parsed.tipo === 'aviso') && !fechaVencimiento) {
fechaVencimiento = getLastDayOfMonth();
}
// Buscar un tipo de publicación por defecto (Requerido por Appwrite Schema)
let tipoPublicacionId = '';
try {
const types = await databases.listDocuments(
APPWRITE_ALMEIRA_CONFIG.databaseId,
APPWRITE_ALMEIRA_CONFIG.collections.publicationTypes,
[]
);
if (types.documents.length > 0) {
tipoPublicacionId = types.documents[0].$id;
}
} catch (e) {
console.warn('No se pudo obtener un tipo de publicación por defecto:', e);
}
// Crear Post en Appwrite
const newPost = await databases.createDocument(
APPWRITE_ALMEIRA_CONFIG.databaseId,
APPWRITE_ALMEIRA_CONFIG.collections.posts,
ID.unique(),
{
titulo: truncateString(parsed.titulo, 70),
slug: generateSlug(parsed.titulo) + '-' + Date.now().toString().slice(-4),
tipo: parsed.tipo,
resumen_redes: truncateString(parsed.seo_descripcion, 155),
seo_titulo: truncateString(parsed.titulo, 60),
seo_descripcion: truncateString(parsed.seo_descripcion, 155),
contenido: parsed.contenido,
estado: 'borrador', // Se requiere aprobación
fecha_publicacion: new Date().toISOString(),
...(tipoPublicacionId && { tipo_publicacion_id: tipoPublicacionId }),
...(fechaVencimiento && { fecha_finalizacion: new Date(fechaVencimiento).toISOString() }),
...(imageId && { imagen_destacada_id: imageId })
}
);
// Notificar al grupo de WhatsApp pidiendo aprobación
const requestText = `📰 *Publicación Procesada (Pendiente de Aprobación)*\n\n`
+ `*Título:* ${parsed.titulo}\n`
+ `*Tipo:* ${parsed.tipo.toUpperCase()}\n`
+ `*SEO:* ${parsed.seo_descripcion}\n\n`
+ `Para aprobar esta publicación, responde a este mismo mensaje con la palabra "Aprobar".\n\n`
+ `(Ref: ${newPost.$id})`;
await sendWhatsAppMessage(remoteJid, requestText);
return NextResponse.json({ success: true, message: 'Mensaje procesado' }, { status: 200 });
} catch (error: any) {
console.error('Error in WhatsApp Webhook:', error);
return NextResponse.json({ success: false, error: error.message }, { status: 500 });
}
}