PLUTON\igor.kreyda
fix: implement DNS-over-HTTPS (DoH) to bypass local resolver issues
e9ebe66
/**
* 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 };