Files
hotell777_260507/modules/telegram/index.js
kalugin66 d612febefb
All checks were successful
Deploy hotel / deploy-kdo (push) Successful in 35s
телеграм
2026-07-20 19:06:49 +05:00

290 lines
11 KiB
JavaScript
Raw 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.
let db;
let settingsModule;
let lastUpdateId = 0;
let pollingTimeout = null;
let pollErrorCount = 0;
let lastPollErrorKey = null;
let pollingFatalLogged = false;
let pollingActiveLogged = false;
let lastSendErrors = {};
function init(database, settings) {
db = database;
settingsModule = settings;
}
function getSettings(callback) {
settingsModule.getAll((err, all) => {
if (err) return callback(err);
callback(null, all);
});
}
function sendMessage(token, chatId, text, callback) {
const body = JSON.stringify({
chat_id: chatId,
text: text,
parse_mode: 'HTML'
});
fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: body
})
.then(res => res.json())
.then(data => {
if (!data.ok) {
const key = `${chatId}:${data.error_code}:${data.description}`;
if (lastSendErrors[key] !== true) {
lastSendErrors[key] = true;
console.error(`Telegram sendMessage: chat ${chatId} — ${data.description}`);
}
if (callback) callback(new Error(data.description));
} else {
if (callback) callback(null, data);
}
})
.catch(err => {
const key = `fetch:${chatId}:${err.message}`;
if (lastSendErrors[key] !== true) {
lastSendErrors[key] = true;
console.error(`Telegram sendMessage: chat ${chatId} — ${err.message}`);
}
if (callback) callback(err);
});
}
function sendBookingNotification(booking) {
if (!booking.phone) return;
getSettings((err, settings) => {
if (err || settings.telegram_notify_enabled !== 'true') return;
const token = settings.telegram_bot_token;
if (!token) return;
const usersStr = settings.telegram_notify_users || '';
const chatIds = usersStr.split(',').map(s => s.trim()).filter(s => s);
if (chatIds.length === 0) return;
const text = [
'<b>Hotel 777 — Новая бронь</b>',
'',
`<b>Имя:</b> ${escapeHtml(booking.name)}`,
`<b>Телефон:</b> ${escapeHtml(booking.phone)}`,
`<b>Номер:</b> ${escapeHtml(booking.room_type || '—')}`,
`<b>Гостей:</b> ${booking.adults} взр.${booking.children ? ', ' + booking.children + ' дет.' : ''}`,
`<b>Заезд:</b> ${escapeHtml(booking.checkin_date)}`,
`<b>Выезд:</b> ${escapeHtml(booking.checkout_date)}`,
`<b>Сумма:</b> ${booking.total_price || booking.base_price || 0} ₽`,
];
if (booking.wishes) {
text.push(`<b>Пожелания:</b> ${escapeHtml(booking.wishes)}`);
}
text.push('');
text.push('<a href="https://hotel777.ru/admin">Перейти в админ-панель</a>');
const message = text.join('\n');
chatIds.forEach(chatId => {
sendMessage(token, chatId, message);
});
});
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
function startPolling() {
getSettings((err, settings) => {
if (err || !settings.telegram_bot_token) {
schedulePolling(60000);
return;
}
const token = settings.telegram_bot_token;
fetch(`https://api.telegram.org/bot${token}/getUpdates?offset=${lastUpdateId + 1}&timeout=10`)
.then(res => res.json())
.then(data => {
if (!data.ok) {
if (data.error_code === 404) {
if (!pollingFatalLogged) {
pollingFatalLogged = true;
console.error(`Telegram polling: бот не найден — ${data.description}. Polling остановлен до смены токена.`);
}
return;
}
pollErrorCount++;
const backoff = Math.min(5000 * Math.pow(2, pollErrorCount - 1), 300000);
const errKey = `${data.error_code}:${data.description}`;
if (lastPollErrorKey !== errKey) {
lastPollErrorKey = errKey;
console.error(`Telegram polling: ${data.description}`);
}
schedulePolling(backoff);
return;
}
pollErrorCount = 0;
lastPollErrorKey = null;
if (!pollingActiveLogged) {
pollingActiveLogged = true;
console.log('Telegram polling active — бот слушает сообщения');
}
if (data.result && data.result.length > 0) {
data.result.forEach(update => {
lastUpdateId = update.update_id;
if (update.message && update.message.text && update.message.chat) {
const chatId = update.message.chat.id;
const replyText = `<b>Ваш chat_id:</b> <code>${chatId}</code>\n\nДобавьте его в настройках админ-панели Hotel 777 для получения уведомлений о новых заявках.`;
sendMessage(token, chatId, replyText);
}
});
}
schedulePolling(1000);
})
.catch(err => {
pollErrorCount++;
const backoff = Math.min(5000 * Math.pow(2, pollErrorCount - 1), 300000);
const errKey = `fetch:${err.message}`;
if (lastPollErrorKey !== errKey) {
lastPollErrorKey = errKey;
console.error(`Telegram polling: ${err.message}`);
}
schedulePolling(backoff);
});
});
}
function schedulePolling(ms) {
if (pollingTimeout) clearTimeout(pollingTimeout);
pollingTimeout = setTimeout(startPolling, ms);
}
function stopPolling() {
if (pollingTimeout) {
clearTimeout(pollingTimeout);
pollingTimeout = null;
}
}
function resetPollState() {
stopPolling();
lastUpdateId = 0;
pollErrorCount = 0;
lastPollErrorKey = null;
pollingFatalLogged = false;
pollingActiveLogged = false;
lastSendErrors = {};
startPolling();
}
function sendTestMessage(settings, callback) {
const token = settings.telegram_bot_token;
if (!token) return callback(new Error('Токен бота не настроен'));
const usersStr = settings.telegram_notify_users || '';
const chatIds = usersStr.split(',').map(s => s.trim()).filter(s => s);
if (chatIds.length === 0) return callback(new Error('Не указаны chat_id получателей'));
let sent = 0;
let errors = [];
chatIds.forEach((chatId, index) => {
sendMessage(token, chatId, '<b>Hotel 777</b> — Тестовое уведомление Telegram. Бот работает корректно.', (err) => {
if (err) {
errors.push(`chat_id ${chatId}: ${err.message}`);
} else {
sent++;
}
if (index === chatIds.length - 1) {
if (errors.length > 0) {
callback(null, { sent, total: chatIds.length, errors });
} else {
callback(null, { sent, total: chatIds.length, errors: [] });
}
}
});
});
}
function setupRoutes(app, authenticateToken, requireAdmin) {
app.get('/api/admin/settings/telegram', authenticateToken, requireAdmin, (req, res) => {
getSettings((err, settings) => {
if (err) return res.status(500).json({ error: 'Database error' });
const token = settings.telegram_bot_token || '';
res.json({
telegram_bot_token_masked: token ? maskToken(token) : '',
telegram_notify_enabled: settings.telegram_notify_enabled || 'false',
telegram_notify_users: settings.telegram_notify_users || ''
});
});
});
app.put('/api/admin/settings/telegram', authenticateToken, requireAdmin, (req, res) => {
const { telegram_bot_token, telegram_notify_enabled, telegram_notify_users } = req.body;
const updates = [];
if (telegram_bot_token !== undefined && !(telegram_bot_token || '').startsWith('****')) {
updates.push(['telegram_bot_token', telegram_bot_token]);
}
if (telegram_notify_enabled !== undefined) {
updates.push(['telegram_notify_enabled', telegram_notify_enabled]);
}
if (telegram_notify_users !== undefined) {
updates.push(['telegram_notify_users', telegram_notify_users]);
}
let processed = 0;
if (updates.length === 0) return res.json({ message: 'No changes' });
updates.forEach(([key, value]) => {
settingsModule.set(key, value, (err) => {
if (err) console.error('Telegram settings update error:', err);
processed++;
if (processed === updates.length) {
res.json({ message: 'Настройки Telegram сохранены' });
resetPollState();
}
});
});
});
app.post('/api/admin/settings/telegram/test', authenticateToken, requireAdmin, (req, res) => {
getSettings((err, settings) => {
if (err) return res.status(500).json({ error: 'Database error' });
sendTestMessage(settings, (sendErr, result) => {
if (sendErr) return res.status(400).json({ error: sendErr.message });
if (result.errors.length > 0) {
return res.json({
message: `Отправлено ${result.sent} из ${result.total}. Ошибки: ${result.errors.join('; ')}`,
sent: result.sent,
total: result.total
});
}
res.json({ message: `Тестовое сообщение отправлено (${result.sent}/${result.total})` });
});
});
});
}
function maskToken(val) {
if (!val) return '';
const parts = val.split(':');
if (parts.length === 2) {
const idPart = parts[0];
const hashPart = parts[1];
if (hashPart.length <= 4) return '****';
return idPart.slice(0, 3) + '***:' + '****' + hashPart.slice(-4);
}
return '****';
}
module.exports = { init, setupRoutes, sendBookingNotification, sendTestMessage, startPolling, stopPolling };