497 lines
24 KiB
TypeScript
497 lines
24 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, 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[^>]*>(.*?)<\/strong>/gi, '*$1*')
|
|
.replace(/<b[^>]*>(.*?)<\/b>/gi, '*$1*')
|
|
.replace(/<em[^>]*>(.*?)<\/em>/gi, '_$1_')
|
|
.replace(/<i[^>]*>(.*?)<\/i>/gi, '_$1_')
|
|
.replace(/<li[^>]*>(.*?)<\/li>/gi, '• $1\n')
|
|
.replace(/<\/?(?:p|div|h[1-6]|ul|ol|table|tr|td)[^>]*>/gi, '\n')
|
|
.replace(/<br\s*\/?>/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();
|
|
|
|
// Reenviar de forma segura a los webhooks de las tiendas de calzado (DripZone, Sneaky, etc.)
|
|
const MULTISTORE_WEBHOOKS = [
|
|
'https://drip-zone.online/api/webhook/whatsapp',
|
|
'https://dripzone.almeira.com.co/api/webhook/whatsapp',
|
|
'https://sneaky.almeira.com.co/api/webhook/whatsapp',
|
|
'https://zapply.almeira.com.co/api/webhook/whatsapp',
|
|
'https://store.almeira.com.co/api/webhook/whatsapp',
|
|
];
|
|
|
|
try {
|
|
await Promise.allSettled(
|
|
MULTISTORE_WEBHOOKS.map(targetUrl =>
|
|
fetch(targetUrl, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload),
|
|
signal: AbortSignal.timeout(12000),
|
|
}).catch(() => {})
|
|
)
|
|
);
|
|
} catch (_) {}
|
|
|
|
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 });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cache en memoria para evitar procesar mensajes duplicados que WhatsApp retransmite en ráfaga
|
|
const processedMessagesCache = new Map<string, number>();
|
|
|
|
function isDuplicateMessage(chatId: string, text: string): boolean {
|
|
const key = `${chatId}:${text.trim().substring(0, 100)}`;
|
|
const now = Date.now();
|
|
const lastSeen = processedMessagesCache.get(key);
|
|
|
|
// Limpieza de claves viejas (> 2 minutos)
|
|
for (const [k, time] of processedMessagesCache.entries()) {
|
|
if (now - time > 120000) {
|
|
processedMessagesCache.delete(k);
|
|
}
|
|
}
|
|
|
|
if (lastSeen && (now - lastSeen) < 45000) {
|
|
return true;
|
|
}
|
|
|
|
processedMessagesCache.set(key, now);
|
|
return false;
|
|
}
|
|
|
|
function detectExplicitPublicationType(inputText: string): 'noticia' | 'evento' | 'aviso' | null {
|
|
if (!inputText) return null;
|
|
const lower = inputText.toLowerCase();
|
|
|
|
// Detección de palabras clave explícitas enviadas por el usuario:
|
|
// Ejemplos: "Noticia: ...", "Evento: ...", "Publicidad: ...", "#noticia", "publicar como evento", etc.
|
|
if (
|
|
/^\s*(?:#|tipo:\s*|categoria:\s*|publicar\s+como:\s*|\[)?\s*noticia\b/i.test(lower) ||
|
|
/\b(?:publicar\s+como\s+noticia|es\s+una\s+noticia)\b/i.test(lower)
|
|
) {
|
|
return 'noticia';
|
|
}
|
|
|
|
if (
|
|
/^\s*(?:#|tipo:\s*|categoria:\s*|publicar\s+como:\s*|\[)?\s*evento\b/i.test(lower) ||
|
|
/\b(?:publicar\s+como\s+evento|es\s+un\s+evento|agenda\s+cultural|agenda\s+de\s+eventos)\b/i.test(lower)
|
|
) {
|
|
return 'evento';
|
|
}
|
|
|
|
if (
|
|
/^\s*(?:#|tipo:\s*|categoria:\s*|publicar\s+como:\s*|\[)?\s*(?:publicidad|aviso|clasificado|anuncio)\b/i.test(lower) ||
|
|
/\b(?:publicar\s+como\s+publicidad|publicar\s+como\s+aviso|publicar\s+como\s+clasificado|es\s+publicidad|es\s+un\s+aviso)\b/i.test(lower)
|
|
) {
|
|
return 'aviso';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// LOGIC 2: PROCESAR PUBLICACIÓN ÚNICA
|
|
// En WhatsApp, los álbumes de fotos (ej. 25 fotos + 1 texto) llegan como 26 eventos separados.
|
|
// Las fotos individuales sin texto sustancial NO deben generar publicaciones independientes;
|
|
// se recolectan como recursos para la publicación principal que contiene el texto de la noticia.
|
|
const hasSubstantialText = Boolean(text && text.trim().length >= 10);
|
|
|
|
if (!hasSubstantialText) {
|
|
console.log(`[WhatsApp Webhook] Mensaje multimedia sin texto editorial (${text?.length || 0} chars). Registrado para lote de fotos.`);
|
|
return NextResponse.json({ success: true, message: 'Media registered for album batch' }, { status: 200 });
|
|
}
|
|
|
|
// Evitar duplicación si WhatsApp envía el mismo texto repetido en la ráfaga
|
|
if (isDuplicateMessage(remoteJid, text)) {
|
|
console.log(`[WhatsApp Webhook] Mensaje de texto duplicado en ráfaga ignorado.`);
|
|
return NextResponse.json({ success: true, message: 'Ignored duplicate text in burst' }, { status: 200 });
|
|
}
|
|
|
|
// Detectar si el usuario definió explícitamente el tipo en el texto (noticia, evento o publicidad/aviso)
|
|
const explicitType = detectExplicitPublicationType(text);
|
|
if (explicitType) {
|
|
console.log(`[WhatsApp Webhook] Tipo de publicación explícito detectado: "${explicitType.toUpperCase()}"`);
|
|
}
|
|
|
|
// 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
|
|
const typeInfoText = explicitType ? ` (${explicitType.toUpperCase()})` : '';
|
|
sendWhatsAppMessage(
|
|
destinationJid,
|
|
`📥 *Nueva Información Recibida${typeInfoText}*\n` +
|
|
`📍 *Fuente:* ${filterResult.resolvedName}\n` +
|
|
`_Almeira Web • Asistente Editorial con IA_\n\n` +
|
|
`🤖 Recolectando lote de imágenes y estructurando 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 + álbum de fotos reciente del 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 todas las fotos que llegaron en el mismo grupo en los últimos 4 minutos (álbum completo de hasta 30 fotos)
|
|
try {
|
|
const recentMedia = await fetchRecentMediaFromSameChat(remoteJid, 30, 240);
|
|
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 del álbum con Gemini (Tipo forzado: ${explicitType || 'Auto'})...`);
|
|
|
|
// Llamar a Gemini para estructurar la publicación y seleccionar la mejor imagen respetando el tipo explícito si existe
|
|
const parsed = await processWhatsAppMessage(text, candidateMediaList, undefined, explicitType);
|
|
|
|
// Determinar dimensiones según el tipo de publicación
|
|
// Noticias: 640x360 (16:9) | Clasificados y Eventos: 640x640 (1:1)
|
|
// Determinar gravedad de encuadre y dimensiones según análisis de IA
|
|
const gravity = parsed.crop_focus === 'top' ? 'top'
|
|
: parsed.crop_focus === 'bottom' ? 'bottom'
|
|
: 'center';
|
|
|
|
// Si es un afiche/flyer con texto integrado o evento/aviso, usar formato cuadrado 640x640 para evitar mutilar texto
|
|
const isSquare = parsed.tipo === 'evento' || parsed.tipo === 'aviso' || Boolean(parsed.has_text_overlay || parsed.crop_focus === 'contain');
|
|
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'} • Gravedad: ${gravity})`
|
|
: `📄 *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}/view?project=${APPWRITE_ALMEIRA_CONFIG.projectId}&width=640&height=${imgHeight}&gravity=${gravity}&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<string, string> = {
|
|
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 con análisis de encuadre
|
|
let imageNotice = 'No se adjuntó imagen';
|
|
const focusLabel = parsed.crop_focus === 'top' ? 'Enfoque superior (preserva titulares/rostros)'
|
|
: parsed.crop_focus === 'contain' ? 'Formato completo (afiche/texto sin cortes)'
|
|
: parsed.crop_focus === 'bottom' ? 'Enfoque inferior'
|
|
: 'Enfoque centrado equilibrado';
|
|
|
|
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})` : '')
|
|
+ `\n 📐 _${focusLabel}_`;
|
|
} else if (candidateMediaList.length === 1) {
|
|
imageNotice = `1 foto optimizada (${isSquare ? '640x640' : '640x360'})\n 📐 _${focusLabel}_`;
|
|
}
|
|
|
|
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 });
|
|
}
|
|
}
|