Spaces:
Build error
Build error
| 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 }); | |
| } | |
| } | |