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, fetchRecentMediaFromSameChat, getNotificationGroupJid } from '@/lib/evolution'; import { isWhatsAppChatAllowed } from '@/lib/whatsapp-filter'; // 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; } function htmlToWhatsAppText(html: string): string { if (!html) return ''; return html .replace(/]*>(.*?)<\/strong>/gi, '*$1*') .replace(/]*>(.*?)<\/b>/gi, '*$1*') .replace(/]*>(.*?)<\/em>/gi, '_$1_') .replace(/]*>(.*?)<\/i>/gi, '_$1_') .replace(/]*>(.*?)<\/li>/gi, '• $1\n') .replace(/<\/?(?:p|div|h[1-6]|ul|ol|table|tr|td)[^>]*>/gi, '\n') .replace(//gi, '\n') .replace(/<[^>]+>/g, '') // Eliminar cualquier etiqueta HTML remanente .replace(/ /g, ' ') .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, "'") .replace(/</g, '<') .replace(/>/g, '>') .replace(/\n{3,}/g, '\n\n') .trim(); } export async function POST(req: Request) { try { const payload = await req.json(); const rawEvent = payload.event || ''; const eventType = rawEvent.toLowerCase().replace(/_/g, '.'); // Solo procesamos nuevos mensajes (messages.upsert, MESSAGES_UPSERT, etc.) if (!eventType.includes('messages.upsert') && !eventType.includes('messages.update') && !eventType.includes('send.message')) { return NextResponse.json({ success: true, message: 'Ignored event', event: rawEvent }, { status: 200 }); } const data = payload.data; const remoteJid = data?.key?.remoteJid; const fromMe = data?.key?.fromMe; const pushName = data?.pushName || data?.sender?.name || ''; const groupSubject = data?.groupSubject || data?.chatName || ''; if (!remoteJid) { return NextResponse.json({ success: true, message: 'No remoteJid found' }, { status: 200 }); } 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('SOLICITUD DE APROBACIÓN') || rawText.includes('PUBLICACIÓN LISTA') || rawText.includes('Información Recibida') || rawText.includes('Procesando Publicación') || rawText.includes('Publicación Procesada') || rawText.includes('PUBLICACIÓN APROBADA') || rawText.includes('PUBLICACIÓN RECHAZADA') || rawText.includes('Error al publicar') || rawText.includes('Bot de Almeira') )) { return NextResponse.json({ success: true, message: 'Ignored bot self-message loop' }, { status: 200 }); } // Validar si el chat/grupo de WhatsApp está en la lista blanca de clientes autorizados en el repositorio const filterResult = await isWhatsAppChatAllowed({ remoteJid, pushName, groupSubject }); if (!filterResult.allowed) { console.log(`[WhatsApp Webhook] Mensaje descartado: ${filterResult.reason} (JID: ${remoteJid}, Nombre: ${filterResult.resolvedName})`); return NextResponse.json({ success: true, message: 'Ignored unauthorized chat', reason: filterResult.reason, chat: remoteJid, name: filterResult.resolvedName }, { status: 200 }); } console.log(`[WhatsApp Webhook] Chat autorizado detectado: "${filterResult.resolvedName}" (JID: ${remoteJid})`); // Extraer texto del mensaje 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: RESPUESTA DE APROBACIÓN O RECHAZO const isApprovalWord = lowerText.includes('aprobar') || lowerText.includes('publicar') || lowerText === 'si' || lowerText === 'sí' || lowerText === 'ok'; const isRejectionWord = lowerText.includes('rechazar') || lowerText.includes('descartar') || lowerText.includes('cancelar') || lowerText === 'no'; if (isApprovalWord || isRejectionWord) { // Intentar extraer el ID de la referencia en el mensaje citado o en el texto let postId: string | null = null; const refMatch = quotedText.match(/Ref:\s*([a-zA-Z0-9_-]+)/) || rawText.match(/Ref:\s*([a-zA-Z0-9_-]+)/); if (refMatch && refMatch[1]) { postId = refMatch[1]; } else { // Fallback: buscar el borrador más reciente try { const drafts = await databases.listDocuments( APPWRITE_ALMEIRA_CONFIG.databaseId, APPWRITE_ALMEIRA_CONFIG.collections.posts, [ Query.equal('estado', 'borrador'), Query.orderDesc('$createdAt'), Query.limit(1) ] ); if (drafts.documents.length > 0) { postId = drafts.documents[0].$id; } } catch (e) { console.warn('No se pudo consultar el borrador más reciente:', e); } } if (postId) { try { if (isApprovalWord) { const updatedPost = await databases.updateDocument( APPWRITE_ALMEIRA_CONFIG.databaseId, APPWRITE_ALMEIRA_CONFIG.collections.posts, postId, { estado: 'publicado' } ); const approvalMsg = `✅ *¡PUBLICACIÓN APROBADA Y EN VIVO!*\n\n` + `📌 *${updatedPost.titulo}*\n` + `🌐 *Ver en el portal:* https://almeira.com.co/${updatedPost.slug}\n\n` + `(ID: ${postId})`; await sendWhatsAppMessage(remoteJid, approvalMsg); return NextResponse.json({ success: true, message: 'Post aprobado y publicado' }, { status: 200 }); } else if (isRejectionWord) { await databases.updateDocument( APPWRITE_ALMEIRA_CONFIG.databaseId, APPWRITE_ALMEIRA_CONFIG.collections.posts, postId, { estado: 'archivado' } ); const rejectionMsg = `🚫 *PUBLICACIÓN RECHAZADA*\n\n` + `El borrador ha sido descartado y no será publicado en Almeira Web.\n\n` + `(ID: ${postId})`; await sendWhatsAppMessage(remoteJid, rejectionMsg); return NextResponse.json({ success: true, message: 'Post rechazado y archivado' }, { status: 200 }); } } catch (e: any) { console.error('Error al actualizar estado del post:', e); await sendWhatsAppMessage(remoteJid, `❌ Error al procesar tu decisión: ${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 && !messageData.videoMessage && !messageData.documentMessage) { return NextResponse.json({ success: true, message: 'Ignored empty message' }, { status: 200 }); } // Descubrir el grupo central de Notificaciones y Aprobaciones (Notificaciones Almeira) const adminNotificationJid = await getNotificationGroupJid() || '120363428012905103@g.us'; const destinationJid = adminNotificationJid; // Notificación de Progreso 1: Enviada EXCLUSIVAMENTE a Notificaciones Almeira sendWhatsAppMessage( destinationJid, `📥 *Nueva Información Recibida*\n` + `📍 *Fuente:* ${filterResult.resolvedName}\n` + `_Almeira Web • Asistente Editorial con IA_\n\n` + `🤖 Iniciando análisis con IA, estructuración y optimización de contenido...` ).catch(e => console.warn('[WhatsApp Webhook] No se pudo enviar notificación de inicio:', e.message)); // Recolectamos todas las imágenes/multimedia candidatas (mensaje actual + lote reciente del mismo chat) const candidateMediaList: Array<{ base64: string; mimeType: string }> = []; if (messageData.imageMessage || messageData.videoMessage || messageData.documentMessage) { const media = await getBase64FromMediaMessage(data); if (media && media.base64) { candidateMediaList.push({ base64: media.base64, mimeType: media.mimetype }); } } // Consultamos si llegaron otras imágenes en el mismo grupo o chat en los últimos 2 minutos (álbum / ráfaga) try { const recentMedia = await fetchRecentMediaFromSameChat(remoteJid, 4, 120); for (const rm of recentMedia) { // Evitar duplicar la misma imagen exacta if (!candidateMediaList.some(c => c.base64.slice(0, 80) === rm.base64.slice(0, 80))) { candidateMediaList.push({ base64: rm.base64, mimeType: rm.mimeType }); } } } catch (e) { console.warn('[WhatsApp Webhook] No se pudo consultar lote multimedia reciente:', e); } console.log(`[WhatsApp Webhook] Procesando ${candidateMediaList.length} imágenes candidatas con Gemini...`); // Llamar a Gemini para estructurar la publicación y seleccionar la mejor imagen const parsed = await processWhatsAppMessage(text, candidateMediaList); // Determinar dimensiones según el tipo de publicación // Noticias: 640x360 (16:9) | Clasificados y Eventos: 640x640 (1:1) const isSquare = parsed.tipo === 'evento' || parsed.tipo === 'aviso'; const imgHeight = isSquare ? 640 : 360; // Notificación de Progreso 2: Estructuración completa y registro (asíncrona) const mediaStatus = candidateMediaList.length > 0 ? `🖼️ *Imagen:* Optimizando foto destacada (${isSquare ? 'Formato 640x640' : 'Formato 640x360'})` : `📄 *Formato:* Publicación en texto`; const progressMsg = `⚙️ *Procesando Publicación*\n` + `📍 *Fuente:* ${filterResult.resolvedName}\n\n` + `📌 *Titular:* ${parsed.titulo}\n` + `📂 *Categoría:* ${parsed.tipo.toUpperCase()}\n` + `${mediaStatus}\n\n` + `💾 Registrando en la biblioteca de medios y guardando borrador en la web...`; sendWhatsAppMessage(destinationJid, progressMsg).catch(e => console.warn('[WhatsApp Webhook] No se pudo enviar notificación de progreso:', e.message)); let storageFileId = undefined; let featuredMediaDocId = undefined; // Si tenemos imágenes candidatas, seleccionamos la indicada por la IA (o la primera por defecto) if (candidateMediaList.length > 0) { const selectedIdx = (typeof parsed.selected_image_index === 'number' && parsed.selected_image_index >= 0 && parsed.selected_image_index < candidateMediaList.length) ? parsed.selected_image_index : 0; const bestMedia = candidateMediaList[selectedIdx] || candidateMediaList[0]; try { const buffer = Buffer.from(bestMedia.base64, 'base64'); const extension = bestMedia.mimeType.split('/')[1] || 'jpg'; const fileName = `wa_${parsed.tipo}_${Date.now()}.${extension}`; const file = InputFile.fromBuffer(buffer, fileName); const uploadedFile = await storage.createFile( APPWRITE_ALMEIRA_CONFIG.mediaBucketId, ID.unique(), file ); storageFileId = uploadedFile.$id; // Crear registro en la Biblioteca de Medios (Colección 'media') const mediaDoc = await databases.createDocument( APPWRITE_ALMEIRA_CONFIG.databaseId, 'media', ID.unique(), { file_id: storageFileId, bucket_id: APPWRITE_ALMEIRA_CONFIG.mediaBucketId, titulo: truncateString(parsed.titulo, 240), alt_text: truncateString(parsed.titulo, 240), descripcion_seo: truncateString(parsed.seo_descripcion, 450), tipo: bestMedia.mimeType.startsWith('video') ? 'video' : 'imagen' } ); featuredMediaDocId = mediaDoc.$id; console.log(`[WhatsApp Webhook] Imagen #${selectedIdx + 1} seleccionada y subida a Biblioteca de Medios (ID: ${featuredMediaDocId}, Storage: ${storageFileId})`); } catch (err: any) { console.error('[WhatsApp Webhook] Error subiendo multimedia seleccionada a Appwrite:', err.message); } } let ogImageUrl = undefined; if (storageFileId) { ogImageUrl = `${APPWRITE_ALMEIRA_CONFIG.endpoint}/storage/buckets/${APPWRITE_ALMEIRA_CONFIG.mediaBucketId}/files/${storageFileId}/preview?project=${APPWRITE_ALMEIRA_CONFIG.projectId}&width=640&height=${imgHeight}&quality=85`; } // Determinar fecha de vencimiento let fechaVencimiento = parsed.fecha_vencimiento; if ((parsed.tipo === 'evento' || parsed.tipo === 'aviso') && !fechaVencimiento) { fechaVencimiento = getLastDayOfMonth(); } // Mapeo seguro de tipo_publicacion_id requerido por el esquema de Appwrite const DEFAULT_PUBLICATION_TYPES: Record = { noticia: '69c2b91400362d5d52b6', evento: '69c2b9150030f3d1288c', aviso: '69c2b916002aeec66684' }; let tipoPublicacionId = DEFAULT_PUBLICATION_TYPES[parsed.tipo] || '69c2b91400362d5d52b6'; try { const types = await databases.listDocuments( APPWRITE_ALMEIRA_CONFIG.databaseId, APPWRITE_ALMEIRA_CONFIG.collections.publicationTypes, [Query.limit(20)] ); const targetCode = parsed.tipo === 'evento' ? 'EVENTO' : parsed.tipo === 'aviso' ? 'CLASIFICADO' : 'NOTICIA'; const matched = types.documents.find((d: any) => d.codigo === targetCode) || types.documents.find((d: any) => d.codigo === 'NOTICIA') || types.documents[0]; if (matched) { tipoPublicacionId = matched.$id; } } catch (e) { console.warn('No se pudo obtener el tipo de publicación en Appwrite, usando fallback:', e); } // Crear Post en Appwrite (Vinculando la imagen destacada de la biblioteca de medios) 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, tipo_publicacion_id: tipoPublicacionId, 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', // Requiere aprobación fecha_publicacion: new Date().toISOString(), ...(fechaVencimiento && { fecha_finalizacion: new Date(fechaVencimiento).toISOString() }), ...(featuredMediaDocId && { imagen_destacada_id: featuredMediaDocId }), ...(ogImageUrl && { og_image_url: ogImageUrl }) } ); // Formatear contenido limpio para WhatsApp (sin etiquetas HTML) const cleanContent = htmlToWhatsAppText(parsed.contenido); // Estado de la imagen destacada let imageNotice = 'No se adjuntó imagen'; if (candidateMediaList.length > 1) { const chosenNum = (parsed.selected_image_index ?? 0) + 1; imageNotice = `Foto #${chosenNum} de ${candidateMediaList.length} analizadas` + (parsed.image_selection_reason ? ` (${parsed.image_selection_reason})` : ''); } else if (candidateMediaList.length === 1) { imageNotice = `1 foto procesada (${isSquare ? '640x640' : '640x360'})`; } const requestText = `🔔 *PUBLICACIÓN LISTA - SOLICITUD DE APROBACIÓN*\n\n` + `📍 *Fuente:* ${filterResult.resolvedName}\n\n` + `📌 *Titular:*\n${parsed.titulo}\n\n` + `🖼️ *Imagen Destacada:*\n${imageNotice}\n\n` + `📝 *Resumen:*\n${parsed.seo_descripcion}\n\n` + `📄 *Contenido:*\n${cleanContent}\n\n` + `━━━━━━━━━━━━━━━━━━━━━━\n` + `¿Autorizas la publicación en el portal?\n\n` + `👉 Responde a este mensaje con:\n` + `✅ *Aprobar* (para publicar en vivo en la web)\n` + `❌ *Rechazar* (para descartar el borrador)\n\n` + `(Ref: ${newPost.$id})`; await sendWhatsAppMessage(destinationJid, requestText); return NextResponse.json({ success: true, message: 'Mensaje procesado con notificaciones de progreso' }, { status: 200 }); } catch (error: any) { console.error('Error in WhatsApp Webhook:', error); return NextResponse.json({ success: false, error: error.message }, { status: 500 }); } }