// backend/bot.js // ─────────────────────────────────────────────────────────────────────────────── // 1) Устанавливаем зависимости: // npm install express body-parser node-telegram-bot-api // // 2) Запуск: // node bot.js // (или: npx nodemon bot.js — для auto-reload при изменениях) // ─────────────────────────────────────────────────────────────────────────────── const express = require('express'); const bodyParser = require('body-parser'); const TelegramBot = require('node-telegram-bot-api'); const crypto = require('crypto'); // Добавляем multer и streamifier const multer = require('multer'); const streamifier = require('streamifier'); const app = express(); const HTTP_PORT = 3001; // порт для BACKEND API const BOT_TOKEN = '7801636590:AAFphqOK0Dqta7v9VCLkTPGYC1OujNIFgXA'; // ← замените на свой токен const WEBAPP_BASE_URL = 'https://dosoap.duckdns.org'; // ← например: https://xyz123.ngrok.io // 1) Запускаем бота (polling) const bot = new TelegramBot(BOT_TOKEN, { polling: true }); // 2) Настраиваем multer (храним файлы в памяти) const storage = multer.memoryStorage(); const upload = multer({ storage }); // 3) Разрешаем CORS для фронтенда app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'Content-Type'); next(); }); // 4) JSON-парсер (не обязателен для multipart/form-data) app.use(bodyParser.json()); // 5) Обработчик POST /api/submit (multipart/form-data) app.post( '/api/submit', upload.single('photo'), // поле "photo" — это файл, если есть (req, res) => { try { // Текстовые поля придут в req.body, файл — в req.file const { chat_id, calculator_id, calculator_name, telegram_message, } = req.body; // Проверяем обязательные поля if (!chat_id) { return res.status(400).send('chat_id не передан'); } // Если есть готовое сообщение для Telegram (форматированное на фронтенде), // используем его, иначе формируем универсальное let text = ''; if (telegram_message) { // Используем готовое сообщение из конфигурации калькулятора text = telegram_message; } else { // Формируем универсальное сообщение из всех полей // (fallback для старых версий или калькуляторов без formatTelegramMessage) text = `📊 Расчёт: ${calculator_name || 'Калькулятор'}\n\n`; // Добавляем все пользовательские поля (кроме служебных) const excludeFields = ['chat_id', 'calculator_id', 'calculator_name', 'telegram_message']; Object.keys(req.body).forEach(key => { if (!excludeFields.includes(key) && !key.startsWith('step_') && !key.startsWith('subtotal_') && !key.startsWith('additional_')) { const value = req.body[key]; if (value !== undefined && value !== '' && value !== null) { text += `${key}: ${value}\n`; } } }); // Добавляем результаты расчетов const steps = {}; const subtotals = {}; const additional = {}; Object.keys(req.body).forEach(key => { if (key.startsWith('step_')) { steps[key.replace('step_', '')] = req.body[key]; } else if (key.startsWith('subtotal_')) { subtotals[key.replace('subtotal_', '')] = req.body[key]; } else if (key.startsWith('additional_')) { additional[key.replace('additional_', '')] = req.body[key]; } }); if (Object.keys(steps).length > 0) { text += '\nШаги расчёта:\n'; Object.keys(steps).forEach(key => { text += ` ${key}: ${Number(steps[key]).toFixed(2)} ₽\n`; }); } if (Object.keys(subtotals).length > 0) { text += '\nПодитоги:\n'; Object.keys(subtotals).forEach(key => { text += ` ${key}: ${Number(subtotals[key]).toFixed(2)} ₽\n`; }); } if (Object.keys(additional).length > 0) { text += '\nДополнительно:\n'; Object.keys(additional).forEach(key => { text += ` ${key}: ${Number(additional[key]).toFixed(2)} ₽\n`; }); } } // Если пользователь прикрепил фото (req.file), шлём sendPhoto if (req.file) { const bufferStream = streamifier.createReadStream(req.file.buffer); return bot .sendPhoto( Number(chat_id), bufferStream, { caption: text, parse_mode: 'HTML' } ) .then(() => res.sendStatus(200)) .catch((err) => { console.error('Ошибка при отправке фото:', err); res.status(500).send('Ошибка при отправке фото ботом'); }); } // Если фото не было, просто шлём текст bot .sendMessage(Number(chat_id), text, { parse_mode: 'HTML' }) .then(() => res.sendStatus(200)) .catch((err) => { console.error('Ошибка при отправке сообщения:', err); res.sendStatus(500); }); } catch (err) { console.error('Ошибка в /api/submit:', err); res.status(500).send('Внутренняя ошибка сервера'); } } ); // 6) Команда /menu — отправляем inline-кнопку с chat_id bot.setMyCommands([ { command: 'menu', description: 'Открыть калькулятор' }, { command: 'myid', description: 'Узнать мой chat_id' }, ]); bot.onText(/\/menu/, (msg) => { try { const chatId = msg.chat.id; const url = `${WEBAPP_BASE_URL}/?chat_id=${chatId}`; bot.sendMessage( chatId, 'Нажмите кнопку ниже, чтобы открыть калькулятор:', { reply_markup: { inline_keyboard: [ [ { text: 'Открыть калькулятор', web_app: { url }, }, ], ], }, } ); } catch (err) { console.error('Ошибка в обработчике /menu:', err); } }); // Команда для получения chat_id bot.onText(/\/myid/, (msg) => { try { const chatId = msg.chat.id; bot.sendMessage( chatId, `Ваш chat_id: ${chatId}\n\nВы можете открыть калькулятор напрямую по ссылке:\n${WEBAPP_BASE_URL}/?chat_id=${chatId}`, { parse_mode: 'HTML' } ); } catch (err) { console.error('Ошибка в обработчике /myid:', err); } }); // 7) Ловим ошибки polling-а и логируем детали bot.on('polling_error', (err) => { console.error('Polling error:', err); }); // 8) Запускаем Express-сервер на порту 3001 app.listen(HTTP_PORT, () => { console.log(`Bot+API запущены, слушаем порт ${HTTP_PORT}`); });