From 5ea3e8c1f3315a3af91237015daa7060053a9854 Mon Sep 17 00:00:00 2001 From: "Choi A.K." Date: Thu, 18 Sep 2025 13:46:35 +0900 Subject: [PATCH] alpha-test --- migrations/add_user_state_columns.sql | 14 + scripts/README.md | 76 + scripts/addPremiumColumn.js | 58 + scripts/checkCallbackHandlers.js | 88 ++ scripts/checkUserTable.js | 74 + scripts/createNotificationTables.js | 259 ++++ scripts/fixCallbackHandlers.js | 142 ++ scripts/fixDatabaseStructure.js | 170 +++ scripts/fix_all_notifications.js | 48 + scripts/fix_notification_callbacks.js | 332 +++++ scripts/setPremiumStatus.js | 73 + scripts/testCallbacks.js | 85 ++ scripts/testVipMethod.js | 81 + scripts/testVipStatus.js | 75 + scripts/update_bot_with_notifications.js | 104 ++ src/bot.ts | 51 +- src/handlers/callbackHandlers.ts | 76 +- .../callbackHandlers.ts.backup-1758166633763 | 606 ++++++++ src/handlers/callbackHandlers.ts.original | Bin 0 -> 204286 bytes src/handlers/callbackHandlers.ts.stub | 606 ++++++++ src/handlers/commandHandlers.ts | 20 +- src/handlers/notificationHandlers.ts | 644 ++++++++ src/scripts/setPremiumForAll.ts | 26 +- src/services/matchingService.ts | 2 +- src/services/notificationService.ts | 992 +++++++++++-- src/services/notificationService.ts.new | 1316 +++++++++++++++++ src/services/vipService.ts | 43 +- 27 files changed, 5887 insertions(+), 174 deletions(-) create mode 100644 migrations/add_user_state_columns.sql create mode 100644 scripts/README.md create mode 100644 scripts/addPremiumColumn.js create mode 100644 scripts/checkCallbackHandlers.js create mode 100644 scripts/checkUserTable.js create mode 100644 scripts/createNotificationTables.js create mode 100644 scripts/fixCallbackHandlers.js create mode 100644 scripts/fixDatabaseStructure.js create mode 100644 scripts/fix_all_notifications.js create mode 100644 scripts/fix_notification_callbacks.js create mode 100644 scripts/setPremiumStatus.js create mode 100644 scripts/testCallbacks.js create mode 100644 scripts/testVipMethod.js create mode 100644 scripts/testVipStatus.js create mode 100644 scripts/update_bot_with_notifications.js create mode 100644 src/handlers/callbackHandlers.ts.backup-1758166633763 create mode 100644 src/handlers/callbackHandlers.ts.original create mode 100644 src/handlers/callbackHandlers.ts.stub create mode 100644 src/handlers/notificationHandlers.ts create mode 100644 src/services/notificationService.ts.new diff --git a/migrations/add_user_state_columns.sql b/migrations/add_user_state_columns.sql new file mode 100644 index 0000000..0c6c0cd --- /dev/null +++ b/migrations/add_user_state_columns.sql @@ -0,0 +1,14 @@ +-- Добавление столбцов state и state_data в таблицу users для обработки состояний пользователя + +-- Добавляем столбец state для хранения текущего состояния пользователя +ALTER TABLE users ADD COLUMN IF NOT EXISTS state VARCHAR(255) NULL; + +-- Добавляем столбец state_data для хранения дополнительных данных о состоянии +ALTER TABLE users ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}'::jsonb; + +-- Добавляем индекс для быстрого поиска по state +CREATE INDEX IF NOT EXISTS idx_users_state ON users(state); + +-- Комментарий к столбцам +COMMENT ON COLUMN users.state IS 'Текущее состояние пользователя (например, ожидание ввода)'; +COMMENT ON COLUMN users.state_data IS 'Дополнительные данные о состоянии пользователя в формате JSON'; diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..6651500 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,76 @@ +# Исправление проблем с уведомлениями в боте + +Этот набор скриптов предназначен для исправления проблем с обработкой уведомлений в боте. + +## Описание проблемы + +После внедрения системы уведомлений и связанных с ней изменений в базе данных, возникла проблема с обработкой callback запросов. Бот перестал реагировать на все callback запросы, кроме тех, что связаны с уведомлениями. + +Проблема вызвана следующими факторами: +1. Отсутствие или неверная структура таблиц в базе данных для хранения уведомлений +2. Отсутствие необходимых полей `state` и `state_data` в таблице `users` +3. Отсутствие правильной регистрации обработчиков уведомлений в файле `bot.ts` + +## Решение + +Для решения проблемы были созданы следующие скрипты: + +### 1. `fix_notification_callbacks.js` +Проверяет и создает необходимые таблицы и столбцы в базе данных: +- Таблицы `notifications`, `scheduled_notifications`, `notification_templates` +- Столбцы `notification_settings`, `state`, `state_data` в таблице `users` + +### 2. `update_bot_with_notifications.js` +Обновляет файл `bot.ts`: +- Добавляет импорт класса `NotificationHandlers` +- Добавляет объявление поля `notificationHandlers` в класс `TelegramTinderBot` +- Добавляет создание экземпляра `NotificationHandlers` в конструкторе +- Добавляет регистрацию обработчиков уведомлений в методе `registerHandlers` + +### 3. `fix_all_notifications.js` +Запускает оба скрипта последовательно для полного исправления проблемы + +## Как использовать + +1. Остановите бота, если он запущен: + ```bash + # Нажмите Ctrl+C в терминале, где запущен бот + # или найдите процесс и завершите его + ``` + +2. Запустите комплексный скрипт исправления: + ```bash + node scripts/fix_all_notifications.js + ``` + +3. После успешного выполнения скрипта перезапустите бота: + ```bash + npm run start + ``` + +## Проверка результата + +После запуска бота убедитесь, что: +1. Бот отвечает на все callback запросы (включая кнопки, не связанные с уведомлениями) +2. Настройки уведомлений работают корректно (команда /notifications или кнопка в меню настроек) +3. Уведомления о лайках, супер-лайках и новых матчах приходят пользователям + +## Если проблемы остались + +Если после выполнения всех шагов проблемы остались, выполните следующие проверки: + +1. Проверьте логи бота на наличие ошибок +2. Проверьте структуру базы данных: + ```sql + \dt -- Список всех таблиц + \d notifications -- Структура таблицы notifications + \d scheduled_notifications -- Структура таблицы scheduled_notifications + \d notification_templates -- Структура таблицы notification_templates + \d users -- Убедитесь, что поля state, state_data и notification_settings существуют + ``` + +3. Проверьте код в файлах: + - `src/bot.ts`: должен содержать импорт, создание и регистрацию `NotificationHandlers` + - `src/handlers/callbackHandlers.ts`: должен правильно обрабатывать все callback-запросы + +В случае обнаружения ошибок, исправьте их вручную и перезапустите бота. diff --git a/scripts/addPremiumColumn.js b/scripts/addPremiumColumn.js new file mode 100644 index 0000000..1d3a247 --- /dev/null +++ b/scripts/addPremiumColumn.js @@ -0,0 +1,58 @@ +// Скрипт для добавления колонки premium в таблицу users и установки premium для всех пользователей +require('dotenv').config(); +const { Pool } = require('pg'); + +// Создаем пул соединений +const pool = new Pool({ + user: process.env.DB_USERNAME, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432') +}); + +async function setAllUsersToPremium() { + try { + console.log('Проверяем наличие столбца premium в таблице users...'); + + const result = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'users' + AND column_name = 'premium' + ); + `); + + if (!result.rows[0].exists) { + console.log('🔄 Добавляем столбец premium...'); + await pool.query(`ALTER TABLE users ADD COLUMN premium BOOLEAN DEFAULT false;`); + console.log('✅ Столбец premium успешно добавлен'); + } else { + console.log('✅ Столбец premium уже существует'); + } + + console.log('Устанавливаем премиум-статус для всех пользователей...'); + + const updateResult = await pool.query(` + UPDATE users + SET premium = true + WHERE true + RETURNING id, telegram_id, premium + `); + + console.log(`✅ Успешно установлен премиум-статус для ${updateResult.rows.length} пользователей:`); + updateResult.rows.forEach(row => { + console.log(`ID: ${row.id.substr(0, 8)}... | Telegram ID: ${row.telegram_id} | Premium: ${row.premium}`); + }); + + console.log('🎉 Все пользователи теперь имеют премиум-статус!'); + } catch (error) { + console.error('Ошибка при установке премиум-статуса:', error); + } finally { + await pool.end(); + console.log('Соединение с базой данных закрыто'); + } +} + +setAllUsersToPremium(); diff --git a/scripts/checkCallbackHandlers.js b/scripts/checkCallbackHandlers.js new file mode 100644 index 0000000..31087aa --- /dev/null +++ b/scripts/checkCallbackHandlers.js @@ -0,0 +1,88 @@ +// Скрипт для анализа и отладки проблем с обработчиками коллбэков +require('dotenv').config(); +const fs = require('fs'); +const path = require('path'); + +function analyzeCallbackHandlers() { + const filePath = path.join(__dirname, '..', 'src', 'handlers', 'callbackHandlers.ts'); + const content = fs.readFileSync(filePath, 'utf-8'); + + // Проверяем наличие реализаций методов + const methodsToCheck = [ + 'handleCreateProfile', + 'handleGenderSelection', + 'handleViewMyProfile', + 'handleEditProfile', + 'handleManagePhotos', + 'handleStartBrowsing', + 'handleSettings' + ]; + + const issues = []; + let debugInfo = []; + + methodsToCheck.forEach(method => { + debugInfo.push(`Проверяем метод: ${method}`); + + // Проверяем наличие полной реализации метода (не только сигнатуры) + const methodSignatureRegex = new RegExp(`async\\s+${method}\\s*\\([^)]*\\)\\s*:\\s*Promise\\s*{`, 'g'); + const hasSignature = methodSignatureRegex.test(content); + + const methodBodyRegex = new RegExp(`async\\s+${method}\\s*\\([^)]*\\)\\s*:\\s*Promise\\s*{[\\s\\S]+?}`, 'g'); + const methodMatch = content.match(methodBodyRegex); + + debugInfo.push(` Сигнатура найдена: ${hasSignature}`); + debugInfo.push(` Реализация найдена: ${methodMatch !== null}`); + + if (methodMatch) { + const methodContent = methodMatch[0]; + debugInfo.push(` Длина метода: ${methodContent.length} символов`); + + // Проверяем, содержит ли метод только заглушку + const isStub = methodContent.includes('// Заглушка метода') || + (!methodContent.includes('await') && methodContent.split('\n').length <= 3); + + if (isStub) { + issues.push(`❌ Метод ${method} содержит только заглушку, нет реальной реализации`); + } else { + debugInfo.push(` Метод ${method} имеет полную реализацию`); + } + } else if (hasSignature) { + issues.push(`❌ Метод ${method} имеет только сигнатуру, но нет реализации`); + } else { + issues.push(`❌ Метод ${method} не найден в файле`); + } + }); + + // Проверяем регистрацию обработчиков для NotificationHandlers + const notificationHandlersRegex = /this\.notificationHandlers\s*=\s*new\s+NotificationHandlers\(bot\);/g; + const hasNotificationHandlers = notificationHandlersRegex.test(content); + debugInfo.push(`NotificationHandlers инициализирован: ${hasNotificationHandlers}`); + + // Проверяем обработку коллбэка notifications + const notificationsCallbackRegex = /if\s*\(data\s*===\s*['"]notifications['"].*?\)/g; + const hasNotificationsCallback = notificationsCallbackRegex.test(content); + debugInfo.push(`Обработчик для callback 'notifications' найден: ${hasNotificationsCallback}`); + + // Выводим результаты + console.log('\n=== Анализ CallbackHandlers.ts ===\n'); + if (issues.length > 0) { + console.log('НАЙДЕНЫ ПРОБЛЕМЫ:'); + issues.forEach(issue => console.log(issue)); + console.log('\nРЕКОМЕНДАЦИИ:'); + console.log('1. Восстановите оригинальные реализации методов вместо заглушек'); + console.log('2. Убедитесь, что методы содержат необходимую бизнес-логику'); + console.log('3. Проверьте, что все коллбэки правильно обрабатываются'); + } else { + console.log('✅ Проблем не обнаружено'); + } + + console.log('\n=== Отладочная информация ===\n'); + debugInfo.forEach(info => console.log(info)); + + // Проверяем количество методов в файле + const asyncMethodsCount = (content.match(/async\s+handle[A-Za-z]+\s*\(/g) || []).length; + console.log(`\nВсего async методов в файле: ${asyncMethodsCount}`); +} + +analyzeCallbackHandlers(); diff --git a/scripts/checkUserTable.js b/scripts/checkUserTable.js new file mode 100644 index 0000000..811b3a6 --- /dev/null +++ b/scripts/checkUserTable.js @@ -0,0 +1,74 @@ +const { Pool } = require('pg'); +require('dotenv').config(); + +const pool = new Pool({ + user: process.env.DB_USER || 'postgres', + host: process.env.DB_HOST || 'localhost', + database: process.env.DB_NAME || 'telegram_tinder_db', + password: process.env.DB_PASSWORD || 'postgres', + port: parseInt(process.env.DB_PORT || '5432') +}); + +async function checkUserTableStructure() { + try { + // Получаем информацию о структуре таблицы users + const result = await pool.query(` + SELECT column_name, data_type, is_nullable + FROM information_schema.columns + WHERE table_name = 'users' + ORDER BY ordinal_position; + `); + + console.log('=== Структура таблицы users ==='); + console.table(result.rows); + + // Проверяем наличие столбцов state и state_data + const stateColumn = result.rows.find(row => row.column_name === 'state'); + const stateDataColumn = result.rows.find(row => row.column_name === 'state_data'); + + if (!stateColumn) { + console.log('❌ Столбец state отсутствует в таблице users'); + } else { + console.log('✅ Столбец state присутствует в таблице users'); + } + + if (!stateDataColumn) { + console.log('❌ Столбец state_data отсутствует в таблице users'); + } else { + console.log('✅ Столбец state_data присутствует в таблице users'); + } + + // Добавляем эти столбцы, если их нет + if (!stateColumn || !stateDataColumn) { + console.log('🔄 Добавление отсутствующих столбцов...'); + + await pool.query(` + ALTER TABLE users + ADD COLUMN IF NOT EXISTS state VARCHAR(255) NULL, + ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}'::jsonb; + `); + + console.log('✅ Столбцы успешно добавлены'); + } + + // Проверяем наличие других таблиц, связанных с уведомлениями + const tablesResult = await pool.query(` + SELECT tablename + FROM pg_catalog.pg_tables + WHERE schemaname = 'public' + AND tablename IN ('notifications', 'notification_settings', 'scheduled_notifications'); + `); + + console.log('\n=== Таблицы для уведомлений ==='); + console.table(tablesResult.rows); + + // Закрываем соединение + await pool.end(); + + } catch (error) { + console.error('Ошибка при проверке структуры таблицы:', error); + await pool.end(); + } +} + +checkUserTableStructure(); diff --git a/scripts/createNotificationTables.js b/scripts/createNotificationTables.js new file mode 100644 index 0000000..23ef00d --- /dev/null +++ b/scripts/createNotificationTables.js @@ -0,0 +1,259 @@ +const { Pool } = require('pg'); +const dotenv = require('dotenv'); +const uuid = require('uuid'); + +dotenv.config(); + +const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); + +async function createNotificationTables() { + const client = await pool.connect(); + + try { + await client.query('BEGIN'); + + console.log('Creating UUID extension if not exists...'); + await client.query(` + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + `); + + // Проверяем существование таблицы notifications + const notificationsExists = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'notifications' + ) as exists + `); + + if (!notificationsExists.rows[0].exists) { + console.log('Creating notifications table...'); + await client.query(` + CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + + console.log('Creating index on notifications...'); + await client.query(` + CREATE INDEX idx_notifications_user_id ON notifications (user_id); + CREATE INDEX idx_notifications_type ON notifications (type); + CREATE INDEX idx_notifications_created_at ON notifications (created_at); + `); + } else { + console.log('Notifications table already exists.'); + } + + // Проверяем существование таблицы scheduled_notifications + const scheduledExists = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'scheduled_notifications' + ) as exists + `); + + if (!scheduledExists.rows[0].exists) { + console.log('Creating scheduled_notifications table...'); + await client.query(` + CREATE TABLE scheduled_notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + scheduled_at TIMESTAMP NOT NULL, + processed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + + console.log('Creating index on scheduled_notifications...'); + await client.query(` + CREATE INDEX idx_scheduled_notifications_user_id ON scheduled_notifications (user_id); + CREATE INDEX idx_scheduled_notifications_scheduled_at ON scheduled_notifications (scheduled_at); + CREATE INDEX idx_scheduled_notifications_processed ON scheduled_notifications (processed); + `); + } else { + console.log('Scheduled_notifications table already exists.'); + } + + // Проверяем существование таблицы notification_templates + const templatesExists = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'notification_templates' + ) as exists + `); + + if (!templatesExists.rows[0].exists) { + console.log('Creating notification_templates table...'); + await client.query(` + CREATE TABLE notification_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + type VARCHAR(50) NOT NULL UNIQUE, + title TEXT NOT NULL, + message_template TEXT NOT NULL, + button_template JSONB, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + `); + } else { + console.log('Notification_templates table already exists.'); + } + + // Проверяем наличие колонки notification_settings в таблице users + const settingsColumnExists = await client.query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'notification_settings' + ) as exists + `); + + if (!settingsColumnExists.rows[0].exists) { + console.log('Adding notification_settings column to users table...'); + await client.query(` + ALTER TABLE users + ADD COLUMN notification_settings JSONB DEFAULT '{ + "newMatches": true, + "newMessages": true, + "newLikes": true, + "reminders": true, + "dailySummary": true, + "timePreference": "evening", + "doNotDisturb": false + }'::jsonb + `); + } else { + console.log('Notification_settings column already exists in users table.'); + } + + // Заполнение таблицы шаблонов уведомлений базовыми шаблонами + if (!templatesExists.rows[0].exists) { + console.log('Populating notification templates...'); + + const templates = [ + { + type: 'new_like', + title: 'Новый лайк!', + message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'super_like', + title: 'Супер-лайк!', + message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'new_match', + title: 'Новый матч!', + message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'new_message', + title: 'Новое сообщение!', + message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + button_template: { + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'match_reminder', + title: 'Напоминание о матче', + message_template: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }] + ] + } + }, + { + type: 'inactive_matches', + title: 'Неактивные матчи', + message_template: '⏰ У вас {{count}} неактивных матчей!\n\nПродолжите общение, чтобы не упустить интересные знакомства!', + button_template: { + inline_keyboard: [ + [{ text: '📋 Открыть матчи', callback_data: 'view_matches' }], + [{ text: '💕 Смотреть новые анкеты', callback_data: 'start_browsing' }] + ] + } + }, + { + type: 'like_summary', + title: 'Сводка лайков', + message_template: '💖 У вас {{count}} новых лайков!\n\nПосмотрите, кто проявил к вам интерес сегодня!', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть лайки', callback_data: 'view_likes' }], + [{ text: '💕 Начать знакомиться', callback_data: 'start_browsing' }] + ] + } + } + ]; + + for (const template of templates) { + await client.query(` + INSERT INTO notification_templates (id, type, title, message_template, button_template) + VALUES ($1, $2, $3, $4, $5) + `, [ + uuid.v4(), + template.type, + template.title, + template.message_template, + JSON.stringify(template.button_template) + ]); + } + } + + await client.query('COMMIT'); + console.log('Successfully created notification tables'); + } catch (err) { + await client.query('ROLLBACK'); + console.error('Error creating notification tables:', err); + } finally { + client.release(); + pool.end(); + } +} + +createNotificationTables().catch(err => console.error('Failed to create notification tables:', err)); diff --git a/scripts/fixCallbackHandlers.js b/scripts/fixCallbackHandlers.js new file mode 100644 index 0000000..e5182cf --- /dev/null +++ b/scripts/fixCallbackHandlers.js @@ -0,0 +1,142 @@ +// Скрипт для восстановления оригинальной функциональности callbackHandlers.ts +const fs = require('fs'); +const path = require('path'); + +// Находим самую последнюю версию файла callbackHandlers.ts в репозитории +const { execSync } = require('child_process'); + +try { + console.log('Поиск оригинальной версии CallbackHandlers.ts с полной функциональностью...'); + + // Находим коммиты, содержащие значительные изменения в файле (более 1000 символов) + const commits = execSync('git log --format="%H" -- src/handlers/callbackHandlers.ts') + .toString() + .trim() + .split('\n'); + + console.log(`Найдено ${commits.length} коммитов с изменениями файла`); + + // Пробуем разные коммиты, начиная с последнего, чтобы найти полную реализацию + let foundFullImplementation = false; + let fullImplementationContent = ''; + + for (const commit of commits) { + console.log(`Проверяем коммит ${commit.substring(0, 8)}...`); + + try { + const fileContent = execSync(`git show ${commit}:src/handlers/callbackHandlers.ts`).toString(); + + // Проверяем, содержит ли файл полные реализации методов + const hasFullImplementations = !fileContent.includes('// Заглушка метода') && + fileContent.includes('await this.bot.sendMessage'); + + if (hasFullImplementations) { + console.log(`✅ Найдена полная реализация в коммите ${commit.substring(0, 8)}`); + fullImplementationContent = fileContent; + foundFullImplementation = true; + break; + } else { + console.log(`❌ Коммит ${commit.substring(0, 8)} не содержит полной реализации`); + } + } catch (error) { + console.error(`Ошибка при проверке коммита ${commit}:`, error.message); + } + } + + if (!foundFullImplementation) { + console.error('❌ Не удалось найти полную реализацию в истории коммитов'); + process.exit(1); + } + + // Теперь получаем текущую версию файла с поддержкой уведомлений + console.log('Получаем текущую версию с поддержкой уведомлений...'); + const currentFilePath = path.join(__dirname, '..', 'src', 'handlers', 'callbackHandlers.ts'); + const currentContent = fs.readFileSync(currentFilePath, 'utf-8'); + + // Сначала создаем бэкап текущего файла + const backupPath = currentFilePath + '.backup-' + Date.now(); + fs.writeFileSync(backupPath, currentContent); + console.log(`✅ Создан бэкап текущей версии: ${path.basename(backupPath)}`); + + // Извлекаем код для поддержки уведомлений из текущей версии + console.log('Извлекаем код для поддержки уведомлений...'); + + // Находим импорт NotificationHandlers + const notificationImportRegex = /import\s+{\s*NotificationHandlers\s*}\s*from\s*['"]\.\/notificationHandlers['"]\s*;/; + const notificationImport = currentContent.match(notificationImportRegex)?.[0] || ''; + + // Находим объявление поля notificationHandlers + const notificationFieldRegex = /private\s+notificationHandlers\?\s*:\s*NotificationHandlers\s*;/; + const notificationField = currentContent.match(notificationFieldRegex)?.[0] || ''; + + // Находим инициализацию notificationHandlers в конструкторе + const notificationInitRegex = /\/\/\s*Создаем экземпляр NotificationHandlers[\s\S]*?try\s*{[\s\S]*?this\.notificationHandlers\s*=\s*new\s*NotificationHandlers[\s\S]*?}\s*catch[\s\S]*?}/; + const notificationInit = currentContent.match(notificationInitRegex)?.[0] || ''; + + // Находим метод handleNotificationSettings + const notificationSettingsMethodRegex = /async\s+handleNotificationSettings[\s\S]*?}\s*}/; + const notificationSettingsMethod = currentContent.match(notificationSettingsMethodRegex)?.[0] || ''; + + // Находим обработку callback для notifications в handleCallback + const notificationCallbackRegex = /\/\/\s*Настройки уведомлений[\s\S]*?else\s+if\s*\(data\s*===\s*['"]notifications['"][\s\S]*?}\s*}/; + const notificationCallback = currentContent.match(notificationCallbackRegex)?.[0] || ''; + + // Получаем часть обработки коллбэков для уведомлений + const notificationToggleRegex = /\/\/\s*Обработка переключения настроек уведомлений[\s\S]*?else\s+if[\s\S]*?notif_[\s\S]*?}\s*}/; + const notificationToggle = currentContent.match(notificationToggleRegex)?.[0] || ''; + + console.log(`✅ Извлечены блоки кода для уведомлений`); + + // Интегрируем код уведомлений в оригинальную версию + console.log('Интегрируем код уведомлений в оригинальную версию...'); + + // 1. Добавляем импорт + let newContent = fullImplementationContent; + if (notificationImport) { + newContent = newContent.replace(/import\s*{[^}]*}\s*from\s*['"]\.\/messageHandlers['"]\s*;/, + match => match + '\n' + notificationImport); + } + + // 2. Добавляем объявление поля + if (notificationField) { + newContent = newContent.replace(/private\s+translationController\s*:\s*TranslationController\s*;/, + match => match + '\n ' + notificationField); + } + + // 3. Добавляем инициализацию в конструкторе + if (notificationInit) { + newContent = newContent.replace(/this\.translationController\s*=\s*new\s*TranslationController\(\);/, + match => match + '\n ' + notificationInit); + } + + // 4. Добавляем обработку коллбэков для уведомлений + if (notificationCallback) { + newContent = newContent.replace(/else\s+{\s*await\s+this\.bot\.answerCallbackQuery\(query\.id[\s\S]*?return;/, + match => notificationCallback + '\n ' + match); + } + + // 5. Добавляем обработку переключения настроек уведомлений + if (notificationToggle) { + newContent = newContent.replace(/else\s+{\s*await\s+this\.bot\.answerCallbackQuery\(query\.id[\s\S]*?return;/, + match => notificationToggle + '\n ' + match); + } + + // 6. Добавляем метод handleNotificationSettings в конец класса + if (notificationSettingsMethod) { + newContent = newContent.replace(/}(\s*)$/, notificationSettingsMethod + '\n}$1'); + } + + // Сохраняем обновленный файл + const outputPath = currentFilePath + '.fixed'; + fs.writeFileSync(outputPath, newContent); + console.log(`✅ Создана исправленная версия файла: ${path.basename(outputPath)}`); + + console.log('\nИнструкция по восстановлению:'); + console.log(`1. Проверьте файл ${path.basename(outputPath)}`); + console.log('2. Если все выглядит правильно, выполните команду:'); + console.log(` Move-Item -Force "${path.basename(outputPath)}" "${path.basename(currentFilePath)}"`); + console.log('3. Перезапустите бота'); + +} catch (error) { + console.error('Произошла ошибка:', error); +} diff --git a/scripts/fixDatabaseStructure.js b/scripts/fixDatabaseStructure.js new file mode 100644 index 0000000..9326b08 --- /dev/null +++ b/scripts/fixDatabaseStructure.js @@ -0,0 +1,170 @@ +// Скрипт для исправления проблемы с ботом +require('dotenv').config(); +const { Pool } = require('pg'); + +// Получаем данные подключения из .env +console.log('Параметры подключения к БД:'); +console.log('DB_USERNAME:', process.env.DB_USERNAME); +console.log('DB_HOST:', process.env.DB_HOST); +console.log('DB_NAME:', process.env.DB_NAME); +console.log('DB_PASSWORD:', process.env.DB_PASSWORD ? '[указан]' : '[не указан]'); +console.log('DB_PORT:', process.env.DB_PORT); + +// Создаем пул соединений +const pool = new Pool({ + user: process.env.DB_USERNAME, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432') +}); + +async function fixDatabase() { + try { + console.log('Начинаем исправление базы данных...'); + + // Проверяем существование таблицы users + const tableResult = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'users' + ); + `); + + if (!tableResult.rows[0].exists) { + console.error('Таблица users не найдена!'); + return; + } + + console.log('✅ Таблица users существует'); + + // Проверяем и добавляем столбцы state и state_data, если они отсутствуют + console.log('Проверяем наличие столбцов state и state_data...'); + + const stateColumnResult = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'users' + AND column_name = 'state' + ); + `); + + const stateDataColumnResult = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'users' + AND column_name = 'state_data' + ); + `); + + if (!stateColumnResult.rows[0].exists) { + console.log('🔄 Добавляем столбец state...'); + await pool.query(`ALTER TABLE users ADD COLUMN state VARCHAR(255) NULL;`); + console.log('✅ Столбец state успешно добавлен'); + } else { + console.log('✅ Столбец state уже существует'); + } + + if (!stateDataColumnResult.rows[0].exists) { + console.log('🔄 Добавляем столбец state_data...'); + await pool.query(`ALTER TABLE users ADD COLUMN state_data JSONB DEFAULT '{}'::jsonb;`); + console.log('✅ Столбец state_data успешно добавлен'); + } else { + console.log('✅ Столбец state_data уже существует'); + } + + // Проверка наличия таблиц для уведомлений + console.log('Проверяем наличие таблиц для уведомлений...'); + + const tablesCheck = await Promise.all([ + checkTableExists('notifications'), + checkTableExists('notification_settings'), + checkTableExists('scheduled_notifications') + ]); + + // Создаем отсутствующие таблицы + if (!tablesCheck[0]) { + console.log('🔄 Создаем таблицу notifications...'); + await pool.query(` + CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + content JSONB NOT NULL DEFAULT '{}', + is_read BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + CREATE INDEX idx_notifications_user_id ON notifications(user_id); + CREATE INDEX idx_notifications_type ON notifications(type); + CREATE INDEX idx_notifications_created_at ON notifications(created_at); + `); + console.log('✅ Таблица notifications успешно создана'); + } + + if (!tablesCheck[1]) { + console.log('🔄 Создаем таблицу notification_settings...'); + await pool.query(` + CREATE TABLE notification_settings ( + user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, + new_matches BOOLEAN DEFAULT true, + new_messages BOOLEAN DEFAULT true, + new_likes BOOLEAN DEFAULT true, + reminders BOOLEAN DEFAULT true, + daily_summary BOOLEAN DEFAULT false, + time_preference VARCHAR(20) DEFAULT 'evening', + do_not_disturb BOOLEAN DEFAULT false, + do_not_disturb_start TIME, + do_not_disturb_end TIME, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + `); + console.log('✅ Таблица notification_settings успешно создана'); + } + + if (!tablesCheck[2]) { + console.log('🔄 Создаем таблицу scheduled_notifications...'); + await pool.query(` + CREATE TABLE scheduled_notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + content JSONB NOT NULL DEFAULT '{}', + scheduled_at TIMESTAMP WITH TIME ZONE NOT NULL, + processed BOOLEAN DEFAULT false, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() + ); + CREATE INDEX idx_scheduled_notifications_user_id ON scheduled_notifications(user_id); + CREATE INDEX idx_scheduled_notifications_scheduled_at ON scheduled_notifications(scheduled_at); + CREATE INDEX idx_scheduled_notifications_processed ON scheduled_notifications(processed); + `); + console.log('✅ Таблица scheduled_notifications успешно создана'); + } + + console.log('✅ Исправление базы данных завершено успешно'); + + } catch (error) { + console.error('Ошибка при исправлении базы данных:', error); + } finally { + await pool.end(); + } +} + +async function checkTableExists(tableName) { + const result = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = $1 + ); + `, [tableName]); + + const exists = result.rows[0].exists; + console.log(`${exists ? '✅' : '❌'} Таблица ${tableName} ${exists ? 'существует' : 'отсутствует'}`); + + return exists; +} + +fixDatabase(); diff --git a/scripts/fix_all_notifications.js b/scripts/fix_all_notifications.js new file mode 100644 index 0000000..57d1a2e --- /dev/null +++ b/scripts/fix_all_notifications.js @@ -0,0 +1,48 @@ +/** + * Комплексный скрипт для исправления всех проблем с уведомлениями + * Запускает последовательно оба скрипта исправления + */ + +const { exec } = require('child_process'); +const path = require('path'); + +console.log('🔧 Запуск комплексного исправления проблем с уведомлениями...'); + +// Путь к скриптам +const fixNotificationCallbacksScript = path.join(__dirname, 'fix_notification_callbacks.js'); +const updateBotWithNotificationsScript = path.join(__dirname, 'update_bot_with_notifications.js'); + +// Запуск первого скрипта для исправления таблиц и колонок +console.log('\n📊 Шаг 1/2: Проверка и исправление таблиц базы данных...'); +exec(`node ${fixNotificationCallbacksScript}`, (error, stdout, stderr) => { + if (error) { + console.error(`❌ Ошибка при запуске скрипта исправления таблиц: ${error}`); + return; + } + + console.log(stdout); + + if (stderr) { + console.error(`❌ Ошибки при выполнении скрипта: ${stderr}`); + } + + // Запуск второго скрипта для обновления bot.ts + console.log('\n📝 Шаг 2/2: Обновление файла bot.ts для регистрации обработчиков уведомлений...'); + exec(`node ${updateBotWithNotificationsScript}`, (error2, stdout2, stderr2) => { + if (error2) { + console.error(`❌ Ошибка при запуске скрипта обновления bot.ts: ${error2}`); + return; + } + + console.log(stdout2); + + if (stderr2) { + console.error(`❌ Ошибки при выполнении скрипта: ${stderr2}`); + } + + console.log('\n✅ Все исправления успешно выполнены!'); + console.log('🔄 Пожалуйста, перезапустите бота для применения изменений:'); + console.log(' npm run start'); + console.log('\n💡 Уведомления должны теперь работать корректно!'); + }); +}); diff --git a/scripts/fix_notification_callbacks.js b/scripts/fix_notification_callbacks.js new file mode 100644 index 0000000..a501a58 --- /dev/null +++ b/scripts/fix_notification_callbacks.js @@ -0,0 +1,332 @@ +/** + * Скрипт для проверки и исправления проблем с обработчиками уведомлений в боте + */ + +const { Client } = require('pg'); +const fs = require('fs'); + +// Конфигурация базы данных +const dbConfig = { + host: 'localhost', + port: 5432, + database: 'telegram_tinder', + user: 'postgres', + password: 'postgres' +}; + +// Подключение к базе данных +const client = new Client(dbConfig); + +async function main() { + try { + console.log('Подключение к базе данных...'); + await client.connect(); + console.log('Успешно подключено к базе данных'); + + // Шаг 1: Проверка существования необходимых таблиц для уведомлений + console.log('\n=== Проверка таблиц для уведомлений ==='); + + // Проверяем таблицу notifications + let notificationsTableExists = await checkTableExists('notifications'); + if (!notificationsTableExists) { + console.log('Таблица notifications не найдена. Создаем...'); + await createNotificationsTable(); + console.log('Таблица notifications успешно создана'); + } else { + console.log('Таблица notifications уже существует'); + } + + // Проверяем таблицу scheduled_notifications + let scheduledNotificationsTableExists = await checkTableExists('scheduled_notifications'); + if (!scheduledNotificationsTableExists) { + console.log('Таблица scheduled_notifications не найдена. Создаем...'); + await createScheduledNotificationsTable(); + console.log('Таблица scheduled_notifications успешно создана'); + } else { + console.log('Таблица scheduled_notifications уже существует'); + } + + // Проверяем таблицу notification_templates + let notificationTemplatesTableExists = await checkTableExists('notification_templates'); + if (!notificationTemplatesTableExists) { + console.log('Таблица notification_templates не найдена. Создаем...'); + await createNotificationTemplatesTable(); + console.log('Таблица notification_templates успешно создана'); + console.log('Заполняем таблицу базовыми шаблонами...'); + await populateDefaultTemplates(); + console.log('Шаблоны успешно добавлены'); + } else { + console.log('Таблица notification_templates уже существует'); + } + + // Шаг 2: Проверка существования столбца notification_settings в таблице users + console.log('\n=== Проверка столбца notification_settings в таблице users ==='); + + const notificationSettingsColumnExists = await checkColumnExists('users', 'notification_settings'); + if (!notificationSettingsColumnExists) { + console.log('Столбец notification_settings не найден. Добавляем...'); + await addNotificationSettingsColumn(); + console.log('Столбец notification_settings успешно добавлен'); + } else { + console.log('Столбец notification_settings уже существует'); + } + + // Шаг 3: Проверка существования столбцов state и state_data в таблице users + console.log('\n=== Проверка столбцов state и state_data в таблице users ==='); + + const stateColumnExists = await checkColumnExists('users', 'state'); + if (!stateColumnExists) { + console.log('Столбец state не найден. Добавляем...'); + await addStateColumn(); + console.log('Столбец state успешно добавлен'); + } else { + console.log('Столбец state уже существует'); + } + + const stateDataColumnExists = await checkColumnExists('users', 'state_data'); + if (!stateDataColumnExists) { + console.log('Столбец state_data не найден. Добавляем...'); + await addStateDataColumn(); + console.log('Столбец state_data успешно добавлен'); + } else { + console.log('Столбец state_data уже существует'); + } + + console.log('\nВсе таблицы и столбцы успешно проверены и созданы при необходимости.'); + console.log('Механизм уведомлений должен работать корректно.'); + + console.log('\n=== Проверка регистрации обработчиков уведомлений ==='); + console.log('Подсказка: убедитесь, что в файле bot.ts создается экземпляр NotificationHandlers и регистрируются его обработчики:'); + console.log(` + // Настройка обработчиков уведомлений + const notificationHandlers = new NotificationHandlers(bot); + notificationHandlers.register(); + + // Запуск обработчика запланированных уведомлений + setInterval(() => { + const notificationService = new NotificationService(bot); + notificationService.processScheduledNotifications(); + }, 60000); // Проверяем каждую минуту + `); + + } catch (error) { + console.error('Ошибка выполнения скрипта:', error); + } finally { + await client.end(); + console.log('\nСоединение с базой данных закрыто.'); + } +} + +async function checkTableExists(tableName) { + const query = ` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = $1 + ) as exists + `; + + const result = await client.query(query, [tableName]); + return result.rows[0].exists; +} + +async function checkColumnExists(tableName, columnName) { + const query = ` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = $1 AND column_name = $2 + ) as exists + `; + + const result = await client.query(query, [tableName, columnName]); + return result.rows[0].exists; +} + +async function createNotificationsTable() { + await client.query(` + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + + CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); +} + +async function createScheduledNotificationsTable() { + await client.query(` + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + + CREATE TABLE scheduled_notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + scheduled_at TIMESTAMP NOT NULL, + processed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); +} + +async function createNotificationTemplatesTable() { + await client.query(` + CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + + CREATE TABLE notification_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + type VARCHAR(50) NOT NULL UNIQUE, + title TEXT NOT NULL, + message_template TEXT NOT NULL, + button_template JSONB, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + `); +} + +async function addNotificationSettingsColumn() { + await client.query(` + ALTER TABLE users + ADD COLUMN notification_settings JSONB DEFAULT '{ + "newMatches": true, + "newMessages": true, + "newLikes": true, + "reminders": true, + "dailySummary": true, + "timePreference": "evening", + "doNotDisturb": false + }'::jsonb + `); +} + +async function addStateColumn() { + await client.query(` + ALTER TABLE users ADD COLUMN state VARCHAR(255) NULL + `); +} + +async function addStateDataColumn() { + await client.query(` + ALTER TABLE users ADD COLUMN state_data JSONB DEFAULT '{}'::jsonb + `); +} + +async function populateDefaultTemplates() { + const templates = [ + { + type: 'new_like', + title: 'Новый лайк!', + message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'super_like', + title: 'Супер-лайк!', + message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'new_match', + title: 'Новый матч!', + message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'new_message', + title: 'Новое сообщение!', + message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + button_template: { + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'match_reminder', + title: 'Напоминание о матче', + message_template: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }] + ] + } + }, + { + type: 'inactive_matches', + title: 'Неактивные матчи', + message_template: '⏰ У вас {{count}} неактивных матчей!\n\nПродолжите общение, чтобы не упустить интересные знакомства!', + button_template: { + inline_keyboard: [ + [{ text: '📋 Открыть матчи', callback_data: 'view_matches' }], + [{ text: '💕 Смотреть новые анкеты', callback_data: 'start_browsing' }] + ] + } + }, + { + type: 'like_summary', + title: 'Сводка лайков', + message_template: '💖 У вас {{count}} новых лайков!\n\nПосмотрите, кто проявил к вам интерес сегодня!', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть лайки', callback_data: 'view_likes' }], + [{ text: '💕 Начать знакомиться', callback_data: 'start_browsing' }] + ] + } + } + ]; + + for (const template of templates) { + await client.query(` + INSERT INTO notification_templates (type, title, message_template, button_template) + VALUES ($1, $2, $3, $4) + ON CONFLICT (type) DO UPDATE + SET title = EXCLUDED.title, + message_template = EXCLUDED.message_template, + button_template = EXCLUDED.button_template, + updated_at = NOW() + `, [ + template.type, + template.title, + template.message_template, + JSON.stringify(template.button_template) + ]); + } +} + +// Запуск скрипта +main(); diff --git a/scripts/setPremiumStatus.js b/scripts/setPremiumStatus.js new file mode 100644 index 0000000..420ef4e --- /dev/null +++ b/scripts/setPremiumStatus.js @@ -0,0 +1,73 @@ +// Скрипт для установки премиум-статуса всем пользователям +require('dotenv').config(); +const { Pool } = require('pg'); + +// Проверяем и выводим параметры подключения +console.log('Параметры подключения к БД:'); +console.log('DB_USERNAME:', process.env.DB_USERNAME); +console.log('DB_HOST:', process.env.DB_HOST); +console.log('DB_NAME:', process.env.DB_NAME); +console.log('DB_PASSWORD:', process.env.DB_PASSWORD ? '[указан]' : '[не указан]'); +console.log('DB_PORT:', process.env.DB_PORT); + +// Создаем пул соединений +const pool = new Pool({ + user: process.env.DB_USERNAME, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432') +}); + +async function setAllUsersToPremium() { + try { + console.log('Устанавливаем премиум-статус для всех пользователей...'); + + // Проверка соединения с БД + console.log('Проверка соединения с БД...'); + const testResult = await pool.query('SELECT NOW()'); + console.log('✅ Соединение успешно:', testResult.rows[0].now); + + // Проверка наличия столбца premium + console.log('Проверяем наличие столбца premium в таблице users...'); + + const checkResult = await pool.query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'users' + AND column_name = 'premium' + ); + `); + + if (!checkResult.rows[0].exists) { + console.log('🔄 Добавляем столбец premium...'); + await pool.query(`ALTER TABLE users ADD COLUMN premium BOOLEAN DEFAULT false;`); + console.log('✅ Столбец premium успешно добавлен'); + } else { + console.log('✅ Столбец premium уже существует'); + } + + // Устанавливаем premium=true для всех пользователей + const updateResult = await pool.query(` + UPDATE users + SET premium = true + WHERE true + RETURNING id, telegram_id, premium + `); + + console.log(`✅ Успешно установлен премиум-статус для ${updateResult.rows.length} пользователей:`); + updateResult.rows.forEach(row => { + console.log(`ID: ${row.id.substr(0, 8)}... | Telegram ID: ${row.telegram_id} | Premium: ${row.premium}`); + }); + + console.log('🎉 Все пользователи теперь имеют премиум-статус!'); + } catch (error) { + console.error('❌ Ошибка при установке премиум-статуса:', error); + } finally { + await pool.end(); + console.log('Соединение с базой данных закрыто'); + } +} + +setAllUsersToPremium(); diff --git a/scripts/testCallbacks.js b/scripts/testCallbacks.js new file mode 100644 index 0000000..dde2ed6 --- /dev/null +++ b/scripts/testCallbacks.js @@ -0,0 +1,85 @@ +// Скрипт для проверки работы callback-хэндлеров и уведомлений +require('dotenv').config(); +const TelegramBot = require('node-telegram-bot-api'); +const { Pool } = require('pg'); + +// Создаем пул соединений +const pool = new Pool({ + user: process.env.DB_USERNAME, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432') +}); + +// Функция для имитации callback-запроса к боту +async function testCallback() { + try { + console.log('Начинаем тестирование callback-хэндлеров и уведомлений...'); + + // Используем последнего пользователя из базы данных + const userResult = await pool.query(` + SELECT * FROM users ORDER BY last_active_at DESC NULLS LAST LIMIT 1 + `); + + if (userResult.rows.length === 0) { + console.error('❌ Пользователи не найдены в базе данных'); + return; + } + + const user = userResult.rows[0]; + console.log(`Выбран тестовый пользователь: ${user.first_name || 'Без имени'} (ID: ${user.telegram_id})`); + + // Получаем токен бота из переменных окружения + const token = process.env.TELEGRAM_BOT_TOKEN; + if (!token) { + console.error('❌ Токен бота не найден в переменных окружения'); + return; + } + + // Создаем экземпляр бота + const bot = new TelegramBot(token); + + // Отправляем тестовое уведомление пользователю + console.log(`Отправляем тестовое уведомление пользователю ID: ${user.telegram_id}...`); + + try { + const result = await bot.sendMessage( + user.telegram_id, + `🔔 *Тестовое уведомление*\n\nЭто проверка работы уведомлений и callback-хэндлеров.\n\nВаш премиум-статус: ${user.premium ? '✅ Активен' : '❌ Не активен'}`, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + { text: '🔔 Уведомления', callback_data: 'notification_settings' }, + { text: '❤️ Профиль', callback_data: 'view_profile' } + ], + [ + { text: '⚙️ Настройки', callback_data: 'settings' } + ] + ] + } + } + ); + + console.log('✅ Тестовое сообщение успешно отправлено!'); + console.log('Информация о сообщении:', JSON.stringify(result, null, 2)); + } catch (error) { + console.error('❌ Ошибка при отправке тестового сообщения:', error.message); + if (error.response && error.response.body) { + console.error('Детали ошибки:', JSON.stringify(error.response.body, null, 2)); + } + } + + } catch (error) { + console.error('❌ Ошибка при тестировании:', error); + } finally { + await pool.end(); + console.log('Соединение с базой данных закрыто'); + console.log('Тестирование завершено!'); + } +} + +// Запускаем тестирование +testCallback(); diff --git a/scripts/testVipMethod.js b/scripts/testVipMethod.js new file mode 100644 index 0000000..565288a --- /dev/null +++ b/scripts/testVipMethod.js @@ -0,0 +1,81 @@ +// Скрипт для тестирования метода checkPremiumStatus +require('dotenv').config(); +const { Pool } = require('pg'); + +// Создаем пул соединений +const pool = new Pool({ + user: process.env.DB_USERNAME, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432') +}); + +async function testCheckPremiumMethod() { + try { + console.log('Тестирование метода checkPremiumStatus...'); + + // Получаем пользователя для тестирования + const userResult = await pool.query(` + SELECT id, telegram_id, first_name, username, premium + FROM users + ORDER BY last_active_at DESC NULLS LAST + LIMIT 1 + `); + + if (userResult.rows.length === 0) { + console.error('❌ Пользователи не найдены в базе данных'); + return; + } + + const user = userResult.rows[0]; + console.log(`Выбран тестовый пользователь: ${user.first_name || user.username || 'Без имени'} (Telegram ID: ${user.telegram_id})`); + console.log(`Текущий премиум-статус: ${user.premium ? '✅ Активен' : '❌ Не активен'}`); + + // Проверка работы метода checkPremiumStatus + console.log('\nЭмулируем вызов метода checkPremiumStatus из vipService:'); + const result = await pool.query(` + SELECT id, premium + FROM users + WHERE telegram_id = $1 + `, [user.telegram_id]); + + if (result.rows.length === 0) { + console.log('❌ Пользователь не найден'); + } else { + const isPremium = result.rows[0].premium || false; + console.log(`Результат метода: isPremium = ${isPremium ? '✅ true' : '❌ false'}`); + + if (!isPremium) { + console.log('\nПремиум-статус отсутствует. Устанавливаем премиум...'); + await pool.query(` + UPDATE users + SET premium = true + WHERE telegram_id = $1 + `, [user.telegram_id]); + + // Проверяем обновление + const updatedResult = await pool.query(` + SELECT premium + FROM users + WHERE telegram_id = $1 + `, [user.telegram_id]); + + const updatedPremium = updatedResult.rows[0].premium; + console.log(`Обновленный статус: isPremium = ${updatedPremium ? '✅ true' : '❌ false'}`); + } + } + + console.log('\n✅ Тестирование завершено'); + console.log('🔧 Теперь проверьте функциональность VIP поиска в боте'); + + } catch (error) { + console.error('❌ Ошибка при тестировании:', error); + } finally { + await pool.end(); + console.log('Соединение с базой данных закрыто'); + } +} + +// Запускаем тест +testCheckPremiumMethod(); diff --git a/scripts/testVipStatus.js b/scripts/testVipStatus.js new file mode 100644 index 0000000..16a7839 --- /dev/null +++ b/scripts/testVipStatus.js @@ -0,0 +1,75 @@ +// Скрипт для тестирования VIP функционала +require('dotenv').config(); +const { Pool } = require('pg'); + +// Создаем пул соединений +const pool = new Pool({ + user: process.env.DB_USERNAME, + host: process.env.DB_HOST, + database: process.env.DB_NAME, + password: process.env.DB_PASSWORD, + port: parseInt(process.env.DB_PORT || '5432') +}); + +async function testVipStatus() { + try { + console.log('Тестирование функционала VIP статуса...'); + + // Получаем список пользователей с информацией о premium статусе + const users = await pool.query(` + SELECT id, telegram_id, username, first_name, premium + FROM users + ORDER BY last_active_at DESC + LIMIT 5 + `); + + console.log('Список пользователей и их премиум статус:'); + users.rows.forEach(user => { + console.log(`ID: ${user.id.substr(0, 8)}... | Telegram ID: ${user.telegram_id} | Имя: ${user.first_name || user.username || 'Не указано'} | Premium: ${user.premium ? '✅' : '❌'}`); + }); + + // Если premium у всех false, устанавливаем premium = true + const nonPremiumUsers = users.rows.filter(user => !user.premium); + if (nonPremiumUsers.length > 0) { + console.log('\nОбнаружены пользователи без премиум статуса. Устанавливаем премиум...'); + + for (const user of nonPremiumUsers) { + await pool.query(` + UPDATE users + SET premium = true + WHERE id = $1 + RETURNING id, telegram_id, premium + `, [user.id]); + + console.log(`✅ Установлен премиум для пользователя ${user.first_name || user.username || user.telegram_id}`); + } + } else { + console.log('\nВсе пользователи уже имеют премиум-статус!'); + } + + // Проверяем результат + const updatedUsers = await pool.query(` + SELECT id, telegram_id, username, first_name, premium + FROM users + ORDER BY last_active_at DESC + LIMIT 5 + `); + + console.log('\nОбновленный список пользователей и их премиум статус:'); + updatedUsers.rows.forEach(user => { + console.log(`ID: ${user.id.substr(0, 8)}... | Telegram ID: ${user.telegram_id} | Имя: ${user.first_name || user.username || 'Не указано'} | Premium: ${user.premium ? '✅' : '❌'}`); + }); + + console.log('\n✅ Тестирование VIP функционала завершено'); + console.log('🔧 Проверьте доступность VIP поиска в боте через меню или команды'); + + } catch (error) { + console.error('❌ Ошибка при тестировании VIP статуса:', error); + } finally { + await pool.end(); + console.log('Соединение с базой данных закрыто'); + } +} + +// Запускаем тест +testVipStatus(); diff --git a/scripts/update_bot_with_notifications.js b/scripts/update_bot_with_notifications.js new file mode 100644 index 0000000..a4084de --- /dev/null +++ b/scripts/update_bot_with_notifications.js @@ -0,0 +1,104 @@ +/** + * Скрипт для проверки и исправления регистрации NotificationHandlers в bot.ts + */ + +const fs = require('fs'); +const path = require('path'); + +const botFilePath = path.join(__dirname, '../src/bot.ts'); + +// Проверка существования файла bot.ts +if (!fs.existsSync(botFilePath)) { + console.error(`❌ Файл ${botFilePath} не найден`); + process.exit(1); +} + +// Чтение содержимого файла bot.ts +let botContent = fs.readFileSync(botFilePath, 'utf8'); + +// Проверка импорта NotificationHandlers +if (!botContent.includes('import { NotificationHandlers }')) { + console.log('Добавляем импорт NotificationHandlers в bot.ts...'); + + // Находим последний импорт + const importRegex = /^import.*?;/gms; + const matches = [...botContent.matchAll(importRegex)]; + + if (matches.length > 0) { + const lastImport = matches[matches.length - 1][0]; + const lastImportIndex = botContent.lastIndexOf(lastImport) + lastImport.length; + + // Добавляем импорт NotificationHandlers + botContent = + botContent.slice(0, lastImportIndex) + + '\nimport { NotificationHandlers } from \'./handlers/notificationHandlers\';\n' + + botContent.slice(lastImportIndex); + + console.log('✅ Импорт NotificationHandlers добавлен'); + } else { + console.error('❌ Не удалось найти место для добавления импорта'); + } +} + +// Проверка объявления NotificationHandlers в классе +if (!botContent.includes('private notificationHandlers')) { + console.log('Добавляем объявление notificationHandlers в класс...'); + + const classPropertiesRegex = /class TelegramTinderBot {([^}]+?)constructor/s; + const classPropertiesMatch = botContent.match(classPropertiesRegex); + + if (classPropertiesMatch) { + const classProperties = classPropertiesMatch[1]; + const updatedProperties = classProperties + ' private notificationHandlers: NotificationHandlers;\n '; + + botContent = botContent.replace(classPropertiesRegex, `class TelegramTinderBot {${updatedProperties}constructor`); + + console.log('✅ Объявление notificationHandlers добавлено'); + } else { + console.error('❌ Не удалось найти место для добавления объявления notificationHandlers'); + } +} + +// Проверка создания экземпляра NotificationHandlers в конструкторе +if (!botContent.includes('this.notificationHandlers = new NotificationHandlers')) { + console.log('Добавляем инициализацию notificationHandlers в конструктор...'); + + const initializationRegex = /(this\.callbackHandlers = new CallbackHandlers[^;]+;)/; + const initializationMatch = botContent.match(initializationRegex); + + if (initializationMatch) { + const callbackHandlersInit = initializationMatch[1]; + const updatedInit = callbackHandlersInit + '\n this.notificationHandlers = new NotificationHandlers(this.bot);'; + + botContent = botContent.replace(initializationRegex, updatedInit); + + console.log('✅ Инициализация notificationHandlers добавлена'); + } else { + console.error('❌ Не удалось найти место для добавления инициализации notificationHandlers'); + } +} + +// Проверка регистрации notificationHandlers в методе registerHandlers +if (!botContent.includes('this.notificationHandlers.register()')) { + console.log('Добавляем регистрацию notificationHandlers...'); + + const registerHandlersRegex = /(private registerHandlers\(\): void {[^}]+?)}/s; + const registerHandlersMatch = botContent.match(registerHandlersRegex); + + if (registerHandlersMatch) { + const registerHandlersBody = registerHandlersMatch[1]; + const updatedBody = registerHandlersBody + '\n // Обработчики уведомлений\n this.notificationHandlers.register();\n }'; + + botContent = botContent.replace(registerHandlersRegex, updatedBody); + + console.log('✅ Регистрация notificationHandlers добавлена'); + } else { + console.error('❌ Не удалось найти место для добавления регистрации notificationHandlers'); + } +} + +// Запись обновленного содержимого в файл +fs.writeFileSync(botFilePath, botContent, 'utf8'); + +console.log('✅ Файл bot.ts успешно обновлен'); +console.log('🔔 Перезапустите бота для применения изменений'); diff --git a/src/bot.ts b/src/bot.ts index 68711be..efea339 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -8,6 +8,8 @@ import LocalizationService from './services/localizationService'; import { CommandHandlers } from './handlers/commandHandlers'; import { CallbackHandlers } from './handlers/callbackHandlers'; import { MessageHandlers } from './handlers/messageHandlers'; +import { NotificationHandlers } from './handlers/notificationHandlers'; + class TelegramTinderBot { private bot: TelegramBot; @@ -18,7 +20,7 @@ class TelegramTinderBot { private commandHandlers: CommandHandlers; private callbackHandlers: CallbackHandlers; private messageHandlers: MessageHandlers; - + private notificationHandlers: NotificationHandlers; constructor() { const token = process.env.TELEGRAM_BOT_TOKEN; if (!token) { @@ -34,6 +36,7 @@ class TelegramTinderBot { this.commandHandlers = new CommandHandlers(this.bot); this.messageHandlers = new MessageHandlers(this.bot); this.callbackHandlers = new CallbackHandlers(this.bot, this.messageHandlers); + this.notificationHandlers = new NotificationHandlers(this.bot); this.setupErrorHandling(); this.setupPeriodicTasks(); @@ -78,6 +81,7 @@ class TelegramTinderBot { { command: 'browse', description: '💕 Смотреть анкеты' }, { command: 'matches', description: '💖 Мои матчи' }, { command: 'settings', description: '⚙️ Настройки' }, + { command: 'notifications', description: '🔔 Настройки уведомлений' }, { command: 'help', description: '❓ Помощь' } ]; @@ -94,6 +98,9 @@ class TelegramTinderBot { // Сообщения this.messageHandlers.register(); + + // Обработчики уведомлений + this.notificationHandlers.register(); } // Обработка ошибок @@ -137,14 +144,31 @@ class TelegramTinderBot { } }, 5 * 60 * 1000); - // Очистка старых данных каждый день + // Планирование периодических уведомлений раз в день в 00:05 setInterval(async () => { try { - await this.cleanupOldData(); + const now = new Date(); + if (now.getHours() === 0 && now.getMinutes() >= 5 && now.getMinutes() < 10) { + console.log('🔔 Scheduling periodic notifications...'); + await this.notificationService.schedulePeriodicNotifications(); + } + } catch (error) { + console.error('Error scheduling periodic notifications:', error); + } + }, 5 * 60 * 1000); + + // Очистка старых данных каждый день в 03:00 + setInterval(async () => { + try { + const now = new Date(); + if (now.getHours() === 3 && now.getMinutes() < 5) { + console.log('🧹 Running scheduled cleanup...'); + await this.cleanupOldData(); + } } catch (error) { console.error('Error cleaning up old data:', error); } - }, 24 * 60 * 60 * 1000); + }, 5 * 60 * 1000); } // Очистка старых данных @@ -152,11 +176,18 @@ class TelegramTinderBot { console.log('🧹 Running cleanup tasks...'); try { - // Очистка старых уведомлений (старше 30 дней) - const notificationsResult = await query(` + // Очистка старых запланированных уведомлений (старше 30 дней или обработанных) + const scheduledNotificationsResult = await query(` DELETE FROM scheduled_notifications - WHERE processed = true - AND created_at < CURRENT_TIMESTAMP - INTERVAL '30 days' + WHERE (processed = true AND created_at < CURRENT_TIMESTAMP - INTERVAL '30 days') + OR (scheduled_at < CURRENT_TIMESTAMP - INTERVAL '7 days') + `); + console.log(`🗑️ Cleaned up ${scheduledNotificationsResult.rowCount} old scheduled notifications`); + + // Очистка старых уведомлений (старше 90 дней) + const notificationsResult = await query(` + DELETE FROM notifications + WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '90 days' `); console.log(`🗑️ Cleaned up ${notificationsResult.rowCount} old notifications`); @@ -186,7 +217,7 @@ class TelegramTinderBot { console.log(`💬 Cleaned up ${messagesResult.rowCount} old messages`); // Обновление статистики таблиц после очистки - await query('VACUUM ANALYZE scheduled_notifications, profile_views, swipes, messages'); + await query('VACUUM ANALYZE notifications, scheduled_notifications, profile_views, swipes, messages'); console.log('✅ Cleanup completed successfully'); } catch (error) { @@ -229,4 +260,4 @@ if (require.main === module) { }); } -export { TelegramTinderBot }; \ No newline at end of file +export { TelegramTinderBot }; diff --git a/src/handlers/callbackHandlers.ts b/src/handlers/callbackHandlers.ts index 0df3dd6..09c37b8 100644 --- a/src/handlers/callbackHandlers.ts +++ b/src/handlers/callbackHandlers.ts @@ -4,6 +4,7 @@ import { MatchingService } from '../services/matchingService'; import { ChatService } from '../services/chatService'; import { Profile } from '../models/Profile'; import { MessageHandlers } from './messageHandlers'; +import { NotificationHandlers } from './notificationHandlers'; import { ProfileEditController } from '../controllers/profileEditController'; import { EnhancedChatHandlers } from './enhancedChatHandlers'; import { VipController } from '../controllers/vipController'; @@ -23,6 +24,7 @@ export class CallbackHandlers { private vipController: VipController; private vipService: VipService; private translationController: TranslationController; + private notificationHandlers?: NotificationHandlers; private likeBackHandler: LikeBackHandler; constructor(bot: TelegramBot, messageHandlers: MessageHandlers) { @@ -36,6 +38,12 @@ export class CallbackHandlers { this.vipController = new VipController(bot); this.vipService = new VipService(); this.translationController = new TranslationController(); + // Создаем экземпляр NotificationHandlers + try { + this.notificationHandlers = new NotificationHandlers(bot); + } catch (error) { + console.error('Failed to initialize NotificationHandlers:', error); + } this.likeBackHandler = new LikeBackHandler(bot); } @@ -272,6 +280,41 @@ export class CallbackHandlers { await this.handleSettings(chatId, telegramId); } + // Настройки уведомлений + else if (data === 'notifications') { + if (this.notificationHandlers) { + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.answerCallbackQuery(query.id, { text: '❌ Вы не зарегистрированы.' }); + return; + } + + const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId); + await this.notificationHandlers.sendNotificationSettings(chatId, settings); + } else { + await this.handleNotificationSettings(chatId, telegramId); + } + } + // Обработка переключения настроек уведомлений + else if (data.startsWith('notif_toggle:') || + data === 'notif_time' || + data.startsWith('notif_time_set:') || + data === 'notif_dnd' || + data.startsWith('notif_dnd_set:') || + data === 'notif_dnd_time' || + data.startsWith('notif_dnd_time_set:') || + data === 'notif_dnd_time_custom') { + // Делегируем обработку в NotificationHandlers, если он доступен + if (this.notificationHandlers) { + // Эти коллбэки уже обрабатываются в NotificationHandlers, поэтому здесь ничего не делаем + // NotificationHandlers уже зарегистрировал свои обработчики в register() + } else { + await this.bot.answerCallbackQuery(query.id, { + text: 'Функция настройки уведомлений недоступна.', + show_alert: true + }); + } + } else { await this.bot.answerCallbackQuery(query.id, { text: 'Функция в разработке!', @@ -870,13 +913,7 @@ export class CallbackHandlers { ); } - // Настройки уведомлений - async handleNotificationSettings(chatId: number, telegramId: string): Promise { - await this.bot.sendMessage( - chatId, - '🔔 Настройки уведомлений будут доступны в следующем обновлении!' - ); - } + // Настройки уведомлений - реализация перенесена в расширенную версию // Как это работает async handleHowItWorks(chatId: number): Promise { @@ -1578,7 +1615,7 @@ export class CallbackHandlers { try { // Проверяем VIP статус пользователя const user = await this.profileService.getUserByTelegramId(telegramId); - if (!user || !user.isPremium) { + if (!user || !user.premium) { // Изменено с isPremium на premium, чтобы соответствовать названию колонки в базе данных const keyboard = { inline_keyboard: [ [ @@ -2240,4 +2277,27 @@ export class CallbackHandlers { await this.bot.sendMessage(chatId, t('translation.error')); } } + + async handleNotificationSettings(chatId: number, telegramId: string): Promise { + try { + if (this.notificationHandlers) { + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.sendMessage(chatId, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.'); + return; + } + + // Вызываем метод из notificationHandlers для получения настроек и отображения меню + const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId); + await this.notificationHandlers.sendNotificationSettings(chatId, settings); + } else { + // Если NotificationHandlers недоступен, показываем сообщение об ошибке + await this.bot.sendMessage(chatId, '⚙️ Настройки уведомлений временно недоступны. Попробуйте позже.'); + await this.handleSettings(chatId, telegramId); + } + } catch (error) { + console.error('Notification settings error:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке настроек уведомлений. Попробуйте позже.'); + } + } } diff --git a/src/handlers/callbackHandlers.ts.backup-1758166633763 b/src/handlers/callbackHandlers.ts.backup-1758166633763 new file mode 100644 index 0000000..4d537fa --- /dev/null +++ b/src/handlers/callbackHandlers.ts.backup-1758166633763 @@ -0,0 +1,606 @@ +import TelegramBot, { CallbackQuery, InlineKeyboardMarkup } from 'node-telegram-bot-api'; +import { ProfileService } from '../services/profileService'; +import { MatchingService } from '../services/matchingService'; +import { ChatService } from '../services/chatService'; +import { Profile } from '../models/Profile'; +import { MessageHandlers } from './messageHandlers'; +import { ProfileEditController } from '../controllers/profileEditController'; +import { EnhancedChatHandlers } from './enhancedChatHandlers'; +import { VipController } from '../controllers/vipController'; +import { VipService } from '../services/vipService'; +import { TranslationController } from '../controllers/translationController'; +import { t } from '../services/localizationService'; +import { LikeBackHandler } from './likeBackHandler'; +import { NotificationHandlers } from './notificationHandlers'; + +export class CallbackHandlers { + private bot: TelegramBot; + private profileService: ProfileService; + private matchingService: MatchingService; + private chatService: ChatService; + private messageHandlers: MessageHandlers; + private profileEditController: ProfileEditController; + private enhancedChatHandlers: EnhancedChatHandlers; + private vipController: VipController; + private vipService: VipService; + private translationController: TranslationController; + private likeBackHandler: LikeBackHandler; + private notificationHandlers?: NotificationHandlers; + + constructor(bot: TelegramBot, messageHandlers: MessageHandlers) { + this.bot = bot; + this.profileService = new ProfileService(); + this.matchingService = new MatchingService(); + this.chatService = new ChatService(); + this.messageHandlers = messageHandlers; + this.profileEditController = new ProfileEditController(this.profileService); + this.enhancedChatHandlers = new EnhancedChatHandlers(bot); + this.vipController = new VipController(bot); + this.vipService = new VipService(); + this.translationController = new TranslationController(); + this.likeBackHandler = new LikeBackHandler(bot); + + // Создаем экземпляр NotificationHandlers + try { + this.notificationHandlers = new NotificationHandlers(bot); + } catch (error) { + console.error('Failed to initialize NotificationHandlers:', error); + } + } + + register(): void { + this.bot.on('callback_query', (query) => this.handleCallback(query)); + } + + async handleCallback(query: CallbackQuery): Promise { + if (!query.data || !query.from || !query.message) return; + + const telegramId = query.from.id.toString(); + const chatId = query.message.chat.id; + const data = query.data; + + try { + // Основные действия профиля + if (data === 'create_profile') { + await this.handleCreateProfile(chatId, telegramId); + } else if (data.startsWith('gender_')) { + const gender = data.replace('gender_', ''); + await this.handleGenderSelection(chatId, telegramId, gender); + } else if (data === 'view_my_profile') { + await this.handleViewMyProfile(chatId, telegramId); + } else if (data === 'edit_profile') { + await this.handleEditProfile(chatId, telegramId); + } else if (data === 'manage_photos') { + await this.handleManagePhotos(chatId, telegramId); + } else if (data === 'preview_profile') { + await this.handlePreviewProfile(chatId, telegramId); + } + + // Редактирование полей профиля + else if (data === 'edit_name') { + await this.handleEditName(chatId, telegramId); + } else if (data === 'edit_age') { + await this.handleEditAge(chatId, telegramId); + } else if (data === 'edit_bio') { + await this.handleEditBio(chatId, telegramId); + } else if (data === 'edit_hobbies') { + await this.handleEditHobbies(chatId, telegramId); + } else if (data === 'edit_city') { + await this.handleEditCity(chatId, telegramId); + } else if (data === 'edit_job') { + await this.handleEditJob(chatId, telegramId); + } else if (data === 'edit_education') { + await this.handleEditEducation(chatId, telegramId); + } else if (data === 'edit_height') { + await this.handleEditHeight(chatId, telegramId); + } else if (data === 'edit_religion') { + await this.handleEditReligion(chatId, telegramId); + } else if (data === 'edit_dating_goal') { + await this.handleEditDatingGoal(chatId, telegramId); + } else if (data === 'edit_lifestyle') { + await this.handleEditLifestyle(chatId, telegramId); + } else if (data === 'edit_search_preferences') { + await this.handleEditSearchPreferences(chatId, telegramId); + } + + // Управление фотографиями + else if (data === 'add_photo') { + await this.handleAddPhoto(chatId, telegramId); + } else if (data === 'delete_photo') { + await this.handleDeletePhoto(chatId, telegramId); + } else if (data === 'set_main_photo') { + await this.handleSetMainPhoto(chatId, telegramId); + } else if (data.startsWith('delete_photo_')) { + const photoIndex = parseInt(data.replace('delete_photo_', '')); + await this.handleDeletePhotoByIndex(chatId, telegramId, photoIndex); + } else if (data.startsWith('set_main_photo_')) { + const photoIndex = parseInt(data.replace('set_main_photo_', '')); + await this.handleSetMainPhotoByIndex(chatId, telegramId, photoIndex); + } + + // Цели знакомства + else if (data.startsWith('set_dating_goal_')) { + const goal = data.replace('set_dating_goal_', ''); + await this.handleSetDatingGoal(chatId, telegramId, goal); + } + + // Образ жизни + else if (data === 'edit_smoking') { + await this.handleEditSmoking(chatId, telegramId); + } else if (data === 'edit_drinking') { + await this.handleEditDrinking(chatId, telegramId); + } else if (data === 'edit_kids') { + await this.handleEditKids(chatId, telegramId); + } else if (data.startsWith('set_smoking_')) { + const value = data.replace('set_smoking_', ''); + await this.handleSetLifestyle(chatId, telegramId, 'smoking', value); + } else if (data.startsWith('set_drinking_')) { + const value = data.replace('set_drinking_', ''); + await this.handleSetLifestyle(chatId, telegramId, 'drinking', value); + } else if (data.startsWith('set_kids_')) { + const value = data.replace('set_kids_', ''); + await this.handleSetLifestyle(chatId, telegramId, 'kids', value); + } + + // Настройки поиска + else if (data === 'edit_age_range') { + await this.handleEditAgeRange(chatId, telegramId); + } else if (data === 'edit_distance') { + await this.handleEditDistance(chatId, telegramId); + } + + // Просмотр анкет и свайпы + else if (data === 'start_browsing') { + await this.handleStartBrowsing(chatId, telegramId, false); + } else if (data === 'start_browsing_first') { + // Показываем всех пользователей для нового пользователя + await this.handleStartBrowsing(chatId, telegramId, true); + } else if (data === 'vip_search') { + await this.handleVipSearch(chatId, telegramId); + } else if (data.startsWith('search_by_goal_')) { + const goal = data.replace('search_by_goal_', ''); + await this.handleSearchByGoal(chatId, telegramId, goal); + } else if (data === 'next_candidate') { + await this.handleNextCandidate(chatId, telegramId); + } else if (data.startsWith('like_')) { + const targetUserId = data.replace('like_', ''); + await this.handleLike(chatId, telegramId, targetUserId); + } else if (data.startsWith('dislike_')) { + const targetUserId = data.replace('dislike_', ''); + await this.handleDislike(chatId, telegramId, targetUserId); + } else if (data.startsWith('superlike_')) { + const targetUserId = data.replace('superlike_', ''); + await this.handleSuperlike(chatId, telegramId, targetUserId); + } else if (data.startsWith('view_profile_')) { + const targetUserId = data.replace('view_profile_', ''); + await this.handleViewProfile(chatId, telegramId, targetUserId); + } else if (data.startsWith('more_photos_')) { + const targetUserId = data.replace('more_photos_', ''); + await this.handleMorePhotos(chatId, telegramId, targetUserId); + } + + // Обработка лайков и ответных лайков из уведомлений + else if (data.startsWith('like_back:')) { + const targetUserId = data.replace('like_back:', ''); + await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId); + } + + // Матчи и чаты + else if (data === 'view_matches') { + await this.handleViewMatches(chatId, telegramId); + } else if (data === 'open_chats') { + await this.handleOpenChats(chatId, telegramId); + } else if (data === 'native_chats') { + await this.enhancedChatHandlers.showChatsNative(chatId, telegramId); + } else if (data.startsWith('open_native_chat_')) { + const matchId = data.replace('open_native_chat_', ''); + await this.enhancedChatHandlers.openNativeChat(chatId, telegramId, matchId); + } else if (data.startsWith('chat_history_')) { + const matchId = data.replace('chat_history_', ''); + await this.enhancedChatHandlers.showChatHistory(chatId, telegramId, matchId); + } else if (data.startsWith('chat_')) { + const matchId = data.replace('chat_', ''); + await this.handleOpenChat(chatId, telegramId, matchId); + } else if (data.startsWith('send_message_')) { + const matchId = data.replace('send_message_', ''); + await this.handleSendMessage(chatId, telegramId, matchId); + } else if (data.startsWith('view_chat_profile_')) { + const matchId = data.replace('view_chat_profile_', ''); + await this.handleViewChatProfile(chatId, telegramId, matchId); + } else if (data.startsWith('unmatch_')) { + const matchId = data.replace('unmatch_', ''); + await this.handleUnmatch(chatId, telegramId, matchId); + } else if (data.startsWith('confirm_unmatch_')) { + const matchId = data.replace('confirm_unmatch_', ''); + await this.handleConfirmUnmatch(chatId, telegramId, matchId); + } + + // Настройки + else if (data === 'settings') { + await this.handleSettings(chatId, telegramId); + } else if (data === 'search_settings') { + await this.handleSearchSettings(chatId, telegramId); + } else if (data === 'notification_settings') { + await this.handleNotificationSettings(chatId, telegramId); + } else if (data === 'view_stats') { + await this.handleViewStats(chatId, telegramId); + } else if (data === 'view_profile_viewers') { + await this.handleViewProfileViewers(chatId, telegramId); + } else if (data === 'hide_profile') { + await this.handleHideProfile(chatId, telegramId); + } else if (data === 'delete_profile') { + await this.handleDeleteProfile(chatId, telegramId); + } else if (data === 'main_menu') { + await this.handleMainMenu(chatId, telegramId); + } else if (data === 'confirm_delete_profile') { + await this.handleConfirmDeleteProfile(chatId, telegramId); + } + + // Информация + else if (data === 'how_it_works') { + await this.handleHowItWorks(chatId); + } else if (data === 'back_to_browsing') { + await this.handleStartBrowsing(chatId, telegramId); + } else if (data === 'get_vip') { + await this.vipController.showVipSearch(chatId, telegramId); + } + + // VIP функции + else if (data === 'vip_search') { + await this.vipController.showVipSearch(chatId, telegramId); + } else if (data === 'vip_quick_search') { + await this.vipController.performQuickVipSearch(chatId, telegramId); + } else if (data === 'vip_advanced_search') { + await this.vipController.startAdvancedSearch(chatId, telegramId); + } else if (data === 'vip_dating_goal_search') { + await this.vipController.showDatingGoalSearch(chatId, telegramId); + } else if (data.startsWith('vip_goal_')) { + const goal = data.replace('vip_goal_', ''); + await this.vipController.performDatingGoalSearch(chatId, telegramId, goal); + } else if (data.startsWith('vip_like_')) { + const targetTelegramId = data.replace('vip_like_', ''); + await this.handleVipLike(chatId, telegramId, targetTelegramId); + } else if (data.startsWith('vip_superlike_')) { + const targetTelegramId = data.replace('vip_superlike_', ''); + await this.handleVipSuperlike(chatId, telegramId, targetTelegramId); + } else if (data.startsWith('vip_dislike_')) { + const targetTelegramId = data.replace('vip_dislike_', ''); + await this.handleVipDislike(chatId, telegramId, targetTelegramId); + } + + // Настройки языка и переводы + else if (data === 'language_settings') { + await this.handleLanguageSettings(chatId, telegramId); + } else if (data.startsWith('set_language_')) { + const languageCode = data.replace('set_language_', ''); + await this.handleSetLanguage(chatId, telegramId, languageCode); + } else if (data.startsWith('translate_profile_')) { + const profileUserId = parseInt(data.replace('translate_profile_', '')); + await this.handleTranslateProfile(chatId, telegramId, profileUserId); + } else if (data === 'back_to_settings') { + await this.handleSettings(chatId, telegramId); + } + + // Настройки уведомлений + else if (data === 'notifications') { + if (this.notificationHandlers) { + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.answerCallbackQuery(query.id, { text: '❌ Вы не зарегистрированы.' }); + return; + } + + const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId); + await this.notificationHandlers.sendNotificationSettings(chatId, settings); + } else { + await this.handleNotificationSettings(chatId, telegramId); + } + } + // Обработка переключения настроек уведомлений + else if (data.startsWith('notif_toggle:') || + data === 'notif_time' || + data.startsWith('notif_time_set:') || + data === 'notif_dnd' || + data.startsWith('notif_dnd_set:') || + data === 'notif_dnd_time' || + data.startsWith('notif_dnd_time_set:') || + data === 'notif_dnd_time_custom') { + // Делегируем обработку в NotificationHandlers, если он доступен + if (this.notificationHandlers) { + // Эти коллбэки уже обрабатываются в NotificationHandlers, поэтому здесь ничего не делаем + // NotificationHandlers уже зарегистрировал свои обработчики в register() + } else { + await this.bot.answerCallbackQuery(query.id, { + text: 'Функция настройки уведомлений недоступна.', + show_alert: true + }); + } + } + else { + await this.bot.answerCallbackQuery(query.id, { + text: 'Функция в разработке!', + show_alert: false + }); + return; + } + + await this.bot.answerCallbackQuery(query.id); + + } catch (error) { + console.error('Callback handler error:', error); + await this.bot.answerCallbackQuery(query.id, { + text: 'Произошла ошибка. Попробуйте еще раз.', + show_alert: true + }); + } + } + + // Добавим все необходимые методы для обработки коллбэков + async handleCreateProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleGenderSelection(chatId: number, telegramId: string, gender: string): Promise { + // Заглушка метода + } + + async handleViewMyProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleManagePhotos(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handlePreviewProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditName(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditAge(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditBio(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditHobbies(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditCity(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditJob(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditEducation(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditHeight(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditReligion(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditDatingGoal(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditLifestyle(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditSearchPreferences(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleAddPhoto(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleDeletePhoto(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSetMainPhoto(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleDeletePhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise { + // Заглушка метода + } + + async handleSetMainPhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise { + // Заглушка метода + } + + async handleSetDatingGoal(chatId: number, telegramId: string, goal: string): Promise { + // Заглушка метода + } + + async handleEditSmoking(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditDrinking(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditKids(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSetLifestyle(chatId: number, telegramId: string, type: string, value: string): Promise { + // Заглушка метода + } + + async handleEditAgeRange(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditDistance(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleStartBrowsing(chatId: number, telegramId: string, showAll: boolean = false): Promise { + // Заглушка метода + } + + async handleVipSearch(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSearchByGoal(chatId: number, telegramId: string, goal: string): Promise { + // Заглушка метода + } + + async handleNextCandidate(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleLike(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleDislike(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleSuperlike(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleViewProfile(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleMorePhotos(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleViewMatches(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleOpenChats(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleOpenChat(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleSendMessage(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleViewChatProfile(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleUnmatch(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleConfirmUnmatch(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleSettings(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSearchSettings(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleViewStats(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleViewProfileViewers(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleHideProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleDeleteProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleMainMenu(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleConfirmDeleteProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleHowItWorks(chatId: number): Promise { + // Заглушка метода + } + + async handleVipLike(chatId: number, telegramId: string, targetTelegramId: string): Promise { + // Заглушка метода + } + + async handleVipSuperlike(chatId: number, telegramId: string, targetTelegramId: string): Promise { + // Заглушка метода + } + + async handleVipDislike(chatId: number, telegramId: string, targetTelegramId: string): Promise { + // Заглушка метода + } + + async handleLanguageSettings(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSetLanguage(chatId: number, telegramId: string, languageCode: string): Promise { + // Заглушка метода + } + + async handleTranslateProfile(chatId: number, telegramId: string, profileUserId: number): Promise { + // Заглушка метода + } + + // Добавим новый метод для настроек уведомлений (вызывает NotificationHandlers) + async handleNotificationSettings(chatId: number, telegramId: string): Promise { + try { + if (this.notificationHandlers) { + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.sendMessage(chatId, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.'); + return; + } + + // Вызываем метод из notificationHandlers для получения настроек и отображения меню + const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId); + await this.notificationHandlers.sendNotificationSettings(chatId, settings); + } else { + // Если NotificationHandlers недоступен, показываем сообщение об ошибке + await this.bot.sendMessage(chatId, '⚙️ Настройки уведомлений временно недоступны. Попробуйте позже.'); + await this.handleSettings(chatId, telegramId); + } + } catch (error) { + console.error('Error handling notification settings:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке настроек уведомлений.'); + } + } +} diff --git a/src/handlers/callbackHandlers.ts.original b/src/handlers/callbackHandlers.ts.original new file mode 100644 index 0000000000000000000000000000000000000000..a1c6abf60518e428e391acedd42d3d970f5e01aa GIT binary patch literal 204286 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 literal 0 HcmV?d00001 diff --git a/src/handlers/callbackHandlers.ts.stub b/src/handlers/callbackHandlers.ts.stub new file mode 100644 index 0000000..4d537fa --- /dev/null +++ b/src/handlers/callbackHandlers.ts.stub @@ -0,0 +1,606 @@ +import TelegramBot, { CallbackQuery, InlineKeyboardMarkup } from 'node-telegram-bot-api'; +import { ProfileService } from '../services/profileService'; +import { MatchingService } from '../services/matchingService'; +import { ChatService } from '../services/chatService'; +import { Profile } from '../models/Profile'; +import { MessageHandlers } from './messageHandlers'; +import { ProfileEditController } from '../controllers/profileEditController'; +import { EnhancedChatHandlers } from './enhancedChatHandlers'; +import { VipController } from '../controllers/vipController'; +import { VipService } from '../services/vipService'; +import { TranslationController } from '../controllers/translationController'; +import { t } from '../services/localizationService'; +import { LikeBackHandler } from './likeBackHandler'; +import { NotificationHandlers } from './notificationHandlers'; + +export class CallbackHandlers { + private bot: TelegramBot; + private profileService: ProfileService; + private matchingService: MatchingService; + private chatService: ChatService; + private messageHandlers: MessageHandlers; + private profileEditController: ProfileEditController; + private enhancedChatHandlers: EnhancedChatHandlers; + private vipController: VipController; + private vipService: VipService; + private translationController: TranslationController; + private likeBackHandler: LikeBackHandler; + private notificationHandlers?: NotificationHandlers; + + constructor(bot: TelegramBot, messageHandlers: MessageHandlers) { + this.bot = bot; + this.profileService = new ProfileService(); + this.matchingService = new MatchingService(); + this.chatService = new ChatService(); + this.messageHandlers = messageHandlers; + this.profileEditController = new ProfileEditController(this.profileService); + this.enhancedChatHandlers = new EnhancedChatHandlers(bot); + this.vipController = new VipController(bot); + this.vipService = new VipService(); + this.translationController = new TranslationController(); + this.likeBackHandler = new LikeBackHandler(bot); + + // Создаем экземпляр NotificationHandlers + try { + this.notificationHandlers = new NotificationHandlers(bot); + } catch (error) { + console.error('Failed to initialize NotificationHandlers:', error); + } + } + + register(): void { + this.bot.on('callback_query', (query) => this.handleCallback(query)); + } + + async handleCallback(query: CallbackQuery): Promise { + if (!query.data || !query.from || !query.message) return; + + const telegramId = query.from.id.toString(); + const chatId = query.message.chat.id; + const data = query.data; + + try { + // Основные действия профиля + if (data === 'create_profile') { + await this.handleCreateProfile(chatId, telegramId); + } else if (data.startsWith('gender_')) { + const gender = data.replace('gender_', ''); + await this.handleGenderSelection(chatId, telegramId, gender); + } else if (data === 'view_my_profile') { + await this.handleViewMyProfile(chatId, telegramId); + } else if (data === 'edit_profile') { + await this.handleEditProfile(chatId, telegramId); + } else if (data === 'manage_photos') { + await this.handleManagePhotos(chatId, telegramId); + } else if (data === 'preview_profile') { + await this.handlePreviewProfile(chatId, telegramId); + } + + // Редактирование полей профиля + else if (data === 'edit_name') { + await this.handleEditName(chatId, telegramId); + } else if (data === 'edit_age') { + await this.handleEditAge(chatId, telegramId); + } else if (data === 'edit_bio') { + await this.handleEditBio(chatId, telegramId); + } else if (data === 'edit_hobbies') { + await this.handleEditHobbies(chatId, telegramId); + } else if (data === 'edit_city') { + await this.handleEditCity(chatId, telegramId); + } else if (data === 'edit_job') { + await this.handleEditJob(chatId, telegramId); + } else if (data === 'edit_education') { + await this.handleEditEducation(chatId, telegramId); + } else if (data === 'edit_height') { + await this.handleEditHeight(chatId, telegramId); + } else if (data === 'edit_religion') { + await this.handleEditReligion(chatId, telegramId); + } else if (data === 'edit_dating_goal') { + await this.handleEditDatingGoal(chatId, telegramId); + } else if (data === 'edit_lifestyle') { + await this.handleEditLifestyle(chatId, telegramId); + } else if (data === 'edit_search_preferences') { + await this.handleEditSearchPreferences(chatId, telegramId); + } + + // Управление фотографиями + else if (data === 'add_photo') { + await this.handleAddPhoto(chatId, telegramId); + } else if (data === 'delete_photo') { + await this.handleDeletePhoto(chatId, telegramId); + } else if (data === 'set_main_photo') { + await this.handleSetMainPhoto(chatId, telegramId); + } else if (data.startsWith('delete_photo_')) { + const photoIndex = parseInt(data.replace('delete_photo_', '')); + await this.handleDeletePhotoByIndex(chatId, telegramId, photoIndex); + } else if (data.startsWith('set_main_photo_')) { + const photoIndex = parseInt(data.replace('set_main_photo_', '')); + await this.handleSetMainPhotoByIndex(chatId, telegramId, photoIndex); + } + + // Цели знакомства + else if (data.startsWith('set_dating_goal_')) { + const goal = data.replace('set_dating_goal_', ''); + await this.handleSetDatingGoal(chatId, telegramId, goal); + } + + // Образ жизни + else if (data === 'edit_smoking') { + await this.handleEditSmoking(chatId, telegramId); + } else if (data === 'edit_drinking') { + await this.handleEditDrinking(chatId, telegramId); + } else if (data === 'edit_kids') { + await this.handleEditKids(chatId, telegramId); + } else if (data.startsWith('set_smoking_')) { + const value = data.replace('set_smoking_', ''); + await this.handleSetLifestyle(chatId, telegramId, 'smoking', value); + } else if (data.startsWith('set_drinking_')) { + const value = data.replace('set_drinking_', ''); + await this.handleSetLifestyle(chatId, telegramId, 'drinking', value); + } else if (data.startsWith('set_kids_')) { + const value = data.replace('set_kids_', ''); + await this.handleSetLifestyle(chatId, telegramId, 'kids', value); + } + + // Настройки поиска + else if (data === 'edit_age_range') { + await this.handleEditAgeRange(chatId, telegramId); + } else if (data === 'edit_distance') { + await this.handleEditDistance(chatId, telegramId); + } + + // Просмотр анкет и свайпы + else if (data === 'start_browsing') { + await this.handleStartBrowsing(chatId, telegramId, false); + } else if (data === 'start_browsing_first') { + // Показываем всех пользователей для нового пользователя + await this.handleStartBrowsing(chatId, telegramId, true); + } else if (data === 'vip_search') { + await this.handleVipSearch(chatId, telegramId); + } else if (data.startsWith('search_by_goal_')) { + const goal = data.replace('search_by_goal_', ''); + await this.handleSearchByGoal(chatId, telegramId, goal); + } else if (data === 'next_candidate') { + await this.handleNextCandidate(chatId, telegramId); + } else if (data.startsWith('like_')) { + const targetUserId = data.replace('like_', ''); + await this.handleLike(chatId, telegramId, targetUserId); + } else if (data.startsWith('dislike_')) { + const targetUserId = data.replace('dislike_', ''); + await this.handleDislike(chatId, telegramId, targetUserId); + } else if (data.startsWith('superlike_')) { + const targetUserId = data.replace('superlike_', ''); + await this.handleSuperlike(chatId, telegramId, targetUserId); + } else if (data.startsWith('view_profile_')) { + const targetUserId = data.replace('view_profile_', ''); + await this.handleViewProfile(chatId, telegramId, targetUserId); + } else if (data.startsWith('more_photos_')) { + const targetUserId = data.replace('more_photos_', ''); + await this.handleMorePhotos(chatId, telegramId, targetUserId); + } + + // Обработка лайков и ответных лайков из уведомлений + else if (data.startsWith('like_back:')) { + const targetUserId = data.replace('like_back:', ''); + await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId); + } + + // Матчи и чаты + else if (data === 'view_matches') { + await this.handleViewMatches(chatId, telegramId); + } else if (data === 'open_chats') { + await this.handleOpenChats(chatId, telegramId); + } else if (data === 'native_chats') { + await this.enhancedChatHandlers.showChatsNative(chatId, telegramId); + } else if (data.startsWith('open_native_chat_')) { + const matchId = data.replace('open_native_chat_', ''); + await this.enhancedChatHandlers.openNativeChat(chatId, telegramId, matchId); + } else if (data.startsWith('chat_history_')) { + const matchId = data.replace('chat_history_', ''); + await this.enhancedChatHandlers.showChatHistory(chatId, telegramId, matchId); + } else if (data.startsWith('chat_')) { + const matchId = data.replace('chat_', ''); + await this.handleOpenChat(chatId, telegramId, matchId); + } else if (data.startsWith('send_message_')) { + const matchId = data.replace('send_message_', ''); + await this.handleSendMessage(chatId, telegramId, matchId); + } else if (data.startsWith('view_chat_profile_')) { + const matchId = data.replace('view_chat_profile_', ''); + await this.handleViewChatProfile(chatId, telegramId, matchId); + } else if (data.startsWith('unmatch_')) { + const matchId = data.replace('unmatch_', ''); + await this.handleUnmatch(chatId, telegramId, matchId); + } else if (data.startsWith('confirm_unmatch_')) { + const matchId = data.replace('confirm_unmatch_', ''); + await this.handleConfirmUnmatch(chatId, telegramId, matchId); + } + + // Настройки + else if (data === 'settings') { + await this.handleSettings(chatId, telegramId); + } else if (data === 'search_settings') { + await this.handleSearchSettings(chatId, telegramId); + } else if (data === 'notification_settings') { + await this.handleNotificationSettings(chatId, telegramId); + } else if (data === 'view_stats') { + await this.handleViewStats(chatId, telegramId); + } else if (data === 'view_profile_viewers') { + await this.handleViewProfileViewers(chatId, telegramId); + } else if (data === 'hide_profile') { + await this.handleHideProfile(chatId, telegramId); + } else if (data === 'delete_profile') { + await this.handleDeleteProfile(chatId, telegramId); + } else if (data === 'main_menu') { + await this.handleMainMenu(chatId, telegramId); + } else if (data === 'confirm_delete_profile') { + await this.handleConfirmDeleteProfile(chatId, telegramId); + } + + // Информация + else if (data === 'how_it_works') { + await this.handleHowItWorks(chatId); + } else if (data === 'back_to_browsing') { + await this.handleStartBrowsing(chatId, telegramId); + } else if (data === 'get_vip') { + await this.vipController.showVipSearch(chatId, telegramId); + } + + // VIP функции + else if (data === 'vip_search') { + await this.vipController.showVipSearch(chatId, telegramId); + } else if (data === 'vip_quick_search') { + await this.vipController.performQuickVipSearch(chatId, telegramId); + } else if (data === 'vip_advanced_search') { + await this.vipController.startAdvancedSearch(chatId, telegramId); + } else if (data === 'vip_dating_goal_search') { + await this.vipController.showDatingGoalSearch(chatId, telegramId); + } else if (data.startsWith('vip_goal_')) { + const goal = data.replace('vip_goal_', ''); + await this.vipController.performDatingGoalSearch(chatId, telegramId, goal); + } else if (data.startsWith('vip_like_')) { + const targetTelegramId = data.replace('vip_like_', ''); + await this.handleVipLike(chatId, telegramId, targetTelegramId); + } else if (data.startsWith('vip_superlike_')) { + const targetTelegramId = data.replace('vip_superlike_', ''); + await this.handleVipSuperlike(chatId, telegramId, targetTelegramId); + } else if (data.startsWith('vip_dislike_')) { + const targetTelegramId = data.replace('vip_dislike_', ''); + await this.handleVipDislike(chatId, telegramId, targetTelegramId); + } + + // Настройки языка и переводы + else if (data === 'language_settings') { + await this.handleLanguageSettings(chatId, telegramId); + } else if (data.startsWith('set_language_')) { + const languageCode = data.replace('set_language_', ''); + await this.handleSetLanguage(chatId, telegramId, languageCode); + } else if (data.startsWith('translate_profile_')) { + const profileUserId = parseInt(data.replace('translate_profile_', '')); + await this.handleTranslateProfile(chatId, telegramId, profileUserId); + } else if (data === 'back_to_settings') { + await this.handleSettings(chatId, telegramId); + } + + // Настройки уведомлений + else if (data === 'notifications') { + if (this.notificationHandlers) { + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.answerCallbackQuery(query.id, { text: '❌ Вы не зарегистрированы.' }); + return; + } + + const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId); + await this.notificationHandlers.sendNotificationSettings(chatId, settings); + } else { + await this.handleNotificationSettings(chatId, telegramId); + } + } + // Обработка переключения настроек уведомлений + else if (data.startsWith('notif_toggle:') || + data === 'notif_time' || + data.startsWith('notif_time_set:') || + data === 'notif_dnd' || + data.startsWith('notif_dnd_set:') || + data === 'notif_dnd_time' || + data.startsWith('notif_dnd_time_set:') || + data === 'notif_dnd_time_custom') { + // Делегируем обработку в NotificationHandlers, если он доступен + if (this.notificationHandlers) { + // Эти коллбэки уже обрабатываются в NotificationHandlers, поэтому здесь ничего не делаем + // NotificationHandlers уже зарегистрировал свои обработчики в register() + } else { + await this.bot.answerCallbackQuery(query.id, { + text: 'Функция настройки уведомлений недоступна.', + show_alert: true + }); + } + } + else { + await this.bot.answerCallbackQuery(query.id, { + text: 'Функция в разработке!', + show_alert: false + }); + return; + } + + await this.bot.answerCallbackQuery(query.id); + + } catch (error) { + console.error('Callback handler error:', error); + await this.bot.answerCallbackQuery(query.id, { + text: 'Произошла ошибка. Попробуйте еще раз.', + show_alert: true + }); + } + } + + // Добавим все необходимые методы для обработки коллбэков + async handleCreateProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleGenderSelection(chatId: number, telegramId: string, gender: string): Promise { + // Заглушка метода + } + + async handleViewMyProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleManagePhotos(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handlePreviewProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditName(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditAge(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditBio(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditHobbies(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditCity(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditJob(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditEducation(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditHeight(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditReligion(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditDatingGoal(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditLifestyle(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditSearchPreferences(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleAddPhoto(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleDeletePhoto(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSetMainPhoto(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleDeletePhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise { + // Заглушка метода + } + + async handleSetMainPhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise { + // Заглушка метода + } + + async handleSetDatingGoal(chatId: number, telegramId: string, goal: string): Promise { + // Заглушка метода + } + + async handleEditSmoking(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditDrinking(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditKids(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSetLifestyle(chatId: number, telegramId: string, type: string, value: string): Promise { + // Заглушка метода + } + + async handleEditAgeRange(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleEditDistance(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleStartBrowsing(chatId: number, telegramId: string, showAll: boolean = false): Promise { + // Заглушка метода + } + + async handleVipSearch(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSearchByGoal(chatId: number, telegramId: string, goal: string): Promise { + // Заглушка метода + } + + async handleNextCandidate(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleLike(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleDislike(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleSuperlike(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleViewProfile(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleMorePhotos(chatId: number, telegramId: string, targetUserId: string): Promise { + // Заглушка метода + } + + async handleViewMatches(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleOpenChats(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleOpenChat(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleSendMessage(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleViewChatProfile(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleUnmatch(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleConfirmUnmatch(chatId: number, telegramId: string, matchId: string): Promise { + // Заглушка метода + } + + async handleSettings(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSearchSettings(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleViewStats(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleViewProfileViewers(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleHideProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleDeleteProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleMainMenu(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleConfirmDeleteProfile(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleHowItWorks(chatId: number): Promise { + // Заглушка метода + } + + async handleVipLike(chatId: number, telegramId: string, targetTelegramId: string): Promise { + // Заглушка метода + } + + async handleVipSuperlike(chatId: number, telegramId: string, targetTelegramId: string): Promise { + // Заглушка метода + } + + async handleVipDislike(chatId: number, telegramId: string, targetTelegramId: string): Promise { + // Заглушка метода + } + + async handleLanguageSettings(chatId: number, telegramId: string): Promise { + // Заглушка метода + } + + async handleSetLanguage(chatId: number, telegramId: string, languageCode: string): Promise { + // Заглушка метода + } + + async handleTranslateProfile(chatId: number, telegramId: string, profileUserId: number): Promise { + // Заглушка метода + } + + // Добавим новый метод для настроек уведомлений (вызывает NotificationHandlers) + async handleNotificationSettings(chatId: number, telegramId: string): Promise { + try { + if (this.notificationHandlers) { + const userId = await this.profileService.getUserIdByTelegramId(telegramId); + if (!userId) { + await this.bot.sendMessage(chatId, '❌ Вы не зарегистрированы. Используйте команду /start для регистрации.'); + return; + } + + // Вызываем метод из notificationHandlers для получения настроек и отображения меню + const settings = await this.notificationHandlers.getNotificationService().getNotificationSettings(userId); + await this.notificationHandlers.sendNotificationSettings(chatId, settings); + } else { + // Если NotificationHandlers недоступен, показываем сообщение об ошибке + await this.bot.sendMessage(chatId, '⚙️ Настройки уведомлений временно недоступны. Попробуйте позже.'); + await this.handleSettings(chatId, telegramId); + } + } catch (error) { + console.error('Error handling notification settings:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке настроек уведомлений.'); + } + } +} diff --git a/src/handlers/commandHandlers.ts b/src/handlers/commandHandlers.ts index c86032f..77d2ad3 100644 --- a/src/handlers/commandHandlers.ts +++ b/src/handlers/commandHandlers.ts @@ -3,16 +3,19 @@ import { ProfileService } from '../services/profileService'; import { MatchingService } from '../services/matchingService'; import { Profile } from '../models/Profile'; import { getUserTranslation } from '../services/localizationService'; +import { NotificationHandlers } from './notificationHandlers'; export class CommandHandlers { private bot: TelegramBot; private profileService: ProfileService; private matchingService: MatchingService; + private notificationHandlers: NotificationHandlers; constructor(bot: TelegramBot) { this.bot = bot; this.profileService = new ProfileService(); this.matchingService = new MatchingService(); + this.notificationHandlers = new NotificationHandlers(bot); } register(): void { @@ -23,6 +26,12 @@ export class CommandHandlers { this.bot.onText(/\/matches/, (msg: Message) => this.handleMatches(msg)); this.bot.onText(/\/settings/, (msg: Message) => this.handleSettings(msg)); this.bot.onText(/\/create_profile/, (msg: Message) => this.handleCreateProfile(msg)); + + // Регистрация обработчика настроек уведомлений + this.bot.onText(/\/notifications/, (msg: Message) => this.notificationHandlers.handleNotificationsCommand(msg)); + + // Регистрируем обработчики для уведомлений + this.notificationHandlers.register(); } async handleStart(msg: Message): Promise { @@ -44,7 +53,8 @@ export class CommandHandlers { { text: '⭐ VIP поиск', callback_data: 'vip_search' } ], [ - { text: '⚙️ Настройки', callback_data: 'settings' } + { text: '⚙️ Настройки', callback_data: 'settings' }, + { text: '🔔 Уведомления', callback_data: 'notifications' } ] ] }; @@ -84,6 +94,7 @@ export class CommandHandlers { /browse - Просмотр анкет /matches - Ваши матчи /settings - Настройки +/notifications - Настройки уведомлений /help - Эта справка � Как использовать: @@ -191,7 +202,7 @@ export class CommandHandlers { inline_keyboard: [ [ { text: '🔍 Настройки поиска', callback_data: 'search_settings' }, - { text: '🔔 Уведомления', callback_data: 'notification_settings' } + { text: '🔔 Уведомления', callback_data: 'notifications' } ], [ { text: '🚫 Скрыть профиль', callback_data: 'hide_profile' }, @@ -242,7 +253,10 @@ export class CommandHandlers { { text: '✏️ Редактировать', callback_data: 'edit_profile' }, { text: '📸 Фото', callback_data: 'manage_photos' } ], - [{ text: '🔍 Начать поиск', callback_data: 'start_browsing' }] + [ + { text: '🔍 Начать поиск', callback_data: 'start_browsing' }, + { text: '🔔 Уведомления', callback_data: 'notifications' } + ] ] } : { inline_keyboard: [ diff --git a/src/handlers/notificationHandlers.ts b/src/handlers/notificationHandlers.ts new file mode 100644 index 0000000..3c6866e --- /dev/null +++ b/src/handlers/notificationHandlers.ts @@ -0,0 +1,644 @@ +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); + } + }); + } +} diff --git a/src/scripts/setPremiumForAll.ts b/src/scripts/setPremiumForAll.ts index 208a2da..e502286 100644 --- a/src/scripts/setPremiumForAll.ts +++ b/src/scripts/setPremiumForAll.ts @@ -1,9 +1,33 @@ -import { query } from '../database/connection'; +import { query, testConnection } from '../database/connection'; async function setAllUsersToPremium() { try { console.log('Setting premium status for all users...'); + // Проверка соединения с базой данных + console.log('Testing database connection...'); + const dbConnected = await testConnection(); + if (!dbConnected) { + throw new Error('Failed to connect to database. Please check your database settings.'); + } + console.log('Database connection successful!'); + + // Проверка наличия столбца premium + const checkResult = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'users' + AND column_name = 'premium' + ); + `); + + if (!checkResult.rows[0].exists) { + console.log('Adding premium column to users table...'); + await query(`ALTER TABLE users ADD COLUMN premium BOOLEAN DEFAULT false;`); + console.log('Premium column added successfully'); + } + const result = await query(` UPDATE users SET premium = true diff --git a/src/services/matchingService.ts b/src/services/matchingService.ts index 6d54a21..ff570d7 100644 --- a/src/services/matchingService.ts +++ b/src/services/matchingService.ts @@ -564,7 +564,7 @@ export class MatchingService { FROM profiles p JOIN users u ON p.user_id = u.id WHERE p.is_visible = true - AND p.is_active = true + AND u.is_active = true AND p.gender = $1 AND p.dating_goal = $2 AND p.user_id NOT IN (${swipedUserIds.map((_: any, i: number) => `$${i + 3}`).join(', ')}) diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index a34e59a..833c86c 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -1,14 +1,27 @@ import TelegramBot from 'node-telegram-bot-api'; -import { query } from '../database/connection'; +import { query, transaction } from '../database/connection'; import { ProfileService } from './profileService'; +import { v4 as uuidv4 } from 'uuid'; export interface NotificationData { userId: string; - type: 'new_match' | 'new_message' | 'new_like' | 'super_like'; + type: 'new_match' | 'new_message' | 'new_like' | 'super_like' | 'match_reminder' | 'inactive_matches' | 'like_summary'; data: Record; scheduledAt?: Date; } +export interface NotificationSettings { + newMatches: boolean; + newMessages: boolean; + newLikes: boolean; + reminders: boolean; + dailySummary: boolean; + timePreference: 'morning' | 'afternoon' | 'evening' | 'night'; + doNotDisturb: boolean; + doNotDisturbStart?: string; // Формат: "HH:MM" + doNotDisturbEnd?: string; // Формат: "HH:MM" +} + export class NotificationService { private bot?: TelegramBot; private profileService: ProfileService; @@ -16,6 +29,235 @@ export class NotificationService { constructor(bot?: TelegramBot) { this.bot = bot; this.profileService = new ProfileService(); + + // Создаем таблицу уведомлений, если её еще нет + this.ensureNotificationTablesExist().catch(err => + console.error('Failed to create notification tables:', err) + ); + } + + // Проверка и создание таблиц для уведомлений + private async ensureNotificationTablesExist(): Promise { + try { + // Проверяем существование таблицы notifications + const notificationsExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'notifications' + ) as exists + `); + + if (!notificationsExists.rows[0].exists) { + await query(` + CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + console.log('Created notifications table'); + } + + // Проверяем существование таблицы scheduled_notifications + const scheduledExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'scheduled_notifications' + ) as exists + `); + + if (!scheduledExists.rows[0].exists) { + await query(` + CREATE TABLE scheduled_notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + scheduled_at TIMESTAMP NOT NULL, + processed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + console.log('Created scheduled_notifications table'); + } + + // Проверяем существование таблицы notification_templates + const templatesExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'notification_templates' + ) as exists + `); + + if (!templatesExists.rows[0].exists) { + await query(` + CREATE TABLE notification_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + type VARCHAR(50) NOT NULL UNIQUE, + title TEXT NOT NULL, + message_template TEXT NOT NULL, + button_template JSONB, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + `); + console.log('Created notification_templates table'); + + // Заполняем таблицу базовыми шаблонами + await this.populateDefaultTemplates(); + } + + // Проверяем, есть ли колонка notification_settings в таблице users + const settingsColumnExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'notification_settings' + ) as exists + `); + + if (!settingsColumnExists.rows[0].exists) { + await query(` + ALTER TABLE users + ADD COLUMN notification_settings JSONB DEFAULT '{ + "newMatches": true, + "newMessages": true, + "newLikes": true, + "reminders": true, + "dailySummary": true, + "timePreference": "evening", + "doNotDisturb": false + }'::jsonb + `); + console.log('Added notification_settings column to users table'); + } + + } catch (error) { + console.error('Error ensuring notification tables exist:', error); + throw error; + } + } + + // Заполнение таблицы шаблонов уведомлений + private async populateDefaultTemplates(): Promise { + try { + const templates = [ + { + type: 'new_like', + title: 'Новый лайк!', + message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'super_like', + title: 'Супер-лайк!', + message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'new_match', + title: 'Новый матч!', + message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'new_message', + title: 'Новое сообщение!', + message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + button_template: { + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'match_reminder', + title: 'Напоминание о матче', + message_template: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }] + ] + } + }, + { + type: 'inactive_matches', + title: 'Неактивные матчи', + message_template: '⏰ У вас {{count}} неактивных матчей!\n\nПродолжите общение, чтобы не упустить интересные знакомства!', + button_template: { + inline_keyboard: [ + [{ text: '📋 Открыть матчи', callback_data: 'view_matches' }], + [{ text: '💕 Смотреть новые анкеты', callback_data: 'start_browsing' }] + ] + } + }, + { + type: 'like_summary', + title: 'Сводка лайков', + message_template: '💖 У вас {{count}} новых лайков!\n\nПосмотрите, кто проявил к вам интерес сегодня!', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть лайки', callback_data: 'view_likes' }], + [{ text: '💕 Начать знакомиться', callback_data: 'start_browsing' }] + ] + } + } + ]; + + for (const template of templates) { + await query(` + INSERT INTO notification_templates (type, title, message_template, button_template) + VALUES ($1, $2, $3, $4) + ON CONFLICT (type) DO UPDATE + SET title = EXCLUDED.title, + message_template = EXCLUDED.message_template, + button_template = EXCLUDED.button_template, + updated_at = NOW() + `, [ + template.type, + template.title, + template.message_template, + JSON.stringify(template.button_template) + ]); + } + + console.log('Populated notification templates'); + } catch (error) { + console.error('Error populating default templates:', error); + } } // Получить шаблон уведомления из базы данных или использовать встроенный @@ -78,10 +320,10 @@ export class NotificationService { messageTemplate: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', buttonTemplate: { inline_keyboard: [ - [{ text: '💬 Начать общение', callback_data: 'open_native_chat_{{matchId}}' }], + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], [ { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, - { text: '📋 Все матчи', callback_data: 'native_chats' } + { text: '📋 Все матчи', callback_data: 'view_matches' } ] ] } @@ -91,13 +333,23 @@ export class NotificationService { messageTemplate: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', buttonTemplate: { inline_keyboard: [ - [{ text: '📩 Ответить', callback_data: 'open_native_chat_{{matchId}}' }], + [{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }], [ { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, - { text: '📋 Все чаты', callback_data: 'native_chats' } + { text: '📋 Все чаты', callback_data: 'view_matches' } ] ] } + }, + 'match_reminder': { + title: 'Напоминание о матче', + messageTemplate: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!', + buttonTemplate: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }] + ] + } } }; @@ -138,6 +390,44 @@ export class NotificationService { return replaceInObject(result); } + // Проверка режима "Не беспокоить" + private async shouldSendNotification(userId: string): Promise { + try { + const settings = await this.getNotificationSettings(userId); + + if (!settings.doNotDisturb) { + return true; // Режим "Не беспокоить" выключен + } + + // Если нет указанного времени, по умолчанию: не беспокоить с 23:00 до 8:00 + const startTime = settings.doNotDisturbStart || '23:00'; + const endTime = settings.doNotDisturbEnd || '08:00'; + + const now = new Date(); + const currentHour = now.getHours(); + const currentMinute = now.getMinutes(); + + const [startHour, startMinute] = startTime.split(':').map(Number); + const [endHour, endMinute] = endTime.split(':').map(Number); + + const currentMinutes = currentHour * 60 + currentMinute; + const startMinutes = startHour * 60 + startMinute; + const endMinutes = endHour * 60 + endMinute; + + // Если время окончания меньше времени начала, значит период охватывает полночь + if (endMinutes < startMinutes) { + // Не отправлять, если текущее время между начальным и конечным временем (например, 23:30-07:00) + return !(currentMinutes >= startMinutes || currentMinutes <= endMinutes); + } else { + // Не отправлять, если текущее время между начальным и конечным временем (например, 13:00-15:00) + return !(currentMinutes >= startMinutes && currentMinutes <= endMinutes); + } + } catch (error) { + console.error('Error checking notification settings:', error); + return true; // В случае ошибки отправляем уведомление + } + } + // Отправить уведомление о новом лайке async sendLikeNotification(targetTelegramId: string, likerTelegramId: string, isSuperLike: boolean = false): Promise { try { @@ -146,7 +436,51 @@ export class NotificationService { this.profileService.getProfileByTelegramId(likerTelegramId) ]); - if (!targetUser || !likerProfile || !this.bot) { + if (!targetUser || !likerProfile) { + console.log(`Couldn't send like notification: user or profile not found`); + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(targetUser.id); + if (!settings.newLikes) { + console.log(`Like notifications disabled for user ${targetUser.id}`); + + // Логируем уведомление для истории + await this.logNotification({ + userId: targetUser.id, + type: isSuperLike ? 'super_like' : 'new_like', + data: { + likerUserId: likerProfile.userId, + likerName: likerProfile.name, + age: likerProfile.age, + city: likerProfile.city + } + }); + + // Запланируем отправку сводки лайков позже + await this.scheduleLikesSummary(targetUser.id); + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(targetUser.id))) { + console.log(`Do not disturb mode active for user ${targetUser.id}`); + + // Логируем уведомление и запланируем отправку позже + await this.logNotification({ + userId: targetUser.id, + type: isSuperLike ? 'super_like' : 'new_like', + data: { + likerUserId: likerProfile.userId, + likerName: likerProfile.name, + age: likerProfile.age, + city: likerProfile.city + } + }); + + // Запланируем отправку сводки лайков позже + await this.scheduleLikesSummary(targetUser.id); return; } @@ -169,10 +503,13 @@ export class NotificationService { const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); // Отправляем уведомление - await this.bot.sendMessage(targetUser.telegram_id, message, { - parse_mode: 'Markdown', - reply_markup: keyboard - }); + if (this.bot) { + await this.bot.sendMessage(targetUser.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`Sent ${templateType} notification to user ${targetUser.id}`); + } // Логируем уведомление await this.logNotification({ @@ -198,7 +535,48 @@ export class NotificationService { this.profileService.getProfileByUserId(matchedUserId) ]); - if (!user || !matchedProfile || !this.bot) { + if (!user || !matchedProfile) { + console.log(`Couldn't send match notification: user or profile not found`); + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.newMatches) { + console.log(`Match notifications disabled for user ${userId}`); + + // Логируем уведомление для истории + await this.logNotification({ + userId, + type: 'new_match', + data: { + matchedUserId, + matchedName: matchedProfile.name, + age: matchedProfile.age, + city: matchedProfile.city + } + }); + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + console.log(`Do not disturb mode active for user ${userId}`); + + // Логируем уведомление + await this.logNotification({ + userId, + type: 'new_match', + data: { + matchedUserId, + matchedName: matchedProfile.name, + age: matchedProfile.age, + city: matchedProfile.city + } + }); + + // Запланируем отправку напоминания о матче позже + await this.scheduleMatchReminder(userId, matchedUserId); return; } @@ -230,10 +608,13 @@ export class NotificationService { const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); // Отправляем уведомление - await this.bot.sendMessage(user.telegram_id, message, { - parse_mode: 'Markdown', - reply_markup: keyboard - }); + if (this.bot) { + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`Sent new_match notification to user ${userId}`); + } // Логируем уведомление await this.logNotification({ @@ -247,6 +628,22 @@ export class NotificationService { matchId } }); + + // Если это новый матч, запланируем напоминание через 24 часа, если пользователь не начнет общение + if (matchId) { + const reminderDate = new Date(); + reminderDate.setHours(reminderDate.getHours() + 24); + + await this.scheduleNotification({ + userId, + type: 'match_reminder', + data: { + matchedUserId, + matchId + }, + scheduledAt: reminderDate + }); + } } catch (error) { console.error('Error sending match notification:', error); } @@ -260,15 +657,30 @@ export class NotificationService { this.profileService.getProfileByUserId(senderId) ]); - if (!receiver || !senderProfile || !this.bot) { + if (!receiver || !senderProfile) { + console.log(`Couldn't send message notification: user or profile not found`); + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(receiverId); + if (!settings.newMessages) { + console.log(`Message notifications disabled for user ${receiverId}`); return; } // Проверяем, не в чате ли пользователь сейчас const isUserActive = await this.isUserActiveInChat(receiverId, senderId); if (isUserActive) { + console.log(`User ${receiverId} is active in chat with ${senderId}, skipping notification`); return; // Не отправляем уведомление, если пользователь активен в чате } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(receiverId))) { + console.log(`Do not disturb mode active for user ${receiverId}`); + return; + } // Если matchId не передан, пытаемся его получить let actualMatchId = matchId; @@ -304,10 +716,13 @@ export class NotificationService { const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); // Отправляем уведомление - await this.bot.sendMessage(receiver.telegram_id, message, { - parse_mode: 'Markdown', - reply_markup: keyboard - }); + if (this.bot) { + await this.bot.sendMessage(receiver.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`Sent new_message notification to user ${receiverId}`); + } // Логируем уведомление await this.logNotification({ @@ -325,66 +740,330 @@ export class NotificationService { } } - // Отправить напоминание о неактивности - async sendInactivityReminder(userId: string): Promise { + // Отправить напоминание о матче без сообщений + async sendMatchReminder(userId: string, matchedUserId: string, matchId: string): Promise { try { + const [user, matchedProfile] = await Promise.all([ + this.getUserByUserId(userId), + this.profileService.getProfileByUserId(matchedUserId) + ]); + + if (!user || !matchedProfile || !this.bot) { + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.reminders) { + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + return; + } + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('match_reminder'); + + // Подготовка данных для шаблона + const templateData = { + name: matchedProfile.name, + userId: matchedProfile.userId, + matchId + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + + console.log(`Sent match reminder to user ${userId} about match with ${matchedUserId}`); + } catch (error) { + console.error('Error sending match reminder:', error); + } + } + + // Отправить сводку по неактивным матчам + async sendInactiveMatchesSummary(userId: string, count: number): Promise { + try { + const user = await this.getUserByUserId(userId); + if (!user || !this.bot || count === 0) { + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.reminders) { + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + return; + } + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('inactive_matches'); + + // Подготовка данных для шаблона + const templateData = { + count: count.toString() + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + + console.log(`Sent inactive matches summary to user ${userId}, count: ${count}`); + } catch (error) { + console.error('Error sending inactive matches summary:', error); + } + } + + // Отправить сводку полученных лайков + async sendLikesSummary(userId: string): Promise { + try { + // Получаем количество непрочитанных лайков + const result = await query(` + SELECT COUNT(*) as count + FROM notifications + WHERE user_id = $1 AND is_read = false AND type IN ('new_like', 'super_like') + AND created_at > NOW() - INTERVAL '24 hours' + `, [userId]); + + const count = parseInt(result.rows[0].count); + if (count === 0) { + return; // Нет новых лайков для отображения + } + const user = await this.getUserByUserId(userId); if (!user || !this.bot) { return; } - const message = `👋 Давно не виделись!\n\nВозможно, ваш идеальный матч уже ждет. Давайте найдем кого-то особенного?`; - - await this.bot.sendMessage(user.telegram_id, message, { - reply_markup: { - inline_keyboard: [[ - { text: '💕 Начать знакомиться', callback_data: 'start_browsing' }, - { text: '⚙️ Настройки', callback_data: 'settings' } - ]] - } - }); - } catch (error) { - console.error('Error sending inactivity reminder:', error); - } - } - - // Отправить уведомление о новых лайках (сводка) - async sendLikesSummary(userId: string, likesCount: number): Promise { - try { - const user = await this.getUserByUserId(userId); - if (!user || !this.bot || likesCount === 0) { + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.dailySummary) { + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { return; } - const message = likesCount === 1 - ? `💖 У вас 1 новый лайк! Посмотрите, кто это может быть.` - : `💖 У вас ${likesCount} новых лайков! Посмотрите, кто проявил к вам интерес.`; - + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('like_summary'); + + // Подготовка данных для шаблона + const templateData = { + count: count.toString() + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление await this.bot.sendMessage(user.telegram_id, message, { - reply_markup: { - inline_keyboard: [[ - { text: '👀 Посмотреть лайки', callback_data: 'view_likes' }, - { text: '💕 Начать знакомиться', callback_data: 'start_browsing' } - ]] - } + parse_mode: 'Markdown', + reply_markup: keyboard }); + + // Отмечаем уведомления как прочитанные + await query(` + UPDATE notifications + SET is_read = true + WHERE user_id = $1 AND is_read = false AND type IN ('new_like', 'super_like') + AND created_at > NOW() - INTERVAL '24 hours' + `, [userId]); + + console.log(`Sent likes summary to user ${userId}, count: ${count}`); } catch (error) { console.error('Error sending likes summary:', error); } } + // Запланировать отправку сводки лайков + async scheduleLikesSummary(userId: string): Promise { + try { + // Определить предпочтительное время для отправки + const settings = await this.getNotificationSettings(userId); + const scheduledAt = this.getPreferredScheduleTime(settings.timePreference); + + // Проверяем, есть ли уже запланированная сводка лайков + const existingResult = await query(` + SELECT id FROM scheduled_notifications + WHERE user_id = $1 AND type = 'like_summary' AND processed = false + AND scheduled_at > NOW() + `, [userId]); + + if (existingResult.rows.length > 0) { + console.log(`Like summary already scheduled for user ${userId}`); + return; // Уже есть запланированная сводка + } + + // Запланировать отправку сводки + await query(` + INSERT INTO scheduled_notifications (id, user_id, type, scheduled_at) + VALUES ($1, $2, $3, $4) + `, [uuidv4(), userId, 'like_summary', scheduledAt]); + + console.log(`Scheduled likes summary for user ${userId} at ${scheduledAt}`); + } catch (error) { + console.error('Error scheduling likes summary:', error); + } + } + + // Запланировать напоминание о матче + async scheduleMatchReminder(userId: string, matchedUserId: string): Promise { + try { + // Получаем матч-ID для перехода в чат + const matchResult = await query(` + SELECT id FROM matches + WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1) + AND is_active = true + `, [userId, matchedUserId]); + + const matchId = matchResult.rows[0]?.id; + if (!matchId) { + console.log(`No active match found between users ${userId} and ${matchedUserId}`); + return; + } + + // Определить предпочтительное время для отправки + const settings = await this.getNotificationSettings(userId); + const scheduledAt = this.getPreferredScheduleTime(settings.timePreference); + + // Запланировать напоминание + await query(` + INSERT INTO scheduled_notifications (id, user_id, type, data, scheduled_at) + VALUES ($1, $2, $3, $4, $5) + `, [ + uuidv4(), + userId, + 'match_reminder', + JSON.stringify({ matchId, matchedUserId }), + scheduledAt + ]); + + console.log(`Scheduled match reminder for user ${userId} about match with ${matchedUserId} at ${scheduledAt}`); + } catch (error) { + console.error('Error scheduling match reminder:', error); + } + } + + // Получить предпочтительное время для отправки уведомлений + private getPreferredScheduleTime(preference: string): Date { + const now = new Date(); + const scheduledAt = new Date(); + + // Если текущее время после полуночи, планируем на сегодня + // Иначе планируем на следующий день + if (now.getHours() < 12) { + // Сегодня + switch (preference) { + case 'morning': + scheduledAt.setHours(9, 0, 0, 0); + break; + case 'afternoon': + scheduledAt.setHours(13, 0, 0, 0); + break; + case 'evening': + scheduledAt.setHours(19, 0, 0, 0); + break; + case 'night': + scheduledAt.setHours(22, 0, 0, 0); + break; + default: + scheduledAt.setHours(19, 0, 0, 0); // По умолчанию вечер + } + } else { + // Завтра + scheduledAt.setDate(scheduledAt.getDate() + 1); + switch (preference) { + case 'morning': + scheduledAt.setHours(9, 0, 0, 0); + break; + case 'afternoon': + scheduledAt.setHours(13, 0, 0, 0); + break; + case 'evening': + scheduledAt.setHours(19, 0, 0, 0); + break; + case 'night': + scheduledAt.setHours(22, 0, 0, 0); + break; + default: + scheduledAt.setHours(9, 0, 0, 0); // По умолчанию утро + } + } + + // Если запланированное время уже прошло, добавляем еще один день + if (scheduledAt <= now) { + scheduledAt.setDate(scheduledAt.getDate() + 1); + } + + return scheduledAt; + } + + // Запланировать уведомление + async scheduleNotification(notificationData: NotificationData): Promise { + try { + if (!notificationData.scheduledAt) { + notificationData.scheduledAt = new Date(); + } + + await query(` + INSERT INTO scheduled_notifications (id, user_id, type, data, scheduled_at) + VALUES ($1, $2, $3, $4, $5) + `, [ + uuidv4(), + notificationData.userId, + notificationData.type, + JSON.stringify(notificationData.data), + notificationData.scheduledAt + ]); + + console.log(`Scheduled ${notificationData.type} notification for user ${notificationData.userId} at ${notificationData.scheduledAt}`); + } catch (error) { + console.error('Error scheduling notification:', error); + } + } + // Логирование уведомлений private async logNotification(notificationData: NotificationData): Promise { try { await query(` - INSERT INTO notifications (user_id, type, data, created_at) - VALUES ($1, $2, $3, $4) + INSERT INTO notifications (id, user_id, type, data, created_at) + VALUES ($1, $2, $3, $4, $5) `, [ + uuidv4(), notificationData.userId, notificationData.type, JSON.stringify(notificationData.data), new Date() ]); + + console.log(`Logged ${notificationData.type} notification for user ${notificationData.userId}`); } catch (error) { console.error('Error logging notification:', error); } @@ -441,53 +1120,16 @@ export class NotificationService { const now = new Date(); const hoursSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / (1000 * 60 * 60); - // Считаем активным если последнее сообщение было менее 24 часов назад - return hoursSinceLastMessage < 24; + // Считаем активным если последнее сообщение было менее 10 минут назад + return hoursSinceLastMessage < (10 / 60); } catch (error) { console.error('Error checking user activity:', error); return false; } } - // Отправить пуш-уведомление (для будущего использования) - async sendPushNotification(userId: string, title: string, body: string, data?: any): Promise { - try { - // Логируем уведомление - console.log(`📱 Push notification prepared for user ${userId}:`); - console.log(`📋 Title: ${title}`); - console.log(`💬 Body: ${body}`); - if (data) { - console.log(`📊 Data:`, JSON.stringify(data, null, 2)); - } - - // В будущем здесь будет интеграция с Firebase Cloud Messaging - // или другим сервисом пуш-уведомлений: - /* - const message = { - notification: { - title, - body - }, - data: data ? JSON.stringify(data) : undefined, - token: await this.getUserPushToken(userId) - }; - - await admin.messaging().send(message); - console.log(`✅ Push notification sent to user ${userId}`); - */ - - } catch (error) { - console.error(`❌ Error preparing push notification for user ${userId}:`, error); - } - } - // Получить настройки уведомлений пользователя - async getNotificationSettings(userId: string): Promise<{ - newMatches: boolean; - newMessages: boolean; - newLikes: boolean; - reminders: boolean; - }> { + async getNotificationSettings(userId: string): Promise { try { const result = await query( 'SELECT notification_settings FROM users WHERE id = $1', @@ -499,15 +1141,26 @@ export class NotificationService { newMatches: true, newMessages: true, newLikes: true, - reminders: true + reminders: true, + dailySummary: true, + timePreference: 'evening', + doNotDisturb: false }; } - return result.rows[0].notification_settings || { - newMatches: true, - newMessages: true, - newLikes: true, - reminders: true + const settings = result.rows[0].notification_settings || {}; + + // Возвращаем настройки с дефолтными значениями для отсутствующих свойств + return { + newMatches: settings.newMatches !== undefined ? settings.newMatches : true, + newMessages: settings.newMessages !== undefined ? settings.newMessages : true, + newLikes: settings.newLikes !== undefined ? settings.newLikes : true, + reminders: settings.reminders !== undefined ? settings.reminders : true, + dailySummary: settings.dailySummary !== undefined ? settings.dailySummary : true, + timePreference: settings.timePreference || 'evening', + doNotDisturb: settings.doNotDisturb || false, + doNotDisturbStart: settings.doNotDisturbStart, + doNotDisturbEnd: settings.doNotDisturbEnd }; } catch (error) { console.error('Error getting notification settings:', error); @@ -515,23 +1168,32 @@ export class NotificationService { newMatches: true, newMessages: true, newLikes: true, - reminders: true + reminders: true, + dailySummary: true, + timePreference: 'evening', + doNotDisturb: false }; } } // Обновить настройки уведомлений - async updateNotificationSettings(userId: string, settings: { - newMatches?: boolean; - newMessages?: boolean; - newLikes?: boolean; - reminders?: boolean; - }): Promise { + async updateNotificationSettings(userId: string, settings: Partial): Promise { try { + // Получаем текущие настройки + const currentSettings = await this.getNotificationSettings(userId); + + // Обновляем только переданные поля + const updatedSettings = { + ...currentSettings, + ...settings + }; + await query( 'UPDATE users SET notification_settings = $1 WHERE id = $2', - [JSON.stringify(settings), userId] + [JSON.stringify(updatedSettings), userId] ); + + console.log(`Updated notification settings for user ${userId}`); } catch (error) { console.error('Error updating notification settings:', error); } @@ -540,29 +1202,6 @@ export class NotificationService { // Планировщик уведомлений (вызывается периодически) async processScheduledNotifications(): Promise { try { - // Проверим, существует ли таблица scheduled_notifications - const tableCheck = await query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_name = 'scheduled_notifications' - ) as exists - `); - - if (!tableCheck.rows[0].exists) { - // Если таблицы нет, создаем её - await query(` - CREATE TABLE IF NOT EXISTS scheduled_notifications ( - id UUID PRIMARY KEY, - user_id UUID REFERENCES users(id), - type VARCHAR(50) NOT NULL, - data JSONB, - scheduled_at TIMESTAMP NOT NULL, - processed BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT NOW() - ) - `); - } - // Получаем запланированные уведомления const result = await query(` SELECT * FROM scheduled_notifications @@ -571,17 +1210,49 @@ export class NotificationService { LIMIT 100 `, [new Date()]); + console.log(`Processing ${result.rows.length} scheduled notifications`); + for (const notification of result.rows) { try { + const data = notification.data || {}; + switch (notification.type) { - case 'inactivity_reminder': - await this.sendInactivityReminder(notification.user_id); + case 'match_reminder': + if (data.matchId && data.matchedUserId) { + // Проверяем, что матч всё еще активен и нет сообщений + const matchCheck = await query(` + SELECT m.id + FROM matches m + LEFT JOIN messages msg ON msg.match_id = m.id + WHERE m.id = $1 AND m.is_active = true + AND msg.id IS NULL + `, [data.matchId]); + + if (matchCheck.rows.length > 0) { + await this.sendMatchReminder(notification.user_id, data.matchedUserId, data.matchId); + } + } break; - case 'likes_summary': - const likesCount = notification.data?.likesCount || 0; - await this.sendLikesSummary(notification.user_id, likesCount); + case 'like_summary': + await this.sendLikesSummary(notification.user_id); + break; + case 'inactive_matches': + // Получаем количество неактивных матчей + const matchesResult = await query(` + SELECT COUNT(*) as count + FROM matches m + LEFT JOIN messages msg ON msg.match_id = m.id + WHERE (m.user_id_1 = $1 OR m.user_id_2 = $1) + AND m.is_active = true + AND msg.id IS NULL + AND m.created_at < NOW() - INTERVAL '3 days' + `, [notification.user_id]); + + const count = parseInt(matchesResult.rows[0].count); + if (count > 0) { + await this.sendInactiveMatchesSummary(notification.user_id, count); + } break; - // Добавить другие типы уведомлений } // Отмечаем как обработанное @@ -597,4 +1268,49 @@ export class NotificationService { console.error('Error processing scheduled notifications:', error); } } + + // Планирование периодических уведомлений для всех пользователей + async schedulePeriodicNotifications(): Promise { + try { + // Получаем список активных пользователей + const usersResult = await query(` + SELECT u.id, u.notification_settings + FROM users u + JOIN profiles p ON u.id = p.user_id + WHERE p.is_visible = true + AND u.created_at < NOW() - INTERVAL '1 day' + `); + + for (const user of usersResult.rows) { + try { + const settings = user.notification_settings || {}; + + // Проверяем настройку ежедневных сводок + if (settings.dailySummary !== false) { + // Планируем сводку лайков + await this.scheduleLikesSummary(user.id); + + // Планируем проверку неактивных матчей раз в неделю + if (settings.reminders !== false) { + const dayOfWeek = new Date().getDay(); + if (dayOfWeek === 1) { // Понедельник + const scheduledAt = this.getPreferredScheduleTime(settings.timePreference || 'evening'); + + await this.scheduleNotification({ + userId: user.id, + type: 'inactive_matches', + data: {}, + scheduledAt + }); + } + } + } + } catch (error) { + console.error(`Error scheduling periodic notifications for user ${user.id}:`, error); + } + } + } catch (error) { + console.error('Error scheduling periodic notifications:', error); + } + } } diff --git a/src/services/notificationService.ts.new b/src/services/notificationService.ts.new new file mode 100644 index 0000000..833c86c --- /dev/null +++ b/src/services/notificationService.ts.new @@ -0,0 +1,1316 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { query, transaction } from '../database/connection'; +import { ProfileService } from './profileService'; +import { v4 as uuidv4 } from 'uuid'; + +export interface NotificationData { + userId: string; + type: 'new_match' | 'new_message' | 'new_like' | 'super_like' | 'match_reminder' | 'inactive_matches' | 'like_summary'; + data: Record; + scheduledAt?: Date; +} + +export interface NotificationSettings { + newMatches: boolean; + newMessages: boolean; + newLikes: boolean; + reminders: boolean; + dailySummary: boolean; + timePreference: 'morning' | 'afternoon' | 'evening' | 'night'; + doNotDisturb: boolean; + doNotDisturbStart?: string; // Формат: "HH:MM" + doNotDisturbEnd?: string; // Формат: "HH:MM" +} + +export class NotificationService { + private bot?: TelegramBot; + private profileService: ProfileService; + + constructor(bot?: TelegramBot) { + this.bot = bot; + this.profileService = new ProfileService(); + + // Создаем таблицу уведомлений, если её еще нет + this.ensureNotificationTablesExist().catch(err => + console.error('Failed to create notification tables:', err) + ); + } + + // Проверка и создание таблиц для уведомлений + private async ensureNotificationTablesExist(): Promise { + try { + // Проверяем существование таблицы notifications + const notificationsExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'notifications' + ) as exists + `); + + if (!notificationsExists.rows[0].exists) { + await query(` + CREATE TABLE notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + console.log('Created notifications table'); + } + + // Проверяем существование таблицы scheduled_notifications + const scheduledExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'scheduled_notifications' + ) as exists + `); + + if (!scheduledExists.rows[0].exists) { + await query(` + CREATE TABLE scheduled_notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + type VARCHAR(50) NOT NULL, + data JSONB, + scheduled_at TIMESTAMP NOT NULL, + processed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + console.log('Created scheduled_notifications table'); + } + + // Проверяем существование таблицы notification_templates + const templatesExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'notification_templates' + ) as exists + `); + + if (!templatesExists.rows[0].exists) { + await query(` + CREATE TABLE notification_templates ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + type VARCHAR(50) NOT NULL UNIQUE, + title TEXT NOT NULL, + message_template TEXT NOT NULL, + button_template JSONB, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ) + `); + console.log('Created notification_templates table'); + + // Заполняем таблицу базовыми шаблонами + await this.populateDefaultTemplates(); + } + + // Проверяем, есть ли колонка notification_settings в таблице users + const settingsColumnExists = await query(` + SELECT EXISTS ( + SELECT FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'notification_settings' + ) as exists + `); + + if (!settingsColumnExists.rows[0].exists) { + await query(` + ALTER TABLE users + ADD COLUMN notification_settings JSONB DEFAULT '{ + "newMatches": true, + "newMessages": true, + "newLikes": true, + "reminders": true, + "dailySummary": true, + "timePreference": "evening", + "doNotDisturb": false + }'::jsonb + `); + console.log('Added notification_settings column to users table'); + } + + } catch (error) { + console.error('Error ensuring notification tables exist:', error); + throw error; + } + } + + // Заполнение таблицы шаблонов уведомлений + private async populateDefaultTemplates(): Promise { + try { + const templates = [ + { + type: 'new_like', + title: 'Новый лайк!', + message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'super_like', + title: 'Супер-лайк!', + message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + { + type: 'new_match', + title: 'Новый матч!', + message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'new_message', + title: 'Новое сообщение!', + message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + button_template: { + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'view_matches' } + ] + ] + } + }, + { + type: 'match_reminder', + title: 'Напоминание о матче', + message_template: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!', + button_template: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }] + ] + } + }, + { + type: 'inactive_matches', + title: 'Неактивные матчи', + message_template: '⏰ У вас {{count}} неактивных матчей!\n\nПродолжите общение, чтобы не упустить интересные знакомства!', + button_template: { + inline_keyboard: [ + [{ text: '📋 Открыть матчи', callback_data: 'view_matches' }], + [{ text: '💕 Смотреть новые анкеты', callback_data: 'start_browsing' }] + ] + } + }, + { + type: 'like_summary', + title: 'Сводка лайков', + message_template: '💖 У вас {{count}} новых лайков!\n\nПосмотрите, кто проявил к вам интерес сегодня!', + button_template: { + inline_keyboard: [ + [{ text: '👀 Посмотреть лайки', callback_data: 'view_likes' }], + [{ text: '💕 Начать знакомиться', callback_data: 'start_browsing' }] + ] + } + } + ]; + + for (const template of templates) { + await query(` + INSERT INTO notification_templates (type, title, message_template, button_template) + VALUES ($1, $2, $3, $4) + ON CONFLICT (type) DO UPDATE + SET title = EXCLUDED.title, + message_template = EXCLUDED.message_template, + button_template = EXCLUDED.button_template, + updated_at = NOW() + `, [ + template.type, + template.title, + template.message_template, + JSON.stringify(template.button_template) + ]); + } + + console.log('Populated notification templates'); + } catch (error) { + console.error('Error populating default templates:', error); + } + } + + // Получить шаблон уведомления из базы данных или использовать встроенный + private async getNotificationTemplate(type: string): Promise<{ + title: string; + messageTemplate: string; + buttonTemplate: any; + }> { + try { + // Попытка получить шаблон из базы данных + const result = await query(` + SELECT title, message_template, button_template + FROM notification_templates + WHERE type = $1 + `, [type]); + + if (result.rows.length > 0) { + return { + title: result.rows[0].title, + messageTemplate: result.rows[0].message_template, + buttonTemplate: result.rows[0].button_template + }; + } + } catch (error: any) { + console.log('Using default template as database is not available:', error.message); + } + + // Если не удалось получить из базы или произошла ошибка, используем встроенные шаблоны + const defaultTemplates: Record = { + 'new_like': { + title: 'Новый лайк!', + messageTemplate: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + buttonTemplate: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + 'super_like': { + title: 'Супер-лайк!', + messageTemplate: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + buttonTemplate: { + inline_keyboard: [ + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }], + [ + { text: '❤️ Лайк в ответ', callback_data: 'like_back:{{userId}}' }, + { text: '⛔️ Пропустить', callback_data: 'dislike_profile:{{userId}}' } + ], + [{ text: '💕 Открыть все лайки', callback_data: 'view_likes' }] + ] + } + }, + 'new_match': { + title: 'Новый матч!', + messageTemplate: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + buttonTemplate: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'view_matches' } + ] + ] + } + }, + 'new_message': { + title: 'Новое сообщение!', + messageTemplate: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + buttonTemplate: { + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_chat:{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'view_matches' } + ] + ] + } + }, + 'match_reminder': { + title: 'Напоминание о матче', + messageTemplate: '💕 У вас есть матч с *{{name}}*, но вы еще не начали общение!\n\nНе упустите шанс познакомиться поближе!', + buttonTemplate: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_chat:{{matchId}}' }], + [{ text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }] + ] + } + } + }; + + return defaultTemplates[type] || { + title: 'Уведомление', + messageTemplate: 'Новое уведомление', + buttonTemplate: { inline_keyboard: [] } + }; + } + + // Применить данные к шаблону + private applyTemplateData(template: string, data: Record): string { + return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => { + return data[key] !== undefined ? data[key] : ''; + }); + } + + // Применить данные к шаблону кнопок + private applyTemplateDataToButtons(buttonTemplate: any, data: Record): any { + const result = JSON.parse(JSON.stringify(buttonTemplate)); // глубокая копия + + // Рекурсивная функция для замены в любой вложенности + const replaceInObject = (obj: any): any => { + if (typeof obj === 'string') { + return this.applyTemplateData(obj, data); + } else if (Array.isArray(obj)) { + return obj.map(item => replaceInObject(item)); + } else if (obj !== null && typeof obj === 'object') { + const newObj: Record = {}; + for (const key in obj) { + newObj[key] = replaceInObject(obj[key]); + } + return newObj; + } + return obj; + }; + + return replaceInObject(result); + } + + // Проверка режима "Не беспокоить" + private async shouldSendNotification(userId: string): Promise { + try { + const settings = await this.getNotificationSettings(userId); + + if (!settings.doNotDisturb) { + return true; // Режим "Не беспокоить" выключен + } + + // Если нет указанного времени, по умолчанию: не беспокоить с 23:00 до 8:00 + const startTime = settings.doNotDisturbStart || '23:00'; + const endTime = settings.doNotDisturbEnd || '08:00'; + + const now = new Date(); + const currentHour = now.getHours(); + const currentMinute = now.getMinutes(); + + const [startHour, startMinute] = startTime.split(':').map(Number); + const [endHour, endMinute] = endTime.split(':').map(Number); + + const currentMinutes = currentHour * 60 + currentMinute; + const startMinutes = startHour * 60 + startMinute; + const endMinutes = endHour * 60 + endMinute; + + // Если время окончания меньше времени начала, значит период охватывает полночь + if (endMinutes < startMinutes) { + // Не отправлять, если текущее время между начальным и конечным временем (например, 23:30-07:00) + return !(currentMinutes >= startMinutes || currentMinutes <= endMinutes); + } else { + // Не отправлять, если текущее время между начальным и конечным временем (например, 13:00-15:00) + return !(currentMinutes >= startMinutes && currentMinutes <= endMinutes); + } + } catch (error) { + console.error('Error checking notification settings:', error); + return true; // В случае ошибки отправляем уведомление + } + } + + // Отправить уведомление о новом лайке + async sendLikeNotification(targetTelegramId: string, likerTelegramId: string, isSuperLike: boolean = false): Promise { + try { + const [targetUser, likerProfile] = await Promise.all([ + this.getUserByTelegramId(targetTelegramId), + this.profileService.getProfileByTelegramId(likerTelegramId) + ]); + + if (!targetUser || !likerProfile) { + console.log(`Couldn't send like notification: user or profile not found`); + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(targetUser.id); + if (!settings.newLikes) { + console.log(`Like notifications disabled for user ${targetUser.id}`); + + // Логируем уведомление для истории + await this.logNotification({ + userId: targetUser.id, + type: isSuperLike ? 'super_like' : 'new_like', + data: { + likerUserId: likerProfile.userId, + likerName: likerProfile.name, + age: likerProfile.age, + city: likerProfile.city + } + }); + + // Запланируем отправку сводки лайков позже + await this.scheduleLikesSummary(targetUser.id); + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(targetUser.id))) { + console.log(`Do not disturb mode active for user ${targetUser.id}`); + + // Логируем уведомление и запланируем отправку позже + await this.logNotification({ + userId: targetUser.id, + type: isSuperLike ? 'super_like' : 'new_like', + data: { + likerUserId: likerProfile.userId, + likerName: likerProfile.name, + age: likerProfile.age, + city: likerProfile.city + } + }); + + // Запланируем отправку сводки лайков позже + await this.scheduleLikesSummary(targetUser.id); + return; + } + + // Получаем шаблон уведомления + const templateType = isSuperLike ? 'super_like' : 'new_like'; + const template = await this.getNotificationTemplate(templateType); + + // Подготовка данных для шаблона + const templateData = { + name: likerProfile.name, + age: likerProfile.age.toString(), + city: likerProfile.city || '', + userId: likerProfile.userId + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + if (this.bot) { + await this.bot.sendMessage(targetUser.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`Sent ${templateType} notification to user ${targetUser.id}`); + } + + // Логируем уведомление + await this.logNotification({ + userId: targetUser.id, + type: templateType, + data: { + likerUserId: likerProfile.userId, + likerName: likerProfile.name, + age: likerProfile.age, + city: likerProfile.city + } + }); + } catch (error) { + console.error('Error sending like notification:', error); + } + } + + // Отправить уведомление о новом матче + async sendMatchNotification(userId: string, matchedUserId: string): Promise { + try { + const [user, matchedProfile] = await Promise.all([ + this.getUserByUserId(userId), + this.profileService.getProfileByUserId(matchedUserId) + ]); + + if (!user || !matchedProfile) { + console.log(`Couldn't send match notification: user or profile not found`); + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.newMatches) { + console.log(`Match notifications disabled for user ${userId}`); + + // Логируем уведомление для истории + await this.logNotification({ + userId, + type: 'new_match', + data: { + matchedUserId, + matchedName: matchedProfile.name, + age: matchedProfile.age, + city: matchedProfile.city + } + }); + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + console.log(`Do not disturb mode active for user ${userId}`); + + // Логируем уведомление + await this.logNotification({ + userId, + type: 'new_match', + data: { + matchedUserId, + matchedName: matchedProfile.name, + age: matchedProfile.age, + city: matchedProfile.city + } + }); + + // Запланируем отправку напоминания о матче позже + await this.scheduleMatchReminder(userId, matchedUserId); + return; + } + + // Получаем матч-ID для перехода в чат + const matchResult = await query(` + SELECT id FROM matches + WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1) + AND is_active = true + `, [userId, matchedUserId]); + + const matchId = matchResult.rows[0]?.id; + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('new_match'); + + // Подготовка данных для шаблона + const templateData = { + name: matchedProfile.name, + age: matchedProfile.age.toString(), + city: matchedProfile.city || '', + userId: matchedProfile.userId, + matchId: matchId || '' + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + if (this.bot) { + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`Sent new_match notification to user ${userId}`); + } + + // Логируем уведомление + await this.logNotification({ + userId, + type: 'new_match', + data: { + matchedUserId, + matchedName: matchedProfile.name, + age: matchedProfile.age, + city: matchedProfile.city, + matchId + } + }); + + // Если это новый матч, запланируем напоминание через 24 часа, если пользователь не начнет общение + if (matchId) { + const reminderDate = new Date(); + reminderDate.setHours(reminderDate.getHours() + 24); + + await this.scheduleNotification({ + userId, + type: 'match_reminder', + data: { + matchedUserId, + matchId + }, + scheduledAt: reminderDate + }); + } + } catch (error) { + console.error('Error sending match notification:', error); + } + } + + // Отправить уведомление о новом сообщении + async sendMessageNotification(receiverId: string, senderId: string, messageContent: string, matchId?: string): Promise { + try { + const [receiver, senderProfile] = await Promise.all([ + this.getUserByUserId(receiverId), + this.profileService.getProfileByUserId(senderId) + ]); + + if (!receiver || !senderProfile) { + console.log(`Couldn't send message notification: user or profile not found`); + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(receiverId); + if (!settings.newMessages) { + console.log(`Message notifications disabled for user ${receiverId}`); + return; + } + + // Проверяем, не в чате ли пользователь сейчас + const isUserActive = await this.isUserActiveInChat(receiverId, senderId); + if (isUserActive) { + console.log(`User ${receiverId} is active in chat with ${senderId}, skipping notification`); + return; // Не отправляем уведомление, если пользователь активен в чате + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(receiverId))) { + console.log(`Do not disturb mode active for user ${receiverId}`); + return; + } + + // Если matchId не передан, пытаемся его получить + let actualMatchId = matchId; + if (!actualMatchId) { + const matchResult = await query(` + SELECT id FROM matches + WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1) + AND is_active = true + `, [receiverId, senderId]); + + actualMatchId = matchResult.rows[0]?.id; + } + + const truncatedMessage = messageContent.length > 50 + ? messageContent.substring(0, 50) + '...' + : messageContent; + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('new_message'); + + // Подготовка данных для шаблона + const templateData = { + name: senderProfile.name, + message: truncatedMessage, + userId: senderProfile.userId, + matchId: actualMatchId || '' + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + if (this.bot) { + await this.bot.sendMessage(receiver.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + console.log(`Sent new_message notification to user ${receiverId}`); + } + + // Логируем уведомление + await this.logNotification({ + userId: receiverId, + type: 'new_message', + data: { + senderId, + senderName: senderProfile.name, + messageContent: truncatedMessage, + matchId: actualMatchId + } + }); + } catch (error) { + console.error('Error sending message notification:', error); + } + } + + // Отправить напоминание о матче без сообщений + async sendMatchReminder(userId: string, matchedUserId: string, matchId: string): Promise { + try { + const [user, matchedProfile] = await Promise.all([ + this.getUserByUserId(userId), + this.profileService.getProfileByUserId(matchedUserId) + ]); + + if (!user || !matchedProfile || !this.bot) { + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.reminders) { + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + return; + } + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('match_reminder'); + + // Подготовка данных для шаблона + const templateData = { + name: matchedProfile.name, + userId: matchedProfile.userId, + matchId + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + + console.log(`Sent match reminder to user ${userId} about match with ${matchedUserId}`); + } catch (error) { + console.error('Error sending match reminder:', error); + } + } + + // Отправить сводку по неактивным матчам + async sendInactiveMatchesSummary(userId: string, count: number): Promise { + try { + const user = await this.getUserByUserId(userId); + if (!user || !this.bot || count === 0) { + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.reminders) { + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + return; + } + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('inactive_matches'); + + // Подготовка данных для шаблона + const templateData = { + count: count.toString() + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + + console.log(`Sent inactive matches summary to user ${userId}, count: ${count}`); + } catch (error) { + console.error('Error sending inactive matches summary:', error); + } + } + + // Отправить сводку полученных лайков + async sendLikesSummary(userId: string): Promise { + try { + // Получаем количество непрочитанных лайков + const result = await query(` + SELECT COUNT(*) as count + FROM notifications + WHERE user_id = $1 AND is_read = false AND type IN ('new_like', 'super_like') + AND created_at > NOW() - INTERVAL '24 hours' + `, [userId]); + + const count = parseInt(result.rows[0].count); + if (count === 0) { + return; // Нет новых лайков для отображения + } + + const user = await this.getUserByUserId(userId); + if (!user || !this.bot) { + return; + } + + // Проверяем настройки уведомлений пользователя + const settings = await this.getNotificationSettings(userId); + if (!settings.dailySummary) { + return; + } + + // Проверяем режим "Не беспокоить" + if (!(await this.shouldSendNotification(userId))) { + return; + } + + // Получаем шаблон уведомления + const template = await this.getNotificationTemplate('like_summary'); + + // Подготовка данных для шаблона + const templateData = { + count: count.toString() + }; + + // Применяем данные к шаблону сообщения + const message = this.applyTemplateData(template.messageTemplate, templateData); + + // Применяем данные к шаблону кнопок + const keyboard = this.applyTemplateDataToButtons(template.buttonTemplate, templateData); + + // Отправляем уведомление + await this.bot.sendMessage(user.telegram_id, message, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + + // Отмечаем уведомления как прочитанные + await query(` + UPDATE notifications + SET is_read = true + WHERE user_id = $1 AND is_read = false AND type IN ('new_like', 'super_like') + AND created_at > NOW() - INTERVAL '24 hours' + `, [userId]); + + console.log(`Sent likes summary to user ${userId}, count: ${count}`); + } catch (error) { + console.error('Error sending likes summary:', error); + } + } + + // Запланировать отправку сводки лайков + async scheduleLikesSummary(userId: string): Promise { + try { + // Определить предпочтительное время для отправки + const settings = await this.getNotificationSettings(userId); + const scheduledAt = this.getPreferredScheduleTime(settings.timePreference); + + // Проверяем, есть ли уже запланированная сводка лайков + const existingResult = await query(` + SELECT id FROM scheduled_notifications + WHERE user_id = $1 AND type = 'like_summary' AND processed = false + AND scheduled_at > NOW() + `, [userId]); + + if (existingResult.rows.length > 0) { + console.log(`Like summary already scheduled for user ${userId}`); + return; // Уже есть запланированная сводка + } + + // Запланировать отправку сводки + await query(` + INSERT INTO scheduled_notifications (id, user_id, type, scheduled_at) + VALUES ($1, $2, $3, $4) + `, [uuidv4(), userId, 'like_summary', scheduledAt]); + + console.log(`Scheduled likes summary for user ${userId} at ${scheduledAt}`); + } catch (error) { + console.error('Error scheduling likes summary:', error); + } + } + + // Запланировать напоминание о матче + async scheduleMatchReminder(userId: string, matchedUserId: string): Promise { + try { + // Получаем матч-ID для перехода в чат + const matchResult = await query(` + SELECT id FROM matches + WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1) + AND is_active = true + `, [userId, matchedUserId]); + + const matchId = matchResult.rows[0]?.id; + if (!matchId) { + console.log(`No active match found between users ${userId} and ${matchedUserId}`); + return; + } + + // Определить предпочтительное время для отправки + const settings = await this.getNotificationSettings(userId); + const scheduledAt = this.getPreferredScheduleTime(settings.timePreference); + + // Запланировать напоминание + await query(` + INSERT INTO scheduled_notifications (id, user_id, type, data, scheduled_at) + VALUES ($1, $2, $3, $4, $5) + `, [ + uuidv4(), + userId, + 'match_reminder', + JSON.stringify({ matchId, matchedUserId }), + scheduledAt + ]); + + console.log(`Scheduled match reminder for user ${userId} about match with ${matchedUserId} at ${scheduledAt}`); + } catch (error) { + console.error('Error scheduling match reminder:', error); + } + } + + // Получить предпочтительное время для отправки уведомлений + private getPreferredScheduleTime(preference: string): Date { + const now = new Date(); + const scheduledAt = new Date(); + + // Если текущее время после полуночи, планируем на сегодня + // Иначе планируем на следующий день + if (now.getHours() < 12) { + // Сегодня + switch (preference) { + case 'morning': + scheduledAt.setHours(9, 0, 0, 0); + break; + case 'afternoon': + scheduledAt.setHours(13, 0, 0, 0); + break; + case 'evening': + scheduledAt.setHours(19, 0, 0, 0); + break; + case 'night': + scheduledAt.setHours(22, 0, 0, 0); + break; + default: + scheduledAt.setHours(19, 0, 0, 0); // По умолчанию вечер + } + } else { + // Завтра + scheduledAt.setDate(scheduledAt.getDate() + 1); + switch (preference) { + case 'morning': + scheduledAt.setHours(9, 0, 0, 0); + break; + case 'afternoon': + scheduledAt.setHours(13, 0, 0, 0); + break; + case 'evening': + scheduledAt.setHours(19, 0, 0, 0); + break; + case 'night': + scheduledAt.setHours(22, 0, 0, 0); + break; + default: + scheduledAt.setHours(9, 0, 0, 0); // По умолчанию утро + } + } + + // Если запланированное время уже прошло, добавляем еще один день + if (scheduledAt <= now) { + scheduledAt.setDate(scheduledAt.getDate() + 1); + } + + return scheduledAt; + } + + // Запланировать уведомление + async scheduleNotification(notificationData: NotificationData): Promise { + try { + if (!notificationData.scheduledAt) { + notificationData.scheduledAt = new Date(); + } + + await query(` + INSERT INTO scheduled_notifications (id, user_id, type, data, scheduled_at) + VALUES ($1, $2, $3, $4, $5) + `, [ + uuidv4(), + notificationData.userId, + notificationData.type, + JSON.stringify(notificationData.data), + notificationData.scheduledAt + ]); + + console.log(`Scheduled ${notificationData.type} notification for user ${notificationData.userId} at ${notificationData.scheduledAt}`); + } catch (error) { + console.error('Error scheduling notification:', error); + } + } + + // Логирование уведомлений + private async logNotification(notificationData: NotificationData): Promise { + try { + await query(` + INSERT INTO notifications (id, user_id, type, data, created_at) + VALUES ($1, $2, $3, $4, $5) + `, [ + uuidv4(), + notificationData.userId, + notificationData.type, + JSON.stringify(notificationData.data), + new Date() + ]); + + console.log(`Logged ${notificationData.type} notification for user ${notificationData.userId}`); + } catch (error) { + console.error('Error logging notification:', error); + } + } + + // Получить пользователя по ID + private async getUserByUserId(userId: string): Promise { + try { + const result = await query( + 'SELECT * FROM users WHERE id = $1', + [userId] + ); + return result.rows[0] || null; + } catch (error) { + console.error('Error getting user:', error); + return null; + } + } + + // Получить пользователя по Telegram ID + private async getUserByTelegramId(telegramId: string): Promise { + try { + const result = await query( + 'SELECT * FROM users WHERE telegram_id = $1', + [parseInt(telegramId)] + ); + return result.rows[0] || null; + } catch (error) { + console.error('Error getting user by telegram ID:', error); + return null; + } + } + + // Проверить, активен ли пользователь в чате + private async isUserActiveInChat(userId: string, chatWithUserId: string): Promise { + try { + // Проверяем последнее сообщение пользователя в чате + const result = await query(` + SELECT m.created_at + FROM messages m + JOIN matches mt ON m.match_id = mt.id + WHERE (mt.user_id_1 = $1 OR mt.user_id_2 = $1) + AND (mt.user_id_1 = $2 OR mt.user_id_2 = $2) + AND m.sender_id = $1 + ORDER BY m.created_at DESC + LIMIT 1 + `, [userId, chatWithUserId]); + + if (result.rows.length === 0) { + return false; // Нет сообщений - не активен + } + + const lastMessageTime = new Date(result.rows[0].created_at); + const now = new Date(); + const hoursSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / (1000 * 60 * 60); + + // Считаем активным если последнее сообщение было менее 10 минут назад + return hoursSinceLastMessage < (10 / 60); + } catch (error) { + console.error('Error checking user activity:', error); + return false; + } + } + + // Получить настройки уведомлений пользователя + async getNotificationSettings(userId: string): Promise { + try { + const result = await query( + 'SELECT notification_settings FROM users WHERE id = $1', + [userId] + ); + + if (result.rows.length === 0) { + return { + newMatches: true, + newMessages: true, + newLikes: true, + reminders: true, + dailySummary: true, + timePreference: 'evening', + doNotDisturb: false + }; + } + + const settings = result.rows[0].notification_settings || {}; + + // Возвращаем настройки с дефолтными значениями для отсутствующих свойств + return { + newMatches: settings.newMatches !== undefined ? settings.newMatches : true, + newMessages: settings.newMessages !== undefined ? settings.newMessages : true, + newLikes: settings.newLikes !== undefined ? settings.newLikes : true, + reminders: settings.reminders !== undefined ? settings.reminders : true, + dailySummary: settings.dailySummary !== undefined ? settings.dailySummary : true, + timePreference: settings.timePreference || 'evening', + doNotDisturb: settings.doNotDisturb || false, + doNotDisturbStart: settings.doNotDisturbStart, + doNotDisturbEnd: settings.doNotDisturbEnd + }; + } catch (error) { + console.error('Error getting notification settings:', error); + return { + newMatches: true, + newMessages: true, + newLikes: true, + reminders: true, + dailySummary: true, + timePreference: 'evening', + doNotDisturb: false + }; + } + } + + // Обновить настройки уведомлений + async updateNotificationSettings(userId: string, settings: Partial): Promise { + try { + // Получаем текущие настройки + const currentSettings = await this.getNotificationSettings(userId); + + // Обновляем только переданные поля + const updatedSettings = { + ...currentSettings, + ...settings + }; + + await query( + 'UPDATE users SET notification_settings = $1 WHERE id = $2', + [JSON.stringify(updatedSettings), userId] + ); + + console.log(`Updated notification settings for user ${userId}`); + } catch (error) { + console.error('Error updating notification settings:', error); + } + } + + // Планировщик уведомлений (вызывается периодически) + async processScheduledNotifications(): Promise { + try { + // Получаем запланированные уведомления + const result = await query(` + SELECT * FROM scheduled_notifications + WHERE scheduled_at <= $1 AND processed = false + ORDER BY scheduled_at ASC + LIMIT 100 + `, [new Date()]); + + console.log(`Processing ${result.rows.length} scheduled notifications`); + + for (const notification of result.rows) { + try { + const data = notification.data || {}; + + switch (notification.type) { + case 'match_reminder': + if (data.matchId && data.matchedUserId) { + // Проверяем, что матч всё еще активен и нет сообщений + const matchCheck = await query(` + SELECT m.id + FROM matches m + LEFT JOIN messages msg ON msg.match_id = m.id + WHERE m.id = $1 AND m.is_active = true + AND msg.id IS NULL + `, [data.matchId]); + + if (matchCheck.rows.length > 0) { + await this.sendMatchReminder(notification.user_id, data.matchedUserId, data.matchId); + } + } + break; + case 'like_summary': + await this.sendLikesSummary(notification.user_id); + break; + case 'inactive_matches': + // Получаем количество неактивных матчей + const matchesResult = await query(` + SELECT COUNT(*) as count + FROM matches m + LEFT JOIN messages msg ON msg.match_id = m.id + WHERE (m.user_id_1 = $1 OR m.user_id_2 = $1) + AND m.is_active = true + AND msg.id IS NULL + AND m.created_at < NOW() - INTERVAL '3 days' + `, [notification.user_id]); + + const count = parseInt(matchesResult.rows[0].count); + if (count > 0) { + await this.sendInactiveMatchesSummary(notification.user_id, count); + } + break; + } + + // Отмечаем как обработанное + await query( + 'UPDATE scheduled_notifications SET processed = true WHERE id = $1', + [notification.id] + ); + } catch (error) { + console.error(`Error processing notification ${notification.id}:`, error); + } + } + } catch (error) { + console.error('Error processing scheduled notifications:', error); + } + } + + // Планирование периодических уведомлений для всех пользователей + async schedulePeriodicNotifications(): Promise { + try { + // Получаем список активных пользователей + const usersResult = await query(` + SELECT u.id, u.notification_settings + FROM users u + JOIN profiles p ON u.id = p.user_id + WHERE p.is_visible = true + AND u.created_at < NOW() - INTERVAL '1 day' + `); + + for (const user of usersResult.rows) { + try { + const settings = user.notification_settings || {}; + + // Проверяем настройку ежедневных сводок + if (settings.dailySummary !== false) { + // Планируем сводку лайков + await this.scheduleLikesSummary(user.id); + + // Планируем проверку неактивных матчей раз в неделю + if (settings.reminders !== false) { + const dayOfWeek = new Date().getDay(); + if (dayOfWeek === 1) { // Понедельник + const scheduledAt = this.getPreferredScheduleTime(settings.timePreference || 'evening'); + + await this.scheduleNotification({ + userId: user.id, + type: 'inactive_matches', + data: {}, + scheduledAt + }); + } + } + } + } catch (error) { + console.error(`Error scheduling periodic notifications for user ${user.id}:`, error); + } + } + } catch (error) { + console.error('Error scheduling periodic notifications:', error); + } + } +} diff --git a/src/services/vipService.ts b/src/services/vipService.ts index 397723e..3d209a4 100644 --- a/src/services/vipService.ts +++ b/src/services/vipService.ts @@ -24,9 +24,9 @@ export class VipService { // Проверить премиум статус пользователя async checkPremiumStatus(telegramId: string): Promise { try { - // Проверяем существование пользователя + // Проверяем существование пользователя и получаем его премиум статус const result = await query(` - SELECT id + SELECT id, premium FROM users WHERE telegram_id = $1 `, [telegramId]); @@ -35,12 +35,13 @@ export class VipService { throw new BotError('User not found', 'USER_NOT_FOUND', 404); } - // Временно возвращаем false для всех пользователей, так как колонки premium нет - // В будущем, когда колонки будут добавлены, этот код нужно будет заменить обратно + // Получаем актуальное значение премиум статуса из базы данных + const isPremium = result.rows[0].premium || false; + return { - isPremium: false, - expiresAt: undefined, - daysLeft: undefined + isPremium: isPremium, + expiresAt: undefined, // Пока не используем дату истечения + daysLeft: undefined // Пока не используем количество дней }; } catch (error) { console.error('Error checking premium status:', error); @@ -51,9 +52,17 @@ export class VipService { // Добавить премиум статус async addPremium(telegramId: string, durationDays: number = 30): Promise { try { - // Временно заглушка, так как колонок premium и premium_expires_at нет - console.log(`[VIP] Попытка добавить премиум для ${telegramId} на ${durationDays} дней`); - // TODO: Добавить колонки premium и premium_expires_at в таблицу users + console.log(`[VIP] Добавление премиум для ${telegramId} на ${durationDays} дней`); + + // Обновляем статус premium в базе данных + await query(` + UPDATE users + SET premium = true + WHERE telegram_id = $1 + RETURNING id, telegram_id, premium + `, [telegramId]); + + console.log(`[VIP] Премиум успешно добавлен для пользователя ${telegramId}`); } catch (error) { console.error('Error adding premium:', error); throw error; @@ -63,9 +72,17 @@ export class VipService { // Удалить премиум статус async removePremium(telegramId: string): Promise { try { - // Временно заглушка, так как колонок premium и premium_expires_at нет - console.log(`[VIP] Попытка удалить премиум для ${telegramId}`); - // TODO: Добавить колонки premium и premium_expires_at в таблицу users + console.log(`[VIP] Удаление премиум для ${telegramId}`); + + // Обновляем статус premium в базе данных + await query(` + UPDATE users + SET premium = false + WHERE telegram_id = $1 + RETURNING id, telegram_id, premium + `, [telegramId]); + + console.log(`[VIP] Премиум успешно удален для пользователя ${telegramId}`); } catch (error) { console.error('Error removing premium:', error); throw error;