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 };