1317 lines
60 KiB
TypeScript
1317 lines
60 KiB
TypeScript
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<string, any>;
|
||
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<void> {
|
||
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<void> {
|
||
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<string, any> = {
|
||
'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, any>): string {
|
||
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
|
||
return data[key] !== undefined ? data[key] : '';
|
||
});
|
||
}
|
||
|
||
// Применить данные к шаблону кнопок
|
||
private applyTemplateDataToButtons(buttonTemplate: any, data: Record<string, any>): 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<string, any> = {};
|
||
for (const key in obj) {
|
||
newObj[key] = replaceInObject(obj[key]);
|
||
}
|
||
return newObj;
|
||
}
|
||
return obj;
|
||
};
|
||
|
||
return replaceInObject(result);
|
||
}
|
||
|
||
// Проверка режима "Не беспокоить"
|
||
private async shouldSendNotification(userId: string): Promise<boolean> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<any> {
|
||
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<any> {
|
||
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<boolean> {
|
||
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 secondsSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / 1000;
|
||
|
||
// Считаем активным если последнее сообщение было менее 30 секунд назад
|
||
return secondsSinceLastMessage < 30;
|
||
} catch (error) {
|
||
console.error('Error checking user activity:', error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
// Получить настройки уведомлений пользователя
|
||
async getNotificationSettings(userId: string): Promise<NotificationSettings> {
|
||
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<NotificationSettings>): Promise<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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);
|
||
}
|
||
}
|
||
}
|