Files
hotell777_260507/modules/telegram/index.js
kalugin66 d539e1f0c0
All checks were successful
Deploy hotel / deploy-kdo (push) Successful in 42s
апи
2026-07-21 20:06:43 +05:00

375 lines
14 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 = {};
let connectionMode = null;
let connectionLoggedKey = null;
const DEFAULT_API = 'https://api.telegram.org';
function init(database, settings) {
db = database;
settingsModule = settings;
}
function getSettings(callback) {
settingsModule.getAll((err, all) => {
if (err) return callback(err);
callback(null, all);
});
}
function isNetworkError(err) {
const msg = (err.message || '').toLowerCase();
return msg.includes('econnrefused') || msg.includes('etimedout') ||
msg.includes('enotfound') || msg.includes('fetch failed') ||
msg.includes('network') || err.name === 'TypeError' ||
err.cause !== undefined;
}
function apiCall(settings, method, body, callback) {
const token = settings.telegram_bot_token;
const apiUrl = (settings.telegram_api_url || DEFAULT_API).replace(/\/+$/, '');
const proxyUrl = apiUrl !== DEFAULT_API ? apiUrl : null;
const urlsToTry = proxyUrl ? [proxyUrl, DEFAULT_API] : [DEFAULT_API];
function tryNext(index) {
if (index >= urlsToTry.length) {
return callback(new Error('Telegram недоступен: все адреса недоступны'));
}
const url = urlsToTry[index];
const fullUrl = `${url}/bot${token}/${method}`;
const fetchOpts = body ? {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
} : {};
fetch(fullUrl, fetchOpts)
.then(res => res.json().then(data => ({ status: res.status, data })))
.then(result => {
const data = result.data;
if (!data.ok) {
const err = new Error(data.description);
err.code = data.error_code;
err.fromProxy = url !== DEFAULT_API;
return callback(err);
}
if (url !== DEFAULT_API) {
updateConnectionMode('proxied', url);
} else {
updateConnectionMode('direct', null);
}
callback(null, data);
})
.catch(fetchErr => {
if (isNetworkError(fetchErr) && index < urlsToTry.length - 1) {
tryNext(index + 1);
} else if (isNetworkError(fetchErr)) {
updateConnectionMode('blocked', null);
callback(fetchErr);
} else {
callback(fetchErr);
}
});
}
tryNext(0);
}
function updateConnectionMode(mode, proxyUrl) {
const key = mode + (proxyUrl || '');
if (connectionLoggedKey === key) return;
connectionLoggedKey = key;
connectionMode = mode;
if (mode === 'direct') {
console.log('Telegram подключён напрямую');
} else if (mode === 'proxied') {
console.log(`Telegram подключён через прокси-сервер: ${proxyUrl}`);
} else if (mode === 'blocked') {
console.error('Telegram заблокирован. Укажите прокси-URL в настройках Telegram.');
}
}
function sendMessage(token, chatId, text, callback) {
getSettings((err, settings) => {
if (err) {
if (callback) callback(err);
return;
}
apiCall(settings, 'sendMessage', { chat_id: chatId, text: text, parse_mode: 'HTML' }, (err, data) => {
if (!err) {
if (callback) callback(null, data);
return;
}
if (!err.code) {
const key = `network:${chatId}`;
if (lastSendErrors[key] !== true) {
lastSendErrors[key] = true;
console.error(`Telegram sendMessage: chat ${chatId} — ${err.message}`);
}
} else {
const key = `${chatId}:${err.code}:${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;
apiCall(settings, `getUpdates?offset=${lastUpdateId + 1}&timeout=10`, null, (err, data) => {
if (err) {
if (err.code === 404) {
if (!pollingFatalLogged) {
pollingFatalLogged = true;
console.error(`Telegram polling: бот не найден — ${err.message}. Polling остановлен до смены токена.`);
}
return;
}
if (err.code) {
pollErrorCount++;
const backoff = Math.min(5000 * Math.pow(2, pollErrorCount - 1), 300000);
const errKey = `${err.code}:${err.message}`;
if (lastPollErrorKey !== errKey) {
lastPollErrorKey = errKey;
console.error(`Telegram polling: ${err.message}`);
}
schedulePolling(backoff);
return;
}
pollErrorCount++;
const backoff = Math.min(5000 * Math.pow(2, pollErrorCount - 1), 300000);
const errKey = `network:${err.message}`;
if (lastPollErrorKey !== errKey) {
lastPollErrorKey = errKey;
}
schedulePolling(backoff);
return;
}
pollErrorCount = 0;
lastPollErrorKey = null;
if (!pollingActiveLogged) {
pollingActiveLogged = true;
const modeText = connectionMode === 'proxied' ? ' (через прокси)' : '';
console.log(`Telegram polling active — бот слушает сообщения${modeText}`);
}
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);
});
});
}
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 = {};
connectionMode = null;
connectionLoggedKey = null;
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) {
let errText = err.message;
if (!err.code) errText = `${errText} (проверьте подключение или укажите прокси-URL)`;
errors.push(`chat_id ${chatId}: ${errText}`);
} 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 || '',
telegram_api_url: settings.telegram_api_url || DEFAULT_API,
telegram_proxy_status: connectionMode || 'unknown'
});
});
});
app.put('/api/admin/settings/telegram', authenticateToken, requireAdmin, (req, res) => {
const { telegram_bot_token, telegram_notify_enabled, telegram_notify_users, telegram_api_url } = 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]);
}
if (telegram_api_url !== undefined) {
const url = (telegram_api_url || '').trim();
updates.push(['telegram_api_url', url || DEFAULT_API]);
}
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, connectionMode };