188 lines
8.6 KiB
TypeScript
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 });
|
|
}
|
|
}
|