/** * Main Bot Logic Module * Initializes Telegraf bot and defines commands/events. */ const { Telegraf, Markup } = require('telegraf'); const https = require('https'); // Helper function: Resolve IP via Google DNS-over-HTTPS // This bypasses local system DNS (UDP:53) which is often blocked on free hosting. async function resolveDoH(hostname) { return new Promise((resolve, reject) => { const options = { hostname: 'dns.google', port: 443, path: `/resolve?name=${hostname}&type=A`, method: 'GET', headers: { 'Accept': 'application/dns-json' } }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const json = JSON.parse(data); // Check if Answer exists and has data if (json.Answer && json.Answer.length > 0) { // Return the first IP address found console.log(`[DNS-over-HTTPS] Resolved ${hostname} to ${json.Answer[0].data}`); resolve(json.Answer[0].data); } else { reject(new Error('No DNS Answer found')); } } catch (e) { reject(e); } }); }); req.on('error', (e) => { console.error('[DNS-over-HTTPS] Request failed:', e); reject(e); }); req.end(); }); } // This function will be called from index.js function setupBot(token, webAppUrl) { if (!token) { throw new Error('BOT_TOKEN is missing!'); } const botOptions = {}; // Only apply this fix in production (on Hugging Face) if (process.env.NODE_ENV === 'production') { console.log('[Bot] Production mode: Configuring DNS-over-HTTPS agent.'); const agent = new https.Agent({ keepAlive: true, family: 4, lookup: async (hostname, options, callback) => { // Only intercept api.telegram.org if (hostname === 'api.telegram.org') { try { const ip = await resolveDoH('api.telegram.org'); return callback(null, ip, 4); } catch (err) { console.error('[Bot] DoH failed, falling back to system DNS:', err); // Fallback to system DNS if DoH fails (unlikely if outbound HTTPS works) const dns = require('dns'); return dns.lookup(hostname, options, callback); } } // Default behavior for other domains const dns = require('dns'); return dns.lookup(hostname, options, callback); } }); botOptions.telegram = { agent }; } // Pass botOptions to Telegraf constructor const bot = new Telegraf(token, botOptions); // Initial setup for the menu button bot.start(async (ctx) => { const welcomeMessage = ` Привет! Я AI Post Generator Bot. 🤖 Я помогу тебе создавать уникальные посты и изображения. Нажми кнопку ниже (или "Меню" -> "Настройки"), чтобы настроить параметры генерации. `; try { await ctx.setChatMenuButton({ type: 'web_app', text: 'Настройки', web_app: { url: `${webAppUrl}/app/index.html` } }); console.log('Menu button set for user:', ctx.from.id); } catch (e) { console.error('Failed to set menu button:', e); } await ctx.reply(welcomeMessage, Markup.inlineKeyboard([ Markup.button.webApp('Открыть настройки ⚙️', `${webAppUrl}/app/index.html`) ])); }); // Imports for logic const { buildStoryPrompt, buildImagePrompt, buildEditPrompt, buildImageRegenerationPrompt } = require('../../prompts/promptBuilder'); const { generateText } = require('../../services/llmService'); const { generateImage, regenerateImage } = require('../../services/imageService'); const { storySystemPrompt } = require('../../prompts/storySystemPrompt'); const { stylePresets } = require('../../prompts/stylePresets'); const { getCurrentPreset, getUserSession, updateLastGeneration, updateLastImage, setAwaitingEdit, setAwaitingImageEdit } = require('./userSession'); const { handleEditCommand, handleEditImageCommand } = require('./actions'); bot.help((ctx) => ctx.reply('Send /start to open the settings app.')); // Command Handlers bot.command('edit_story', handleEditCommand); bot.command('edit_image', handleEditImageCommand); // Text Message Handler bot.on('text', async (ctx) => { const userMessage = ctx.message.text; // Ignore commands if (userMessage.startsWith('/')) return; try { // Show typing status await ctx.sendChatAction('typing'); // 1. Get Settings from session const userId = ctx.from.id; const session = getUserSession(userId); const selectedPreset = getCurrentPreset(userId); console.log(`[Bot] Active Preset: ${selectedPreset.preset_name}, Awaiting Edit: ${session.awaitingEdit}, Awaiting Image Edit: ${session.awaitingImageEdit}`); // --- 2a. Check for Image Edit State --- if (session.awaitingImageEdit && session.lastImage) { console.log(`[Bot] Processing IMAGE EDIT request.`); const regenPrompt = buildImageRegenerationPrompt(userMessage); // Regenerate const newBase64 = await regenerateImage(regenPrompt, session.lastImage); // Send and Save await ctx.replyWithPhoto({ source: Buffer.from(newBase64, 'base64') }); updateLastImage(userId, newBase64); setAwaitingImageEdit(userId, false); return; // Stop here, don't generate story } let currentPrompt = ""; // 2. Build Prompt based on state (Text Edit) if (session.awaitingEdit && session.lastPrompt && session.lastStory) { console.log(`[Bot] Processing TEXT EDIT request from user ${userId}`); currentPrompt = buildEditPrompt(userMessage, session.lastPrompt, session.lastStory); } else { currentPrompt = buildStoryPrompt( userMessage, storySystemPrompt, selectedPreset ); } // 3. Call LLM Service for Story let responseText = await generateText(currentPrompt); // --- Tags Logic --- const userTagsString = session.tags || ""; // Split user tags by space/newline, filter empty, ensure they start with # const userTagsArray = userTagsString.split(/[\s\n]+/).filter(t => t.startsWith('#')); // Regex to capture hashtags at the end of the text const tagRegex = /((?:#[\w\u0590-\u05ff]+(?:\s+|$))+)$/u; let storyText = responseText.trim(); const llmTagsArray = []; const match = storyText.match(tagRegex); if (match) { const tagsPart = match[1]; storyText = storyText.substring(0, storyText.length - tagsPart.length).trim(); const extracted = tagsPart.match(/#[\w\u0590-\u05ff]+/gu); if (extracted) { llmTagsArray.push(...extracted); } } // Merge unique tags const allTags = [...new Set([...llmTagsArray, ...userTagsArray])]; // Format: Story text + double newline + all tags in one line const finalMessage = `${storyText}\n\n${allTags.join(' ')}`; // 4. Send Story Response to user await ctx.reply(finalMessage); // --- Save History and Reset State --- updateLastGeneration(userId, currentPrompt, storyText); setAwaitingEdit(userId, false); // 5. Generate Image Prompt based on the Story await ctx.sendChatAction('upload_photo'); // Keep "upload_photo" status alive const statusInterval = setInterval(() => { ctx.sendChatAction('upload_photo').catch(e => console.error('[Bot] Action error:', e)); }, 4000); try { // If this was an edit, we might want to regenerate image for the new story OR just keep it. // Current workflow: Always generate new image for new story/edited story. // If it's a Text Edit, we probably want a new image too? // Let's assume yes for now, or we can make it optional. // The user requested to uncomment the block, implying we want images back. const imagePromptLayout = buildImagePrompt( responseText, selectedPreset.image_style_suffix ); // Get refined prompt from LLM const refinedImagePrompt = await generateText(imagePromptLayout); console.log('[Bot] Refined Image Prompt:', refinedImagePrompt); // 6. Generate Image const base64Image = await generateImage(refinedImagePrompt); // 7. Send Photo await ctx.replyWithPhoto({ source: Buffer.from(base64Image, 'base64') }); // Save for future edits updateLastImage(userId, base64Image); } finally { clearInterval(statusInterval); } } catch (error) { console.error('Error handling message:', error); await ctx.reply('Произошла ошибка при генерации ответа. Попробуйте позже.'); } }); return bot; } module.exports = { setupBot };