import TelegramBot from 'node-telegram-bot-api'; import { v4 as uuidv4 } from 'uuid'; import { query } from '../database/connection'; import { NotificationService } from '../services/notificationService'; interface NotificationSettings { newMatches: boolean; newMessages: boolean; newLikes: boolean; reminders: boolean; dailySummary: boolean; timePreference: 'morning' | 'afternoon' | 'evening' | 'night'; doNotDisturb: boolean; doNotDisturbStart?: string; doNotDisturbEnd?: string; } export class NotificationHandlers { private bot: TelegramBot; private notificationService: NotificationService; constructor(bot: TelegramBot) { this.bot = bot; this.notificationService = new NotificationService(bot); } // Метод для получения экземпляра сервиса уведомлений getNotificationService(): NotificationService { return this.notificationService; } // Обработка команды /notifications async handleNotificationsCommand(msg: TelegramBot.Message): Promise { const telegramId = msg.from?.id.toString(); if (!telegramId) return; try { const userId = await this.getUserIdByTelegramId(telegramId); if (!userId) { await this.bot.sendMessage(msg.chat.id, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.'); return; } const settings = await this.notificationService.getNotificationSettings(userId); await this.sendNotificationSettings(msg.chat.id, settings as NotificationSettings); } catch (error) { console.error('Error handling notifications command:', error); await this.bot.sendMessage(msg.chat.id, '❌ Произошла ошибка при загрузке настроек уведомлений.'); } } // Отправка меню настроек уведомлений async sendNotificationSettings(chatId: number, settings: NotificationSettings): Promise { const message = ` 🔔 *Настройки уведомлений* Выберите, какие уведомления вы хотите получать: ${settings.newMatches ? '✅' : '❌'} Новые матчи ${settings.newMessages ? '✅' : '❌'} Новые сообщения ${settings.newLikes ? '✅' : '❌'} Новые лайки ${settings.reminders ? '✅' : '❌'} Напоминания ${settings.dailySummary ? '✅' : '❌'} Ежедневные сводки ⏰ Предпочтительное время: ${this.getTimePreferenceText(settings.timePreference)} ${settings.doNotDisturb ? '🔕' : '🔔'} Режим "Не беспокоить": ${settings.doNotDisturb ? 'Включен' : 'Выключен'} ${settings.doNotDisturb && settings.doNotDisturbStart && settings.doNotDisturbEnd ? `с ${settings.doNotDisturbStart} до ${settings.doNotDisturbEnd}` : ''} Нажмите на кнопку, чтобы изменить настройку: `; await this.bot.sendMessage(chatId, message, { parse_mode: 'Markdown', reply_markup: { inline_keyboard: [ [ { text: `${settings.newMatches ? '✅' : '❌'} Новые матчи`, callback_data: 'notif_toggle:newMatches' }, { text: `${settings.newMessages ? '✅' : '❌'} Новые сообщения`, callback_data: 'notif_toggle:newMessages' } ], [ { text: `${settings.newLikes ? '✅' : '❌'} Новые лайки`, callback_data: 'notif_toggle:newLikes' }, { text: `${settings.reminders ? '✅' : '❌'} Напоминания`, callback_data: 'notif_toggle:reminders' } ], [ { text: `${settings.dailySummary ? '✅' : '❌'} Ежедневные сводки`, callback_data: 'notif_toggle:dailySummary' } ], [ { text: `⏰ Время: ${this.getTimePreferenceText(settings.timePreference)}`, callback_data: 'notif_time' } ], [ { text: `${settings.doNotDisturb ? '🔕' : '🔔'} Режим "Не беспокоить"`, callback_data: 'notif_dnd' } ], [ { text: '↩️ Назад', callback_data: 'settings' } ] ] } }); } // Обработка переключения настройки уведомления async handleNotificationToggle(callbackQuery: TelegramBot.CallbackQuery): Promise { const telegramId = callbackQuery.from?.id.toString(); if (!telegramId || !callbackQuery.message) return; try { const userId = await this.getUserIdByTelegramId(telegramId); if (!userId) { await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' }); return; } // notif_toggle:settingName const settingName = callbackQuery.data?.split(':')[1]; if (!settingName) return; const settings = await this.notificationService.getNotificationSettings(userId); let updatedSettings: Partial = { ...settings }; // Инвертируем значение настройки if (settingName in updatedSettings) { switch(settingName) { case 'newMatches': updatedSettings.newMatches = !updatedSettings.newMatches; break; case 'newMessages': updatedSettings.newMessages = !updatedSettings.newMessages; break; case 'newLikes': updatedSettings.newLikes = !updatedSettings.newLikes; break; case 'reminders': updatedSettings.reminders = !updatedSettings.reminders; break; case 'dailySummary': updatedSettings.dailySummary = !updatedSettings.dailySummary; break; } } // Обновляем настройки await this.notificationService.updateNotificationSettings(userId, updatedSettings); // Отправляем обновленные настройки await this.bot.answerCallbackQuery(callbackQuery.id, { text: `✅ Настройка "${this.getSettingName(settingName)}" ${updatedSettings[settingName as keyof NotificationSettings] ? 'включена' : 'отключена'}` }); await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings as NotificationSettings); } catch (error) { console.error('Error handling notification toggle:', error); await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при обновлении настроек.' }); } } // Обработка выбора времени для уведомлений async handleTimePreference(callbackQuery: TelegramBot.CallbackQuery): Promise { if (!callbackQuery.message) return; await this.bot.editMessageText('⏰ *Выберите предпочтительное время для уведомлений:*', { chat_id: callbackQuery.message.chat.id, message_id: callbackQuery.message.message_id, parse_mode: 'Markdown', reply_markup: { inline_keyboard: [ [ { text: '🌅 Утро (9:00)', callback_data: 'notif_time_set:morning' }, { text: '☀️ День (13:00)', callback_data: 'notif_time_set:afternoon' } ], [ { text: '🌆 Вечер (19:00)', callback_data: 'notif_time_set:evening' }, { text: '🌙 Ночь (22:00)', callback_data: 'notif_time_set:night' } ], [ { text: '↩️ Назад', callback_data: 'notifications' } ] ] } }); await this.bot.answerCallbackQuery(callbackQuery.id); } // Обработка установки времени для уведомлений async handleTimePreferenceSet(callbackQuery: TelegramBot.CallbackQuery): Promise { const telegramId = callbackQuery.from?.id.toString(); if (!telegramId || !callbackQuery.message) return; try { const userId = await this.getUserIdByTelegramId(telegramId); if (!userId) { await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' }); return; } // notif_time_set:timePreference const timePreference = callbackQuery.data?.split(':')[1] as 'morning' | 'afternoon' | 'evening' | 'night'; if (!timePreference) return; const settings = await this.notificationService.getNotificationSettings(userId); // Копируем существующие настройки и обновляем нужные поля const existingSettings = settings as NotificationSettings; const updatedSettings: NotificationSettings = { ...existingSettings, timePreference }; // Обновляем настройки await this.notificationService.updateNotificationSettings(userId, updatedSettings); // Отправляем обновленные настройки await this.bot.answerCallbackQuery(callbackQuery.id, { text: `✅ Время уведомлений установлено на ${this.getTimePreferenceText(timePreference)}` }); await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings); } catch (error) { console.error('Error handling time preference set:', error); await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при обновлении времени уведомлений.' }); } } // Обработка режима "Не беспокоить" async handleDndMode(callbackQuery: TelegramBot.CallbackQuery): Promise { if (!callbackQuery.message) return; await this.bot.editMessageText('🔕 *Режим "Не беспокоить"*\n\nВыберите действие:', { chat_id: callbackQuery.message.chat.id, message_id: callbackQuery.message.message_id, parse_mode: 'Markdown', reply_markup: { inline_keyboard: [ [ { text: '✅ Включить', callback_data: 'notif_dnd_set:on' }, { text: '❌ Выключить', callback_data: 'notif_dnd_set:off' } ], [ { text: '⏰ Настроить время', callback_data: 'notif_dnd_time' } ], [ { text: '↩️ Назад', callback_data: 'notifications' } ] ] } }); await this.bot.answerCallbackQuery(callbackQuery.id); } // Обработка установки режима "Не беспокоить" async handleDndModeSet(callbackQuery: TelegramBot.CallbackQuery): Promise { const telegramId = callbackQuery.from?.id.toString(); if (!telegramId || !callbackQuery.message) return; try { const userId = await this.getUserIdByTelegramId(telegramId); if (!userId) { await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' }); return; } // notif_dnd_set:on/off const mode = callbackQuery.data?.split(':')[1]; if (!mode) return; const settings = await this.notificationService.getNotificationSettings(userId); // Копируем существующие настройки и обновляем нужное поле const existingSettings = settings as NotificationSettings; let updatedSettings: NotificationSettings = { ...existingSettings, doNotDisturb: mode === 'on' }; // Если включаем режим "Не беспокоить", но не задано время, ставим дефолтные значения if (mode === 'on' && (!updatedSettings.doNotDisturbStart || !updatedSettings.doNotDisturbEnd)) { updatedSettings.doNotDisturbStart = '23:00'; updatedSettings.doNotDisturbEnd = '08:00'; } // Обновляем настройки await this.notificationService.updateNotificationSettings(userId, updatedSettings); // Отправляем обновленные настройки await this.bot.answerCallbackQuery(callbackQuery.id, { text: `✅ Режим "Не беспокоить" ${mode === 'on' ? 'включен' : 'выключен'}` }); await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings); } catch (error) { console.error('Error handling DND mode set:', error); await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при обновлении режима "Не беспокоить".' }); } } // Настройка времени для режима "Не беспокоить" async handleDndTimeSetup(callbackQuery: TelegramBot.CallbackQuery): Promise { if (!callbackQuery.message) return; await this.bot.editMessageText('⏰ *Настройка времени для режима "Не беспокоить"*\n\nВыберите один из предустановленных вариантов или введите свой:', { chat_id: callbackQuery.message.chat.id, message_id: callbackQuery.message.message_id, parse_mode: 'Markdown', reply_markup: { inline_keyboard: [ [ { text: '🌙 23:00 - 08:00', callback_data: 'notif_dnd_time_set:23:00:08:00' } ], [ { text: '🌙 22:00 - 07:00', callback_data: 'notif_dnd_time_set:22:00:07:00' } ], [ { text: '🌙 00:00 - 09:00', callback_data: 'notif_dnd_time_set:00:00:09:00' } ], [ { text: '✏️ Ввести свой вариант', callback_data: 'notif_dnd_time_custom' } ], [ { text: '↩️ Назад', callback_data: 'notif_dnd' } ] ] } }); await this.bot.answerCallbackQuery(callbackQuery.id); } // Установка предустановленного времени для режима "Не беспокоить" async handleDndTimeSet(callbackQuery: TelegramBot.CallbackQuery): Promise { const telegramId = callbackQuery.from?.id.toString(); if (!telegramId || !callbackQuery.message) return; try { const userId = await this.getUserIdByTelegramId(telegramId); if (!userId) { await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' }); return; } // notif_dnd_time_set:startTime:endTime const parts = callbackQuery.data?.split(':'); if (parts && parts.length >= 4) { const startTime = `${parts[2]}:${parts[3]}`; const endTime = `${parts[4]}:${parts[5]}`; const settings = await this.notificationService.getNotificationSettings(userId); // Копируем существующие настройки и обновляем нужные поля const existingSettings = settings as NotificationSettings; const updatedSettings: NotificationSettings = { ...existingSettings, doNotDisturb: true, doNotDisturbStart: startTime, doNotDisturbEnd: endTime }; // Обновляем настройки await this.notificationService.updateNotificationSettings(userId, updatedSettings); // Отправляем обновленные настройки await this.bot.answerCallbackQuery(callbackQuery.id, { text: `✅ Время "Не беспокоить" установлено с ${startTime} до ${endTime}` }); await this.sendNotificationSettings(callbackQuery.message.chat.id, updatedSettings); } } catch (error) { console.error('Error handling DND time set:', error); await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при настройке времени "Не беспокоить".' }); } } // Запрос пользовательского времени для режима "Не беспокоить" async handleDndTimeCustom(callbackQuery: TelegramBot.CallbackQuery): Promise { if (!callbackQuery.message) return; // Устанавливаем ожидание пользовательского ввода const userId = callbackQuery.from?.id.toString(); if (userId) { await this.setUserState(userId, 'waiting_dnd_time'); } await this.bot.editMessageText('⏰ *Введите время для режима "Не беспокоить"*\n\nУкажите время в формате:\n`с [ЧЧ:ММ] до [ЧЧ:ММ]`\n\nНапример: `с 23:30 до 07:00`', { chat_id: callbackQuery.message.chat.id, message_id: callbackQuery.message.message_id, parse_mode: 'Markdown', reply_markup: { inline_keyboard: [ [ { text: '↩️ Отмена', callback_data: 'notif_dnd_time' } ] ] } }); await this.bot.answerCallbackQuery(callbackQuery.id); } // Обработка пользовательского ввода времени для режима "Не беспокоить" async handleDndTimeInput(msg: TelegramBot.Message): Promise { const telegramId = msg.from?.id.toString(); if (!telegramId) return; try { const userId = await this.getUserIdByTelegramId(telegramId); if (!userId) { await this.bot.sendMessage(msg.chat.id, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.'); return; } // Очищаем состояние ожидания await this.clearUserState(telegramId); // Парсим введенное время const timeRegex = /с\s+(\d{1,2}[:\.]\d{2})\s+до\s+(\d{1,2}[:\.]\d{2})/i; const match = msg.text?.match(timeRegex); if (match && match.length >= 3) { let startTime = match[1].replace('.', ':'); let endTime = match[2].replace('.', ':'); // Проверяем и форматируем время if (this.isValidTime(startTime) && this.isValidTime(endTime)) { startTime = this.formatTime(startTime); endTime = this.formatTime(endTime); const settings = await this.notificationService.getNotificationSettings(userId); // Копируем существующие настройки и обновляем нужные поля const existingSettings = settings as NotificationSettings; const updatedSettings: NotificationSettings = { ...existingSettings, doNotDisturb: true, doNotDisturbStart: startTime, doNotDisturbEnd: endTime }; // Обновляем настройки await this.notificationService.updateNotificationSettings(userId, updatedSettings); await this.bot.sendMessage(msg.chat.id, `✅ Время "Не беспокоить" установлено с ${startTime} до ${endTime}`); await this.sendNotificationSettings(msg.chat.id, updatedSettings); } else { await this.bot.sendMessage(msg.chat.id, '❌ Неверный формат времени. Пожалуйста, используйте формат ЧЧ:ММ (например, 23:30).'); } } else { await this.bot.sendMessage(msg.chat.id, '❌ Неверный формат ввода. Пожалуйста, введите время в формате "с [ЧЧ:ММ] до [ЧЧ:ММ]" (например, "с 23:30 до 07:00").'); } } catch (error) { console.error('Error handling DND time input:', error); await this.bot.sendMessage(msg.chat.id, '❌ Произошла ошибка при настройке времени "Не беспокоить".'); } } // Проверка валидности времени private isValidTime(time: string): boolean { const regex = /^(\d{1,2}):(\d{2})$/; const match = time.match(regex); if (match) { const hours = parseInt(match[1]); const minutes = parseInt(match[2]); return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59; } return false; } // Форматирование времени в формат ЧЧ:ММ private formatTime(time: string): string { const [hours, minutes] = time.split(':').map(Number); return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`; } // Получение текстового представления времени private getTimePreferenceText(preference: string): string { switch (preference) { case 'morning': return 'Утро (9:00)'; case 'afternoon': return 'День (13:00)'; case 'evening': return 'Вечер (19:00)'; case 'night': return 'Ночь (22:00)'; default: return 'Вечер (19:00)'; } } // Получение названия настройки private getSettingName(setting: string): string { switch (setting) { case 'newMatches': return 'Новые матчи'; case 'newMessages': return 'Новые сообщения'; case 'newLikes': return 'Новые лайки'; case 'reminders': return 'Напоминания'; case 'dailySummary': return 'Ежедневные сводки'; default: return setting; } } // Получение ID пользователя по Telegram ID private async getUserIdByTelegramId(telegramId: string): Promise { try { const result = await query( 'SELECT id FROM users WHERE telegram_id = $1', [parseInt(telegramId)] ); return result.rows.length > 0 ? result.rows[0].id : null; } catch (error) { console.error('Error getting user by telegram ID:', error); return null; } } // Установка состояния ожидания пользователя private async setUserState(telegramId: string, state: string): Promise { try { const userId = await this.getUserIdByTelegramId(telegramId); if (!userId) return; // Сначала проверяем, существуют ли столбцы state и state_data const checkColumnResult = await query(` SELECT column_name FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'state' `); if (checkColumnResult.rows.length === 0) { console.log('Adding state and state_data columns to users table...'); // Добавляем столбцы, если их нет await query(` ALTER TABLE users ADD COLUMN IF NOT EXISTS state VARCHAR(255) NULL; ALTER TABLE users ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}'::jsonb; `); } // Теперь устанавливаем состояние await query( `UPDATE users SET state = $1, state_data = jsonb_set(COALESCE(state_data, '{}'::jsonb), '{timestamp}', to_jsonb(NOW())) WHERE telegram_id = $2`, [state, parseInt(telegramId)] ); } catch (error) { console.error('Error setting user state:', error); } } // Очистка состояния ожидания пользователя private async clearUserState(telegramId: string): Promise { try { await query( 'UPDATE users SET state = NULL WHERE telegram_id = $1', [parseInt(telegramId)] ); } catch (error) { console.error('Error clearing user state:', error); } } // Регистрация обработчиков уведомлений register(): void { // Команда настройки уведомлений this.bot.onText(/\/notifications/, this.handleNotificationsCommand.bind(this)); // Обработчик для кнопки настроек уведомлений в меню настроек this.bot.on('callback_query', async (callbackQuery) => { if (callbackQuery.data === 'notifications') { const telegramId = callbackQuery.from?.id.toString(); if (!telegramId || !callbackQuery.message) return; try { const userId = await this.getUserIdByTelegramId(telegramId); if (!userId) { await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Вы не зарегистрированы.' }); return; } const settings = await this.notificationService.getNotificationSettings(userId); await this.sendNotificationSettings(callbackQuery.message.chat.id, settings as NotificationSettings); await this.bot.answerCallbackQuery(callbackQuery.id); } catch (error) { console.error('Error handling notifications callback:', error); await this.bot.answerCallbackQuery(callbackQuery.id, { text: '❌ Произошла ошибка при загрузке настроек уведомлений.' }); } } else if (callbackQuery.data?.startsWith('notif_toggle:')) { await this.handleNotificationToggle(callbackQuery); } else if (callbackQuery.data === 'notif_time') { await this.handleTimePreference(callbackQuery); } else if (callbackQuery.data?.startsWith('notif_time_set:')) { await this.handleTimePreferenceSet(callbackQuery); } else if (callbackQuery.data === 'notif_dnd') { await this.handleDndMode(callbackQuery); } else if (callbackQuery.data?.startsWith('notif_dnd_set:')) { await this.handleDndModeSet(callbackQuery); } else if (callbackQuery.data === 'notif_dnd_time') { await this.handleDndTimeSetup(callbackQuery); } else if (callbackQuery.data?.startsWith('notif_dnd_time_set:')) { await this.handleDndTimeSet(callbackQuery); } else if (callbackQuery.data === 'notif_dnd_time_custom') { await this.handleDndTimeCustom(callbackQuery); } }); // Обработчик пользовательского ввода для времени "Не беспокоить" this.bot.on('message', async (msg) => { if (!msg.text) return; const telegramId = msg.from?.id.toString(); if (!telegramId) return; try { // Сначала проверяем, существует ли столбец state const checkColumnResult = await query(` SELECT column_name FROM information_schema.columns WHERE table_name = 'users' AND column_name = 'state' `); if (checkColumnResult.rows.length === 0) { console.log('State column does not exist in users table. Skipping state check.'); return; } // Теперь проверяем состояние пользователя const result = await query( 'SELECT state FROM users WHERE telegram_id = $1', [parseInt(telegramId)] ); if (result.rows.length > 0 && result.rows[0].state === 'waiting_dnd_time') { await this.handleDndTimeInput(msg); } } catch (error) { console.error('Error checking user state:', error); } }); } }