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 = [
'Hotel 777 — Новая бронь',
'',
`Имя: ${escapeHtml(booking.name)}`,
`Телефон: ${escapeHtml(booking.phone)}`,
`Номер: ${escapeHtml(booking.room_type || '—')}`,
`Гостей: ${booking.adults} взр.${booking.children ? ', ' + booking.children + ' дет.' : ''}`,
`Заезд: ${escapeHtml(booking.checkin_date)}`,
`Выезд: ${escapeHtml(booking.checkout_date)}`,
`Сумма: ${booking.total_price || booking.base_price || 0} ₽`,
];
if (booking.wishes) {
text.push(`Пожелания: ${escapeHtml(booking.wishes)}`);
}
text.push('');
text.push('Перейти в админ-панель');
const message = text.join('\n');
chatIds.forEach(chatId => {
sendMessage(token, chatId, message);
});
});
}
function escapeHtml(str) {
if (!str) return '';
return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
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 = `Ваш chat_id: ${chatId}\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, 'Hotel 777 — Тестовое уведомление 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 };