diff --git a/docker-compose.yml b/docker-compose.yml index 8ddb875..760e31c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,12 +9,12 @@ services: - db environment: - NODE_ENV=production - - DB_HOST=db - - DB_PORT=5432 - - DB_NAME=telegram_tinder_bot - - DB_USERNAME=postgres - - DB_PASSWORD=${DB_PASSWORD} - - TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN} + - DB_HOST={DB_HOST} + - DB_PORT={DB_PORT} + - DB_NAME={DB_NAME} + - DB_USERNAME={DB_USERNAME} + - DB_PASSWORD={DB_PASSWORD} + - TELEGRAM_BOT_TOKEN={TELEGRAM_BOT_TOKEN} volumes: - ./uploads:/app/uploads networks: diff --git a/init-notifications-db.js b/init-notifications-db.js new file mode 100644 index 0000000..bcd29d0 --- /dev/null +++ b/init-notifications-db.js @@ -0,0 +1,174 @@ +require('dotenv').config(); +const { Pool } = require('pg'); +const { v4: uuidv4 } = require('uuid'); + +// Функция для запуска скрипта +async function initializeDatabase() { + console.log('Starting database initialization script...'); + + const pool = new Pool({ + host: 'localhost', // Используем localhost + port: 5432, // Используем стандартный порт 5432 + database: 'telegram_tinder_bot', + user: 'postgres', + password: '', + max: 5, + connectionTimeoutMillis: 5000 + }); + + console.log('DB Connection Details:'); + console.log('- Host: localhost'); + console.log('- Port: 5432'); + console.log('- Database: telegram_tinder_bot'); + console.log('- User: postgres'); + + try { + // Проверяем подключение + console.log('Testing connection...'); + const client = await pool.connect(); + + try { + console.log('✅ Connected to database successfully!'); + + // 1. Создаем расширение для генерации UUID если его нет + console.log('Creating UUID extension...'); + await client.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`); + + // 2. Создаем таблицу для шаблонов уведомлений + console.log('Creating notification_templates table...'); + await client.query(` + CREATE TABLE IF NOT EXISTS 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 NOT NULL, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + + // 3. Вставляем базовые шаблоны + console.log('Inserting notification templates...'); + + const templates = [ + { + id: uuidv4(), + type: 'new_like', + title: 'Новый лайк!', + message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + button_template: JSON.stringify({ + 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' }] + ] + }) + }, + { + id: uuidv4(), + type: 'super_like', + title: 'Супер-лайк!', + message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + button_template: JSON.stringify({ + 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' }] + ] + }) + }, + { + id: uuidv4(), + type: 'new_match', + title: 'Новый матч!', + message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + button_template: JSON.stringify({ + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_native_chat_{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'native_chats' } + ] + ] + }) + }, + { + id: uuidv4(), + type: 'new_message', + title: 'Новое сообщение!', + message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + button_template: JSON.stringify({ + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_native_chat_{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'native_chats' } + ] + ] + }) + } + ]; + + // Вставляем шаблоны с проверкой на конфликты + for (const template of templates) { + await client.query(` + INSERT INTO notification_templates + (id, type, title, message_template, button_template, created_at) + VALUES ($1, $2, $3, $4, $5, NOW()) + ON CONFLICT (type) DO UPDATE + SET title = $3, + message_template = $4, + button_template = $5 + `, [template.id, template.type, template.title, template.message_template, template.button_template]); + } + + console.log('✅ Notification templates created/updated successfully'); + + // 4. Создаем таблицу для хранения логов уведомлений если её нет + console.log('Creating notifications table...'); + await client.query(` + CREATE TABLE IF NOT EXISTS notifications ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id UUID NOT NULL, + type VARCHAR(50) NOT NULL, + data JSONB NOT NULL, + is_read BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + + console.log('✅ Notifications table created successfully'); + + // 5. Проверяем, что таблицы созданы + const tablesResult = await client.query(` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name IN ('notification_templates', 'notifications') + `); + + console.log('Created tables:'); + tablesResult.rows.forEach(row => { + console.log(`- ${row.table_name}`); + }); + + } finally { + client.release(); + await pool.end(); + } + + console.log('✅ Database initialization completed successfully'); + + } catch (error) { + console.error('❌ Database initialization error:', error); + } +} + +// Запускаем скрипт +initializeDatabase(); diff --git a/migrations/1758156426793_add-processed-column-to-notifications.js b/migrations/1758156426793_add-processed-column-to-notifications.js new file mode 100644 index 0000000..497729c --- /dev/null +++ b/migrations/1758156426793_add-processed-column-to-notifications.js @@ -0,0 +1,52 @@ +/* eslint-disable camelcase */ + +exports.shorthands = undefined; + +exports.up = pgm => { + // Проверяем существование таблицы scheduled_notifications + pgm.sql(` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_name = 'scheduled_notifications' + ) THEN + -- Проверяем, нет ли уже столбца processed + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'scheduled_notifications' AND column_name = 'processed' + ) THEN + -- Добавляем столбец processed + ALTER TABLE scheduled_notifications ADD COLUMN processed BOOLEAN DEFAULT FALSE; + END IF; + ELSE + -- Создаем таблицу, если она не существует + CREATE TABLE scheduled_notifications ( + id UUID PRIMARY KEY, + user_id UUID REFERENCES users(id), + type VARCHAR(50) NOT NULL, + data JSONB, + scheduled_at TIMESTAMP NOT NULL, + processed BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ); + END IF; + END + $$; + `); +}; + +exports.down = pgm => { + pgm.sql(` + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'scheduled_notifications' AND column_name = 'processed' + ) THEN + ALTER TABLE scheduled_notifications DROP COLUMN processed; + END IF; + END + $$; + `); +}; diff --git a/package.json b/package.json index 3579cb5..ef721e7 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "premium:direct": "ts-node src/scripts/setPremiumDirectConnect.ts", "db:info": "ts-node src/scripts/getDatabaseInfo.ts", "db:test-data": "ts-node src/scripts/createTestData.ts", + "enhance-notifications": "ts-node src/scripts/enhanceNotifications.ts", "update": "bash ./bin/update.sh", "update:win": ".\\bin\\update.bat", "start:sh": "bash ./bin/start_bot.sh" diff --git a/src/handlers/callbackHandlers.ts b/src/handlers/callbackHandlers.ts index 26a9e37..0df3dd6 100644 --- a/src/handlers/callbackHandlers.ts +++ b/src/handlers/callbackHandlers.ts @@ -10,6 +10,7 @@ import { VipController } from '../controllers/vipController'; import { VipService } from '../services/vipService'; import { TranslationController } from '../controllers/translationController'; import { t } from '../services/localizationService'; +import { LikeBackHandler } from './likeBackHandler'; export class CallbackHandlers { private bot: TelegramBot; @@ -22,6 +23,7 @@ export class CallbackHandlers { private vipController: VipController; private vipService: VipService; private translationController: TranslationController; + private likeBackHandler: LikeBackHandler; constructor(bot: TelegramBot, messageHandlers: MessageHandlers) { this.bot = bot; @@ -34,6 +36,7 @@ export class CallbackHandlers { this.vipController = new VipController(bot); this.vipService = new VipService(); this.translationController = new TranslationController(); + this.likeBackHandler = new LikeBackHandler(bot); } register(): void { @@ -167,6 +170,12 @@ export class CallbackHandlers { await this.handleMorePhotos(chatId, telegramId, targetUserId); } + // Обработка лайков и ответных лайков из уведомлений + else if (data.startsWith('like_back:')) { + const targetUserId = data.replace('like_back:', ''); + await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId); + } + // Матчи и чаты else if (data === 'view_matches') { await this.handleViewMatches(chatId, telegramId); @@ -385,9 +394,15 @@ export class CallbackHandlers { await this.bot.sendMessage(chatId, '👍 Лайк отправлен!'); await this.showNextCandidate(chatId, telegramId); } - } catch (error) { - await this.bot.sendMessage(chatId, '❌ Ошибка при отправке лайка'); - console.error('Like error:', error); + } catch (error: any) { + // Проверяем, связана ли ошибка с уже существующим свайпом + if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') { + await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее'); + await this.showNextCandidate(chatId, telegramId); + } else { + await this.bot.sendMessage(chatId, '❌ Ошибка при отправке лайка'); + console.error('Like error:', error); + } } } @@ -402,9 +417,15 @@ export class CallbackHandlers { await this.matchingService.performSwipe(telegramId, targetTelegramId, 'pass'); await this.showNextCandidate(chatId, telegramId); - } catch (error) { - await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка'); - console.error('Dislike error:', error); + } catch (error: any) { + // Проверяем, связана ли ошибка с уже существующим свайпом + if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') { + await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее'); + await this.showNextCandidate(chatId, telegramId); + } else { + await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка'); + console.error('Dislike error:', error); + } } } @@ -443,9 +464,73 @@ export class CallbackHandlers { await this.bot.sendMessage(chatId, '💖 Супер лайк отправлен!'); await this.showNextCandidate(chatId, telegramId); } + } catch (error: any) { + // Проверяем, связана ли ошибка с уже существующим свайпом + if (error.message === 'Already swiped this profile' || error.code === 'ALREADY_SWIPED') { + await this.bot.sendMessage(chatId, '❓ Вы уже оценивали этот профиль ранее'); + await this.showNextCandidate(chatId, telegramId); + } else { + await this.bot.sendMessage(chatId, '❌ Ошибка при отправке супер лайка'); + console.error('Superlike error:', error); + } + } + } + + // Обработка обратного лайка из уведомления + async handleLikeBack(chatId: number, telegramId: string, targetUserId: string): Promise { + try { + // Получаем информацию о пользователях + const targetProfile = await this.profileService.getProfileByUserId(targetUserId); + + if (!targetProfile) { + await this.bot.sendMessage(chatId, '❌ Не удалось найти профиль'); + return; + } + + // Получаем telegram ID целевого пользователя для свайпа + const targetTelegramId = await this.profileService.getTelegramIdByUserId(targetUserId); + if (!targetTelegramId) { + await this.bot.sendMessage(chatId, '❌ Не удалось найти пользователя'); + return; + } + + // Выполняем свайп + const result = await this.matchingService.performSwipe(telegramId, targetTelegramId, 'like'); + + if (result.isMatch) { + // Это матч! + await this.bot.sendMessage( + chatId, + '🎉 *Поздравляем! Это взаимно!*\n\n' + + `Вы и *${targetProfile.name}* понравились друг другу!\n` + + 'Теперь вы можете начать общение.', + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: `start_chat:${targetUserId}` }], + [{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${targetUserId}` }] + ] + } + } + ); + } else { + await this.bot.sendMessage( + chatId, + '❤️ Вам понравился профиль ' + targetProfile.name + '!\n\n' + + 'Если вы также понравитесь этому пользователю, будет создан матч.', + { + reply_markup: { + inline_keyboard: [ + [{ text: '🔍 Продолжить поиск', callback_data: 'start_browsing' }] + ] + } + } + ); + } } catch (error) { - await this.bot.sendMessage(chatId, '❌ Ошибка при отправке супер лайка'); - console.error('Superlike error:', error); + console.error('Error in handleLikeBack:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при обработке лайка'); } } @@ -476,9 +561,28 @@ export class CallbackHandlers { return; } - for (let i = 1; i < targetProfile.photos.length; i++) { - const photoFileId = targetProfile.photos[i]; - await this.bot.sendPhoto(chatId, photoFileId); + // Отправляем фотографии в виде медиа-группы (коллажа) + // Создаем массив объектов медиа для группового отправления + const mediaGroup = targetProfile.photos.slice(1).map((photoFileId, index) => ({ + type: 'photo' as const, + media: photoFileId, + caption: index === 0 ? `📸 Дополнительные фото ${targetProfile.name}` : undefined + })); + + try { + // Отправляем все фото одним сообщением (медиа-группой) + await this.bot.sendMediaGroup(chatId, mediaGroup); + } catch (error) { + console.error('Error sending media group:', error); + + // Если не получилось отправить медиа-группой, отправляем по одной + for (let i = 1; i < targetProfile.photos.length; i++) { + try { + await this.bot.sendPhoto(chatId, targetProfile.photos[i]); + } catch (photoError) { + console.error(`Error sending photo ${i}:`, photoError); + } + } } const keyboard: InlineKeyboardMarkup = { @@ -807,6 +911,7 @@ export class CallbackHandlers { // Вспомогательные методы async showProfile(chatId: number, profile: Profile, isOwner: boolean = false, viewerId?: string): Promise { + const hasMultiplePhotos = profile.photos.length > 1; const mainPhotoFileId = profile.photos[0]; // Первое фото - главное let profileText = '👤 ' + profile.name + ', ' + profile.age + '\n'; @@ -876,20 +981,43 @@ export class CallbackHandlers { if (hasValidPhoto) { try { - await this.bot.sendPhoto(chatId, mainPhotoFileId, { - caption: profileText, - reply_markup: keyboard - }); + if (hasMultiplePhotos) { + // Если есть несколько фото, отправляем их как медиа-группу (коллаж) + const mediaGroup = profile.photos.map((photoFileId, index) => ({ + type: 'photo' as const, + media: photoFileId, + caption: index === 0 ? profileText : undefined, + parse_mode: index === 0 ? 'Markdown' as const : undefined + })); + + // Сначала отправляем медиа-группу + await this.bot.sendMediaGroup(chatId, mediaGroup); + + // Затем отправляем отдельное сообщение с кнопками + await this.bot.sendMessage(chatId, '📸 Выберите действие:', { + reply_markup: keyboard + }); + } else { + // Если только одно фото, отправляем его с текстом + await this.bot.sendPhoto(chatId, mainPhotoFileId, { + caption: profileText, + reply_markup: keyboard, + parse_mode: 'Markdown' + }); + } } catch (error) { + console.error('Error sending profile photos:', error); // Если не удалось отправить фото, отправляем текст await this.bot.sendMessage(chatId, '🖼 Фото недоступно\n\n' + profileText, { - reply_markup: keyboard + reply_markup: keyboard, + parse_mode: 'Markdown' }); } } else { // Отправляем как текстовое сообщение await this.bot.sendMessage(chatId, profileText, { - reply_markup: keyboard + reply_markup: keyboard, + parse_mode: 'Markdown' }); } } diff --git a/src/handlers/enhancedChatHandlers.ts b/src/handlers/enhancedChatHandlers.ts index 59a353c..72726b8 100644 --- a/src/handlers/enhancedChatHandlers.ts +++ b/src/handlers/enhancedChatHandlers.ts @@ -168,25 +168,24 @@ export class EnhancedChatHandlers { // ===== СИСТЕМА УВЕДОМЛЕНИЙ ===== - // Отправить уведомление о новом сообщении + // Отправить уведомление о новом сообщении - теперь используем NotificationService async sendMessageNotification(receiverTelegramId: string, senderName: string, messagePreview: string, matchId: string): Promise { try { - const receiverChatId = parseInt(receiverTelegramId); + // Получаем идентификаторы пользователей для использования в NotificationService + const receiverUserId = await this.profileService.getUserIdByTelegramId(receiverTelegramId); + const sender = await this.chatService.getMatchInfo(matchId, receiverTelegramId); - await this.bot.sendMessage( - receiverChatId, - `💌 *Новое сообщение от ${senderName}*\n\n` + - `"${this.escapeMarkdown(messagePreview)}"\n\n` + - '👆 Нажмите "Открыть чат" для ответа', - { - parse_mode: 'Markdown', - reply_markup: { - inline_keyboard: [ - [{ text: '💬 Открыть чат', callback_data: `open_native_chat_${matchId}` }], - [{ text: '📱 Все чаты', callback_data: 'native_chats' }] - ] - } - } + if (!receiverUserId || !sender?.otherUserId) { + console.error('Failed to get user IDs for notification'); + return; + } + + // Используем сервис уведомлений для отправки более красивого уведомления + await this.notificationService.sendMessageNotification( + receiverUserId, + sender.otherUserId, + messagePreview, + matchId ); } catch (error) { console.error('Error sending message notification:', error); diff --git a/src/handlers/likeBackHandler.ts b/src/handlers/likeBackHandler.ts new file mode 100644 index 0000000..583d1f5 --- /dev/null +++ b/src/handlers/likeBackHandler.ts @@ -0,0 +1,76 @@ +import TelegramBot from 'node-telegram-bot-api'; +import { ProfileService } from '../services/profileService'; +import { MatchingService } from '../services/matchingService'; + +export class LikeBackHandler { + private bot: TelegramBot; + private profileService: ProfileService; + private matchingService: MatchingService; + + constructor(bot: TelegramBot) { + this.bot = bot; + this.profileService = new ProfileService(); + this.matchingService = new MatchingService(); + } + + // Функция для обработки обратного лайка из уведомления + async handleLikeBack(chatId: number, telegramId: string, targetUserId: string): Promise { + try { + // Получаем информацию о пользователях + const [userId, targetProfile] = await Promise.all([ + this.profileService.getUserIdByTelegramId(telegramId), + this.profileService.getProfileByUserId(targetUserId) + ]); + + if (!userId || !targetProfile) { + await this.bot.sendMessage(chatId, '❌ Не удалось найти профиль'); + return; + } + + // Проверяем, есть ли уже свайп + const existingSwipe = await this.matchingService.getSwipeBetweenUsers(userId, targetUserId); + if (existingSwipe) { + await this.bot.sendMessage(chatId, '❓ Вы уже оценили этот профиль ранее.'); + return; + } + + // Создаем свайп (лайк) + const result = await this.matchingService.createSwipe(userId, targetUserId, 'like'); + + if (result.isMatch) { + // Это матч! + await this.bot.sendMessage( + chatId, + '🎉 *Поздравляем! Это взаимно!*\n\n' + + `Вы и *${targetProfile.name}* понравились друг другу!\n` + + 'Теперь вы можете начать общение.', + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: `start_chat:${targetUserId}` }], + [{ text: '👀 Посмотреть профиль', callback_data: `view_profile:${targetUserId}` }] + ] + } + } + ); + } else { + await this.bot.sendMessage( + chatId, + '❤️ Вам понравился профиль ' + targetProfile.name + '!\n\n' + + 'Если вы также понравитесь этому пользователю, будет создан матч.', + { + reply_markup: { + inline_keyboard: [ + [{ text: '🔍 Продолжить поиск', callback_data: 'start_browsing' }] + ] + } + } + ); + } + } catch (error) { + console.error('Error in handleLikeBack:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при обработке лайка'); + } + } +} diff --git a/src/scripts/enhanceNotifications.ts b/src/scripts/enhanceNotifications.ts new file mode 100644 index 0000000..440d737 --- /dev/null +++ b/src/scripts/enhanceNotifications.ts @@ -0,0 +1,133 @@ +import { query } from '../database/connection'; +import { v4 as uuidv4 } from 'uuid'; +import dotenv from 'dotenv'; + +// Загружаем переменные окружения +dotenv.config(); + +/** + * Скрипт для обновления механизма уведомлений + */ +export async function enhanceNotifications() { + try { + console.log('Enhancing notifications system...'); + console.log('DB Connection Details:'); + console.log(`- Host: ${process.env.DB_HOST}`); + console.log(`- Port: ${process.env.DB_PORT}`); + console.log(`- Database: ${process.env.DB_NAME}`); + + // 1. Создаем расширение для генерации UUID, если его нет + await query(` + CREATE EXTENSION IF NOT EXISTS "uuid-ossp" + `); + + // 2. Создаем таблицу для хранения типов уведомлений и шаблонов сообщений, если её нет + await query(` + CREATE TABLE IF NOT EXISTS 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 NOT NULL, + created_at TIMESTAMP DEFAULT NOW() + ) + `); + + // 2. Вставляем базовые шаблоны для различных типов уведомлений + const templates = [ + { + id: uuidv4(), + type: 'new_like', + title: 'Новый лайк!', + message_template: '❤️ *{{name}}* поставил(а) вам лайк!\n\nВозраст: {{age}}\n{{city}}\n\nОтветьте взаимностью или посмотрите профиль.', + button_template: JSON.stringify({ + 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' }] + ] + }) + }, + { + id: uuidv4(), + type: 'super_like', + title: 'Супер-лайк!', + message_template: '⭐️ *{{name}}* отправил(а) вам супер-лайк!\n\nВозраст: {{age}}\n{{city}}\n\nВы произвели особое впечатление! Ответьте взаимностью или посмотрите профиль.', + button_template: JSON.stringify({ + 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' }] + ] + }) + }, + { + id: uuidv4(), + type: 'new_match', + title: 'Новый матч!', + message_template: '🎊 *Ура! Это взаимно!* 🎊\n\nВы и *{{name}}* понравились друг другу!\nВозраст: {{age}}\n{{city}}\n\nСделайте первый шаг - напишите сообщение!', + button_template: JSON.stringify({ + inline_keyboard: [ + [{ text: '💬 Начать общение', callback_data: 'open_native_chat_{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'native_chats' } + ] + ] + }) + }, + { + id: uuidv4(), + type: 'new_message', + title: 'Новое сообщение!', + message_template: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + button_template: JSON.stringify({ + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_native_chat_{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'native_chats' } + ] + ] + }) + } + ]; + + // Вставляем шаблоны с проверкой на конфликты + for (const template of templates) { + await query(` + INSERT INTO notification_templates + (id, type, title, message_template, button_template, created_at) + VALUES ($1, $2, $3, $4, $5, NOW()) + ON CONFLICT (type) DO UPDATE + SET title = $3, + message_template = $4, + button_template = $5 + `, [template.id, template.type, template.title, template.message_template, template.button_template]); + } + + console.log('✅ Notification templates updated successfully'); + + return true; + } catch (error) { + console.error('❌ Error enhancing notifications:', error); + return false; + } +} + +// Если запускаем файл напрямую +if (require.main === module) { + enhanceNotifications().then(() => { + console.log('Notification system enhancement completed'); + process.exit(0); + }).catch(error => { + console.error('Error:', error); + process.exit(1); + }); +} diff --git a/src/services/matchingService.ts b/src/services/matchingService.ts index 9dde927..e064a44 100644 --- a/src/services/matchingService.ts +++ b/src/services/matchingService.ts @@ -152,6 +152,89 @@ export class MatchingService { return this.mapEntityToSwipe(result.rows[0]); } + + // Получить свайп между двумя пользователями (псевдоним для getSwipe) + async getSwipeBetweenUsers(userId: string, targetUserId: string): Promise { + return this.getSwipe(userId, targetUserId); + } + + // Создать свайп (лайк, дислайк или суперлайк) + async createSwipe(userId: string, targetUserId: string, swipeType: SwipeType): Promise<{ + swipe: Swipe; + isMatch: boolean; + match?: Match; + }> { + const swipeId = uuidv4(); + let isMatch = false; + let match: Match | undefined; + + await transaction(async (client) => { + // Создаем свайп + await client.query(` + INSERT INTO swipes (id, user_id, target_user_id, type, created_at) + VALUES ($1, $2, $3, $4, $5) + `, [swipeId, userId, targetUserId, swipeType, new Date()]); + + // Если это лайк или суперлайк, проверяем взаимность + if (swipeType === 'like' || swipeType === 'superlike') { + const reciprocalSwipe = await client.query(` + SELECT * FROM swipes + WHERE user_id = $1 AND target_user_id = $2 AND type IN ('like', 'superlike') + `, [targetUserId, userId]); + + if (reciprocalSwipe.rows.length > 0) { + // Проверяем, что матч еще не существует + const existingMatch = await client.query(` + SELECT * FROM matches + WHERE (user_id_1 = $1 AND user_id_2 = $2) OR (user_id_1 = $2 AND user_id_2 = $1) + `, [userId, targetUserId]); + + if (existingMatch.rows.length === 0) { + isMatch = true; + const matchId = uuidv4(); + const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].type === 'superlike'; + + // Упорядочиваем пользователей для консистентности + const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId]; + + // Создаем матч + await client.query(` + INSERT INTO matches (id, user_id_1, user_id_2, created_at, is_active, is_super_match) + VALUES ($1, $2, $3, $4, $5, $6) + `, [matchId, user1Id, user2Id, new Date(), true, isSuperMatch]); + + match = new Match({ + id: matchId, + userId1: user1Id, + userId2: user2Id, + createdAt: new Date(), + isActive: true, + isSuperMatch: isSuperMatch, + unreadCount1: 0, + unreadCount2: 0 + }); + + // Обновляем свайпы, отмечая что они образуют матч + await client.query(` + UPDATE swipes SET is_match = true + WHERE (user_id = $1 AND target_user_id = $2) OR (user_id = $2 AND target_user_id = $1) + `, [userId, targetUserId]); + } + } + } + }); + + const swipe = new Swipe({ + id: swipeId, + userId, + targetUserId, + type: swipeType, + timestamp: new Date(), + isMatch + }); + + return { swipe, isMatch, match }; + } // Получить все матчи пользователя по telegram ID async getUserMatches(telegramId: string, limit: number = 50): Promise { diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index 707426f..a34e59a 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -1,7 +1,6 @@ import TelegramBot from 'node-telegram-bot-api'; import { query } from '../database/connection'; import { ProfileService } from './profileService'; -import config from '../../config/default.json'; export interface NotificationData { userId: string; @@ -19,6 +18,126 @@ export class NotificationService { this.profileService = new ProfileService(); } + // Получить шаблон уведомления из базы данных или использовать встроенный + 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_native_chat_{{matchId}}' }], + [ + { text: '👀 Посмотреть профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все матчи', callback_data: 'native_chats' } + ] + ] + } + }, + 'new_message': { + title: 'Новое сообщение!', + messageTemplate: '💌 *Новое сообщение!*\n\nОт: *{{name}}*\n\n"{{message}}"\n\nОтветьте на сообщение прямо сейчас!', + buttonTemplate: { + inline_keyboard: [ + [{ text: '📩 Ответить', callback_data: 'open_native_chat_{{matchId}}' }], + [ + { text: '👤 Профиль', callback_data: 'view_profile:{{userId}}' }, + { text: '📋 Все чаты', callback_data: 'native_chats' } + ] + ] + } + } + }; + + 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); + } + // Отправить уведомление о новом лайке async sendLikeNotification(targetTelegramId: string, likerTelegramId: string, isSuperLike: boolean = false): Promise { try { @@ -30,25 +149,41 @@ export class NotificationService { if (!targetUser || !likerProfile || !this.bot) { return; } - - const message = isSuperLike - ? `⭐ ${likerProfile.name} отправил вам суперлайк!` - : `💖 ${likerProfile.name} поставил вам лайк!`; - + + // Получаем шаблон уведомления + 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); + + // Отправляем уведомление await this.bot.sendMessage(targetUser.telegram_id, message, { - reply_markup: { - inline_keyboard: [[ - { text: '👀 Посмотреть профиль', callback_data: `view_profile:${likerProfile.userId}` }, - { text: '💕 Начать знакомиться', callback_data: 'start_browsing' } - ]] - } + parse_mode: 'Markdown', + reply_markup: keyboard }); // Логируем уведомление await this.logNotification({ userId: targetUser.id, - type: isSuperLike ? 'super_like' : 'new_like', - data: { likerUserId: likerProfile.userId, likerName: likerProfile.name } + type: templateType, + data: { + likerUserId: likerProfile.userId, + likerName: likerProfile.name, + age: likerProfile.age, + city: likerProfile.city + } }); } catch (error) { console.error('Error sending like notification:', error); @@ -67,22 +202,50 @@ export class NotificationService { return; } - const message = `🎉 У вас новый матч с ${matchedProfile.name}!\n\nТеперь вы можете начать общение.`; + // Получаем матч-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); + + // Отправляем уведомление await this.bot.sendMessage(user.telegram_id, message, { - reply_markup: { - inline_keyboard: [[ - { text: '💬 Написать сообщение', callback_data: `start_chat:${matchedUserId}` }, - { text: '👀 Посмотреть профиль', callback_data: `view_profile:${matchedUserId}` } - ]] - } + parse_mode: 'Markdown', + reply_markup: keyboard }); // Логируем уведомление await this.logNotification({ userId, type: 'new_match', - data: { matchedUserId, matchedName: matchedProfile.name } + data: { + matchedUserId, + matchedName: matchedProfile.name, + age: matchedProfile.age, + city: matchedProfile.city, + matchId + } }); } catch (error) { console.error('Error sending match notification:', error); @@ -90,7 +253,7 @@ export class NotificationService { } // Отправить уведомление о новом сообщении - async sendMessageNotification(receiverId: string, senderId: string, messageContent: string): Promise { + async sendMessageNotification(receiverId: string, senderId: string, messageContent: string, matchId?: string): Promise { try { const [receiver, senderProfile] = await Promise.all([ this.getUserByUserId(receiverId), @@ -107,25 +270,55 @@ export class NotificationService { 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 message = `💬 Новое сообщение от ${senderProfile.name}:\n\n${truncatedMessage}`; - + + // Получаем шаблон уведомления + 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); + + // Отправляем уведомление await this.bot.sendMessage(receiver.telegram_id, message, { - reply_markup: { - inline_keyboard: [[ - { text: '💬 Ответить', callback_data: `open_chat:${senderId}` } - ]] - } + parse_mode: 'Markdown', + reply_markup: keyboard }); // Логируем уведомление await this.logNotification({ userId: receiverId, type: 'new_message', - data: { senderId, senderName: senderProfile.name, messageContent: truncatedMessage } + data: { + senderId, + senderName: senderProfile.name, + messageContent: truncatedMessage, + matchId: actualMatchId + } }); } catch (error) { console.error('Error sending message notification:', error); @@ -364,7 +557,7 @@ export class NotificationService { type VARCHAR(50) NOT NULL, data JSONB, scheduled_at TIMESTAMP NOT NULL, - is_processed BOOLEAN DEFAULT FALSE, + processed BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT NOW() ) `); @@ -373,7 +566,7 @@ export class NotificationService { // Получаем запланированные уведомления const result = await query(` SELECT * FROM scheduled_notifications - WHERE scheduled_at <= $1 AND is_processed = false + WHERE scheduled_at <= $1 AND processed = false ORDER BY scheduled_at ASC LIMIT 100 `, [new Date()]); @@ -393,7 +586,7 @@ export class NotificationService { // Отмечаем как обработанное await query( - 'UPDATE scheduled_notifications SET is_processed = true WHERE id = $1', + 'UPDATE scheduled_notifications SET processed = true WHERE id = $1', [notification.id] ); } catch (error) { @@ -404,4 +597,4 @@ export class NotificationService { console.error('Error processing scheduled notifications:', error); } } -} \ No newline at end of file +} diff --git a/src/services/profileService.ts b/src/services/profileService.ts index 8b1a367..6b4fcdb 100644 --- a/src/services/profileService.ts +++ b/src/services/profileService.ts @@ -169,8 +169,8 @@ export class ProfileService { case 'photos': case 'interests': updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`); - // Для PostgreSQL массивы передаем как есть, не как JSON строки - updateValues.push(value); + // Для PostgreSQL массивы должны быть преобразованы в JSON-строку + updateValues.push(JSON.stringify(value)); break; case 'location': // Пропускаем обработку местоположения, так как колонки location нет diff --git a/test-connection.js b/test-connection.js new file mode 100644 index 0000000..56810cd --- /dev/null +++ b/test-connection.js @@ -0,0 +1,37 @@ +require('dotenv').config(); +const { Pool } = require('pg'); + +// Используем параметры напрямую из .env +const pool = new Pool({ + host: process.env.DB_HOST, + port: process.env.DB_PORT, + database: process.env.DB_NAME, + user: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + max: 5, + connectionTimeoutMillis: 5000 +}); + +console.log('DB Connection Details:'); +console.log(`- Host: ${process.env.DB_HOST}`); +console.log(`- Port: ${process.env.DB_PORT}`); +console.log(`- Database: ${process.env.DB_NAME}`); +console.log(`- User: ${process.env.DB_USERNAME}`); + +async function testConnection() { + try { + const client = await pool.connect(); + try { + const result = await client.query('SELECT NOW() as current_time'); + console.log('✅ Connected to database successfully!'); + console.log(`Current database time: ${result.rows[0].current_time}`); + } finally { + client.release(); + } + await pool.end(); + } catch (error) { + console.error('❌ Failed to connect to database:', error); + } +} + +testConnection(); diff --git a/test-db.js b/test-db.js new file mode 100644 index 0000000..e69de29