Gmagl
feat: complete Telegram AI Girlfriend mechanics & deduplication
b8b98dd
Raw
History Blame Contribute Delete
14.5 kB
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 });
}
}