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); } } }