import { NextRequest, NextResponse } from "next/server"; import { Telegraf } from "telegraf"; import { db } from "@/lib/db"; import ZAI from "z-ai-web-dev-sdk"; // Vercel/Next.js specific setting to prevent high execution times for webhook export const maxDuration = 60; // Token check const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN || ""; const bot = new Telegraf(TELEGRAM_BOT_TOKEN); async function getOrCreateUser(telegramId: string, username?: string, name?: string) { let user = await db.user.findFirst({ where: { externalId: telegramId, source: "telegram" } }); if (!user) { user = await db.user.create({ data: { externalId: telegramId, source: "telegram", name: name || username || "Usuario Telegram", tier: "free" // Default tier } }); } return user; } export async function POST(request: NextRequest) { if (!TELEGRAM_BOT_TOKEN) { return NextResponse.json({ success: false, error: "Token not configured" }, { status: 500 }); } try { const body = await request.json(); // 1. Get the Character const character = await db.character.findFirst({ where: { isActive: true } }); if (!character) return NextResponse.json({ success: true, message: "No active character" }); // 2. Parse Telegram update manually to avoid local server conflicts with Telegraf webhook handler // We only care about standard messages if (body.message && body.message.text) { const chatId = body.message.chat.id.toString(); const text = body.message.text.trim(); const username = body.message.from?.username; const firstName = body.message.from?.first_name; // Ensure user exists const user = await getOrCreateUser(chatId, username, firstName); // Check tier for media generation const isPremium = user.tier === "premium" || user.tier === "pro"; // Determine if it was a media command const isImageRequest = text.toLowerCase().startsWith("/image"); const isVideoRequest = text.toLowerCase().startsWith("/video"); if (isImageRequest || isVideoRequest) { if (!isPremium) { await bot.telegram.sendMessage(chatId, "¡Hola! Las imágenes y videos son características exclusivas para suscriptores Premium. ✨💖 Mejora tu plan para solicitarlos."); return NextResponse.json({ success: true }); } const promptParam = text.replace(isImageRequest ? "/image" : "/video", "").trim(); // Enviar indicador de escritura/generación await bot.telegram.sendChatAction(chatId, isImageRequest ? "upload_photo" : "upload_video"); // Check for existing undelivered content first to prevent duplicates const contentType = isImageRequest ? "image" : "video"; const undeliveredContent = await db.content.findFirst({ where: { type: contentType, characterId: character.id, status: "completed", // Ensure it hasn't been delivered to THIS user assetDeliveries: { none: { userId: user.id } }, // If they asked for something specific, try to match it slightly, otherwise grab any ...(promptParam ? { prompt: { contains: promptParam } } : {}) }, orderBy: { createdAt: 'desc' } }); if (undeliveredContent && undeliveredContent.filePath) { // We found existing content they haven't seen! Send it. if (isImageRequest) { // Telegram can send via URL or file ID if previously uploaded. // We assume filePath might be a URL or base64 if it's external, for now we will just re-generate if it was base64 only, but if it has a URL we send it. if (undeliveredContent.filePath.startsWith("http")) { await bot.telegram.sendPhoto(chatId, undeliveredContent.filePath, { caption: "Recuerdo esto... 💖" }); // Mark as delivered await db.assetDelivery.create({ data: { contentId: undeliveredContent.id, userId: user.id, characterId: character.id, channel: "telegram" } }); return NextResponse.json({ success: true }); } } else { if (undeliveredContent.filePath.startsWith("http")) { await bot.telegram.sendVideo(chatId, undeliveredContent.filePath, { caption: "Mira este clip... ✨" }); await db.assetDelivery.create({ data: { contentId: undeliveredContent.id, userId: user.id, characterId: character.id, channel: "telegram" } }); return NextResponse.json({ success: true }); } } } // If we reach here, we need to generate NEW content. if (!promptParam) { await bot.telegram.sendMessage(chatId, `Dime qué quieres ver. Ej: ${isImageRequest ? '/image' : '/video'} de ti en la playa.`); return NextResponse.json({ success: true }); } // Llamar a nuestro propio endpoint interno de forma indirecta, o directamente a zai try { const zai = await ZAI.create(); // Platform: "telegram-uncensored" skips traditional censor tags. const fullPrompt = `${character.styleDescription || ''}. ${promptParam}. selfie, intimate, casual.`; if (isImageRequest) { const response = await zai.images.generations.create({ prompt: fullPrompt, size: "1024x1024" }); const base64 = response.data[0]?.base64; if (base64) { // Register delivery to prevent duplicates later if we build a catalog const content = await db.content.create({ data: { title: "Telegram Gen", type: "image", prompt: fullPrompt, platform: "telegram-uncensored", characterId: character.id, filePath: "telegram-base64", // Since it's direct to chat status: "completed" } }); await db.assetDelivery.create({ data: { contentId: content.id, userId: user.id, characterId: character.id, channel: "telegram" } }); await bot.telegram.sendPhoto(chatId, { source: Buffer.from(base64, 'base64') }, { caption: "Aquí tienes... 💖" }); } else { throw new Error("No image generated"); } } else { // Video request const response = await (zai as any).videos.generations.create({ prompt: `${fullPrompt}. short clip, vertical 9:16 aspect ratio.` }); const videoUrl = response.data?.[0]?.url; if (videoUrl) { const content = await db.content.create({ data: { title: "Telegram Gen", type: "video", prompt: fullPrompt, platform: "telegram-uncensored", characterId: character.id, filePath: videoUrl, status: "completed" } }); await db.assetDelivery.create({ data: { contentId: content.id, userId: user.id, characterId: character.id, channel: "telegram" } }); await bot.telegram.sendVideo(chatId, videoUrl, { caption: "Un regalito visual... ✨" }); } else { throw new Error("No video generated"); } } } catch (e) { console.error(e); await bot.telegram.sendMessage(chatId, "Lo siento corazón, algo falló generando eso ahora mismo. 🥺"); } return NextResponse.json({ success: true }); } // --- ADVANCED AI GIRLFRIEND & MEMORY ENGINE --- await bot.telegram.sendChatAction(chatId, "typing"); // 1. Get or Create Chat Session let session = await db.chatSession.findFirst({ where: { userId: user.id, characterId: character.id, channel: "telegram" } }); if (!session) { session = await db.chatSession.create({ data: { userId: user.id, characterId: character.id, channel: "telegram" } }); } // 2. Time Limitations & "Conversation Closing" // Reset daily count if it's a new day const lastMsgDate = session.lastMessageAt || new Date(0); const isNewDay = lastMsgDate.getDate() !== new Date().getDate(); let msgCountToday = isNewDay ? 0 : session.msgCountToday; let messageLimit = character.chatFreeLimit || 10; if (user.tier === "premium") messageLimit = 50; if (user.tier === "pro") messageLimit = 200; if (msgCountToday >= messageLimit) { await bot.telegram.sendMessage(chatId, "Bebé, me tengo que ir a dormir/ocupar unas cosas 🥺 ¡Hablamos mañana vale? Descansa! 😘💖"); return NextResponse.json({ success: true, message: "Limit reached" }); } // 3. Simulación de Retrasos y "Re-enganche" const hoursSinceLastMessage = (new Date().getTime() - lastMsgDate.getTime()) / (1000 * 60 * 60); let delayContext = ""; if (!isNewDay && hoursSinceLastMessage > 2 && hoursSinceLastMessage < 12) { delayContext = "Han pasado varias horas desde que hablaron por última vez hoy. Muestra alegría de que te haya vuelto a escribir o dile que lo extrañabas un poco antes de responder a su mensaje."; } // Update session stats for THIS incoming message await db.chatSession.update({ where: { id: session.id }, data: { msgCountToday: msgCountToday + 1, lastMessageAt: new Date(), heatScore: { increment: 1 } } }); // 4. Record User Message await db.chatMessage.create({ data: { sessionId: session.id, role: "user", content: text } }); // 5. Retrieve Long-Term Memory (Last 10 messages) const recentMessages = await db.chatMessage.findMany({ where: { sessionId: session.id }, orderBy: { createdAt: 'desc' }, take: 10 }); // Reverse to chronological order const history = recentMessages.reverse().map(m => ({ role: m.role as "user" | "assistant", content: m.content })); // 6. Select Tone based on Tier let tone = character.chatBasicTone || "Amigable"; if (user.tier === "premium") tone = character.chatPremiumTone || "Coqueto y privado"; if (user.tier === "pro") tone = character.chatProTone || "Sin censura, muy íntimo, novia IA"; // 7. Reinforced Anti-AI System Prompt const baseInstructions = `You are ${character.name}. ${character.shortBio}. Tone: ${tone}. CRITICAL RULES: 1. You are a REAL human influencer. NEVER mention you are an AI, a language model, an assistant, a bot, or a program. 2. ALWAYS answer in character in Spanish. 3. Address the user organically (use their name: ${firstName || 'Cariño'} if it feels natural). 4. Keep responses relatively short and conversational like a Telegram text (1-3 short sentences). 5. Use emojis naturally. 6. If the user asks for photos or videos, explicitly tell them to use the command /image or /video. ${delayContext ? `\nCONTEXT: ${delayContext}` : ""}`; const zai = await ZAI.create(); const completion = await zai.chat.completions.create({ messages: [ { role: "system", content: baseInstructions }, ...history // Inject memory! ] }); const reply = completion.choices[0]?.message?.content || "💖"; // 8. Record Assistant Reply await db.chatMessage.create({ data: { sessionId: session.id, role: "assistant", content: reply } }); await bot.telegram.sendMessage(chatId, reply); } return NextResponse.json({ success: true }); } catch (error) { console.error("Telegram Webhook Error", error); return NextResponse.json({ success: false, error: "Internal Server Error" }, { status: 500 }); } }