Spaces:
Sleeping
Sleeping
File size: 10,379 Bytes
66e3a81 e9ebe66 66e3a81 49f3cdf e9ebe66 1f9b2b7 e9ebe66 1f9b2b7 e9ebe66 1f9b2b7 e9ebe66 1f9b2b7 49f3cdf 66e3a81 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | /**
* 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 };
|