From e00faf1ace0c09cc813760a31c5e455bf3e8edd0 Mon Sep 17 00:00:00 2001 From: "Carlos (Almeira)" Date: Sun, 21 Jun 2026 11:54:55 -0500 Subject: [PATCH] feat: Add Evolution API webhook and utilities --- src/app/api/webhooks/openclaw/route.ts | 85 ++++++++++++++++++++++++++ src/lib/evolution.ts | 67 ++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 src/app/api/webhooks/openclaw/route.ts create mode 100644 src/lib/evolution.ts diff --git a/src/app/api/webhooks/openclaw/route.ts b/src/app/api/webhooks/openclaw/route.ts new file mode 100644 index 0000000..914ca60 --- /dev/null +++ b/src/app/api/webhooks/openclaw/route.ts @@ -0,0 +1,85 @@ +import { NextResponse } from 'next/server'; +import { ID } from 'node-appwrite'; +import { createAdminClient } from '@/lib/appwrite.server'; +import { APPWRITE_ALMEIRA_CONFIG } from '@/lib/appwrite'; +import { collectionMap } from '@/lib/schema'; + +// Espera un token de seguridad para evitar spam +const OPENCLAW_SECRET = process.env.OPENCLAW_WEBHOOK_SECRET || 'secret-openclaw-token'; + +export async function POST(req: Request) { + try { + // 1. Verificar autenticación básica mediante Header o Query + const { searchParams } = new URL(req.url); + const token = searchParams.get('token') || req.headers.get('authorization')?.replace('Bearer ', ''); + + if (token !== OPENCLAW_SECRET) { + return NextResponse.json({ success: false, error: 'No autorizado. Token inválido.' }, { status: 401 }); + } + + // 2. Parsear la información enviada por OpenClaw + const payload = await req.json(); + + // Validar campos mínimos + if (!payload.titulo || !payload.contenido) { + return NextResponse.json({ + success: false, + error: 'Faltan campos requeridos. Se espera "titulo" y "contenido".' + }, { status: 400 }); + } + + const { databases } = await createAdminClient(); + const postsCollectionId = collectionMap['posts']; + + // 3. Preparar el documento de la noticia + // Convertimos el título en un slug básico (ej: "Mi Noticia" -> "mi-noticia") + const slug = payload.titulo + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/(^-|-$)+/g, ''); + + // Valores por defecto para relaciones requeridas según el schema + let tipoPublicacionId = payload.tipo_publicacion_id; + + // Si OpenClaw no envía el tipo_publicacion_id, buscamos uno por defecto + if (!tipoPublicacionId) { + try { + const pubTypesCollection = collectionMap['publication_types']; + const types = await databases.listDocuments(APPWRITE_ALMEIRA_CONFIG.databaseId, pubTypesCollection, []); + 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); + } + } + + const postData = { + titulo: payload.titulo, + slug: slug, + tipo: payload.tipo || 'noticia', + estado: 'borrador', // Se guarda como borrador por seguridad + fecha_publicacion: new Date().toISOString(), + contenido: payload.contenido, + tipo_publicacion_id: tipoPublicacionId + }; + + // 4. Guardar en Appwrite + const document = await databases.createDocument( + APPWRITE_ALMEIRA_CONFIG.databaseId, + postsCollectionId, + ID.unique(), + postData + ); + + return NextResponse.json({ + success: true, + message: 'Noticia creada exitosamente', + data: { id: document.$id, titulo: document.titulo } + }, { status: 201 }); + + } catch (error: any) { + console.error('Error in OpenClaw Webhook:', error); + return NextResponse.json({ success: false, error: error.message }, { status: 500 }); + } +} diff --git a/src/lib/evolution.ts b/src/lib/evolution.ts new file mode 100644 index 0000000..16189e3 --- /dev/null +++ b/src/lib/evolution.ts @@ -0,0 +1,67 @@ +import axios from 'axios'; + +const EVOLUTION_API_URL = process.env.EVOLUTION_API_URL || ''; +const EVOLUTION_API_KEY = process.env.EVOLUTION_API_KEY || ''; +const EVOLUTION_INSTANCE_NAME = process.env.EVOLUTION_INSTANCE_NAME || ''; + +/** + * Send a simple text message via Evolution API + */ +export async function sendWhatsAppMessage(number: string, text: string) { + if (!EVOLUTION_API_URL || !EVOLUTION_API_KEY || !EVOLUTION_INSTANCE_NAME) { + console.warn('Evolution API no está configurada en las variables de entorno.'); + return { success: false, error: 'Configuración faltante' }; + } + + try { + const response = await axios.post( + `${EVOLUTION_API_URL}/message/sendText/${EVOLUTION_INSTANCE_NAME}`, + { + number, + text + }, + { + headers: { + 'apikey': EVOLUTION_API_KEY, + 'Content-Type': 'application/json' + } + } + ); + return { success: true, data: response.data }; + } catch (error: any) { + console.error('Error al enviar mensaje de WhatsApp:', error?.response?.data || error.message); + return { success: false, error: error.message }; + } +} + +/** + * Send an image with text/caption via Evolution API + */ +export async function sendWhatsAppImage(number: string, imageUrl: string, caption: string) { + if (!EVOLUTION_API_URL || !EVOLUTION_API_KEY || !EVOLUTION_INSTANCE_NAME) { + console.warn('Evolution API no está configurada en las variables de entorno.'); + return { success: false, error: 'Configuración faltante' }; + } + + try { + const response = await axios.post( + `${EVOLUTION_API_URL}/message/sendMedia/${EVOLUTION_INSTANCE_NAME}`, + { + number, + mediatype: 'image', + media: imageUrl, + caption + }, + { + headers: { + 'apikey': EVOLUTION_API_KEY, + 'Content-Type': 'application/json' + } + } + ); + return { success: true, data: response.data }; + } catch (error: any) { + console.error('Error al enviar imagen de WhatsApp:', error?.response?.data || error.message); + return { success: false, error: error.message }; + } + }