DoSoapCalc/backend/bot.js
DosAi 02c7520c90 Refactor: Modular calculator architecture
Created modular system for calculators, added soap and candles calculators, universal components, updated backend
2025-11-02 15:45:07 +03:00

208 lines
8.0 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = `📊 <b>Расчёт:</b> ${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<b>Шаги расчёта:</b>\n';
Object.keys(steps).forEach(key => {
text += ` ${key}: ${Number(steps[key]).toFixed(2)}\n`;
});
}
if (Object.keys(subtotals).length > 0) {
text += '\n<b>Подитоги:</b>\n';
Object.keys(subtotals).forEach(key => {
text += ` ${key}: ${Number(subtotals[key]).toFixed(2)}\n`;
});
}
if (Object.keys(additional).length > 0) {
text += '\n<b>Дополнительно:</b>\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: <code>${chatId}</code>\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}`);
});