Files
tg_tinder_bot/src/services/notificationService.ts
2025-09-18 13:46:35 +09:00

1317 lines
60 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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