181 lines
6.6 KiB
JavaScript
181 lines
6.6 KiB
JavaScript
|
||
// 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://d472-46-246-24-76.ngrok-free.app/'; // ← например: 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,
|
||
soapName,
|
||
weight,
|
||
basePrice,
|
||
aromaPrice,
|
||
aromaWeight,
|
||
pigmentPrice,
|
||
pigmentWeight,
|
||
moldPrice,
|
||
box,
|
||
filler,
|
||
ribbon,
|
||
labelValue,
|
||
markup,
|
||
totalCost,
|
||
finalPrice,
|
||
pricePer100g,
|
||
} = req.body;
|
||
|
||
// Проверяем обязательные поля
|
||
if (!chat_id) {
|
||
return res.status(400).send('chat_id не передан');
|
||
}
|
||
if (!soapName) {
|
||
return res.status(400).send('soapName не передан');
|
||
}
|
||
|
||
// Соберём сообщение так, чтобы в чат пришло всё, что ввели:
|
||
// 1. Название мыла
|
||
// 2. Вес и цена основы
|
||
// 3. Отдушка
|
||
// 4. Пигмент
|
||
// 5. Форма
|
||
// 6. Упаковка
|
||
// 7. Наценка
|
||
// 8. Итоги
|
||
let text = `🧼 <b>Расчёт мыла:</b> <i>${soapName}</i>\n\n`;
|
||
|
||
text += `🔹 <b>Вес мыла:</b> ${weight} г\n`;
|
||
text += `🔹 <b>Цена за 1 кг основы:</b> ${basePrice} ₽/кг\n\n`;
|
||
|
||
text += `🔹 <b>Отдушка:</b> ${aromaWeight} г по ${aromaPrice} ₽/фасовка\n`;
|
||
text += `🔹 <b>Пигмент:</b> ${pigmentWeight} г по ${pigmentPrice} ₽/фасовка\n\n`;
|
||
|
||
text += `🔹 <b>Цена формы:</b> ${moldPrice} ₽\n\n`;
|
||
|
||
text += `🔹 <b>Упаковка:</b>\n`;
|
||
text += ` • Пакет/коробка: ${box} ₽\n`;
|
||
text += ` • Наполнитель: ${filler} ₽\n`;
|
||
text += ` • Лента: ${ribbon} ₽\n`;
|
||
text += ` • Наклейка: ${labelValue} ₽\n\n`;
|
||
|
||
text += `🔹 <b>Наценка:</b> ${markup}%\n\n`;
|
||
|
||
text += `📊 <b>Итоги расчёта:</b>\n`;
|
||
text += ` • Себестоимость: ${Number(totalCost).toFixed(1)} ₽\n`;
|
||
text += ` • Итоговая цена с наценкой: ${Number(finalPrice).toFixed(1)} ₽\n`;
|
||
text += ` • Цена за 100 г: ${Number(pricePer100g).toFixed(1)} ₽`;
|
||
|
||
// Если пользователь прикрепил фото (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: 'Открыть калькулятор' },
|
||
]);
|
||
|
||
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);
|
||
}
|
||
});
|
||
|
||
// 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}`);
|
||
}); |