Spaces:
Sleeping
Sleeping
| /** | |
| * 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 }; | |