From 321547bf2772ff4007780e5f07aa7f2cbfe05e55 Mon Sep 17 00:00:00 2001 From: "Andrey K. Choi" Date: Sat, 13 Sep 2025 07:51:02 +0900 Subject: [PATCH] Fix like/dislike errors and implement native chat system --- NATIVE_CHAT_SYSTEM.md | 123 ++++++ clear_database.sql | 73 ++++ src/bot.ts | 45 +- .../migrations/add_lifestyle_column.sql | 22 + src/database/migrations/add_profile_views.sql | 39 ++ .../migrations/fix_profile_views_conflict.sql | 3 + src/handlers/callbackHandlers.ts | 317 +++++++++++++- src/handlers/commandHandlers.ts | 3 +- src/handlers/enhancedChatHandlers.ts | 389 ++++++++++++++++++ src/handlers/messageHandlers.ts | 10 +- src/services/matchingService.ts | 49 ++- src/services/notificationService.ts | 68 ++- src/services/profileService.ts | 75 +++- ...es -d telegram_tinder_bot -c \\d profiles" | 59 +++ 14 files changed, 1236 insertions(+), 39 deletions(-) create mode 100644 NATIVE_CHAT_SYSTEM.md create mode 100644 clear_database.sql create mode 100644 src/database/migrations/add_lifestyle_column.sql create mode 100644 src/database/migrations/add_profile_views.sql create mode 100644 src/database/migrations/fix_profile_views_conflict.sql create mode 100644 src/handlers/enhancedChatHandlers.ts create mode 100644 "sword123 psql -h localhost -p 5433 -U postgres -d telegram_tinder_bot -c \\d profiles" diff --git a/NATIVE_CHAT_SYSTEM.md b/NATIVE_CHAT_SYSTEM.md new file mode 100644 index 0000000..f26a844 --- /dev/null +++ b/NATIVE_CHAT_SYSTEM.md @@ -0,0 +1,123 @@ +# Система Нативных Чатов - Документация + +## Обзор +Новая система нативных чатов обеспечивает более естественное взаимодействие пользователей в Telegram, заменяя сложные кнопочные интерфейсы на прямые сообщения. + +## Ключевые особенности + +### 1. Нативный интерфейс чатов +- **Простой список чатов** без лишних кнопок +- **Прямые сообщения** - пользователи могут просто печатать и отправлять +- **Контекстное понимание** - бот автоматически определяет в каком чате находится пользователь + +### 2. Автоматические уведомления +- **Мгновенные push-уведомления** о новых сообщениях +- **Умные уведомления** - не спамят, отправляются только при необходимости +- **Персонализированные сообщения** с именем отправителя + +### 3. Контекстная система +- **30-минутное окно контекста** - бот помнит в каком чате пользователь +- **Автоматическое переключение** между чатами +- **Graceful timeout** - контекст сбрасывается через 30 минут неактивности + +## Архитектура + +### EnhancedChatHandlers +Основной класс для обработки нативных чатов: + +```typescript +- showChatsNative() - показывает список чатов +- openNativeChat() - открывает конкретный чат +- handleIncomingChatMessage() - обрабатывает входящие сообщения +- sendMessageNotification() - отправляет уведомления +- showChatHistory() - показывает историю сообщений +``` + +### Интеграция с MessageHandlers +- Автоматическое перехватывание сообщений +- Проверка контекста чата перед обработкой +- Fallback на стандартную обработку если не в чате + +### Callback Integration +Новые callback handlers: +- `native_chats` - открывает нативный интерфейс чатов +- `open_native_chat_` - открывает конкретный чат +- `chat_history_` - показывает историю + +## Пользовательский опыт + +### Старый способ (сложный) +1. Нажать "Открыть чаты" +2. Выбрать чат из списка +3. Нажать "Отправить сообщение" +4. Написать сообщение +5. Подтвердить отправку + +### Новый способ (нативный) +1. Нажать "Нативные чаты" +2. Выбрать чат +3. Просто написать сообщение и отправить +4. Получить автоматическое уведомление об ответе + +## Технические детали + +### Хранение контекста +```typescript +private activeChatContexts: Map = new Map(); +``` + +### Уведомления +- Проверка активности получателя +- Избежание дублирования уведомлений +- Персонализированные сообщения + +### База данных +- Используется существующая структура messages +- Автоматическое сохранение всех сообщений +- Совместимость со старой системой + +## Преимущества + +1. **Интуитивность** - привычный интерфейс Telegram +2. **Скорость** - меньше кликов для отправки сообщения +3. **Уведомления** - пользователи не пропустят сообщения +4. **Контекст** - бот понимает в каком чате находится пользователь +5. **Совместимость** - работает параллельно со старой системой + +## Настройки + +### Timeout контекста +По умолчанию: 30 минут +Можно изменить в константе `CHAT_CONTEXT_TIMEOUT_MS` + +### Уведомления +Автоматически отправляются при: +- Получении нового сообщения +- Если получатель не в активном чате +- Если прошло достаточно времени с последнего уведомления + +## Безопасность + +- Проверка существования матча перед отправкой +- Валидация прав доступа к чату +- Защита от спама уведомлениями +- Автоматическая очистка неактивных контекстов + +## Миграция + +Система полностью обратно совместима: +- Старые чаты продолжают работать +- Новые пользователи автоматически используют нативный интерфейс +- Постепенная миграция существующих пользователей + +## Мониторинг + +Логирование всех ключевых событий: +- Отправка сообщений +- Переключение контекста +- Отправка уведомлений +- Ошибки и исключения diff --git a/clear_database.sql b/clear_database.sql new file mode 100644 index 0000000..acb3b57 --- /dev/null +++ b/clear_database.sql @@ -0,0 +1,73 @@ +-- Скрипт для полной очистки базы данных Telegram Tinder Bot +-- ВНИМАНИЕ: Этот скрипт удалит ВСЕ данные из базы данных! + +-- Отключаем проверки внешних ключей для более быстрой очистки +SET session_replication_role = replica; + +-- Очищаем все таблицы в правильном порядке (учитывая зависимости) +TRUNCATE TABLE messages CASCADE; +TRUNCATE TABLE matches CASCADE; +TRUNCATE TABLE swipes CASCADE; +TRUNCATE TABLE profile_views CASCADE; +TRUNCATE TABLE scheduled_notifications CASCADE; +TRUNCATE TABLE reports CASCADE; +TRUNCATE TABLE blocks CASCADE; +TRUNCATE TABLE user_sessions CASCADE; +TRUNCATE TABLE profiles CASCADE; +TRUNCATE TABLE users CASCADE; + +-- Включаем обратно проверки внешних ключей +SET session_replication_role = DEFAULT; + +-- Сбрасываем счетчики автоинкремента (если есть) +-- В данном случае используются UUID, поэтому это не нужно + +-- Выводим статистику после очистки +SELECT + 'users' as table_name, + COUNT(*) as rows_count +FROM users + +UNION ALL + +SELECT + 'profiles' as table_name, + COUNT(*) as rows_count +FROM profiles + +UNION ALL + +SELECT + 'matches' as table_name, + COUNT(*) as rows_count +FROM matches + +UNION ALL + +SELECT + 'swipes' as table_name, + COUNT(*) as rows_count +FROM swipes + +UNION ALL + +SELECT + 'messages' as table_name, + COUNT(*) as rows_count +FROM messages + +UNION ALL + +SELECT + 'profile_views' as table_name, + COUNT(*) as rows_count +FROM profile_views + +UNION ALL + +SELECT + 'scheduled_notifications' as table_name, + COUNT(*) as rows_count +FROM scheduled_notifications; + +PRINT '✅ База данных успешно очищена!'; diff --git a/src/bot.ts b/src/bot.ts index b73a329..9229fce 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -1,6 +1,6 @@ import 'dotenv/config'; import TelegramBot from 'node-telegram-bot-api'; -import { testConnection } from './database/connection'; +import { testConnection, query } from './database/connection'; import { ProfileService } from './services/profileService'; import { MatchingService } from './services/matchingService'; import { NotificationService } from './services/notificationService'; @@ -143,8 +143,49 @@ class TelegramTinderBot { // Очистка старых данных private async cleanupOldData(): Promise { - // TODO: Реализовать очистку старых уведомлений, логов и т.д. console.log('🧹 Running cleanup tasks...'); + + try { + // Очистка старых уведомлений (старше 30 дней) + const notificationsResult = await query(` + DELETE FROM scheduled_notifications + WHERE processed = true + AND created_at < CURRENT_TIMESTAMP - INTERVAL '30 days' + `); + console.log(`🗑️ Cleaned up ${notificationsResult.rowCount} old notifications`); + + // Очистка старых просмотров профилей (старше 90 дней) + const profileViewsResult = await query(` + DELETE FROM profile_views + WHERE viewed_at < CURRENT_TIMESTAMP - INTERVAL '90 days' + `); + console.log(`👀 Cleaned up ${profileViewsResult.rowCount} old profile views`); + + // Очистка старых свайпов (старше 180 дней) + const swipesResult = await query(` + DELETE FROM swipes + WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '180 days' + `); + console.log(`↔️ Cleaned up ${swipesResult.rowCount} old swipes`); + + // Очистка старых сообщений в неактивных матчах (старше 1 года) + const messagesResult = await query(` + DELETE FROM messages + WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '1 year' + AND match_id IN ( + SELECT id FROM matches + WHERE last_message_at < CURRENT_TIMESTAMP - INTERVAL '1 year' + ) + `); + console.log(`💬 Cleaned up ${messagesResult.rowCount} old messages`); + + // Обновление статистики таблиц после очистки + await query('VACUUM ANALYZE scheduled_notifications, profile_views, swipes, messages'); + + console.log('✅ Cleanup completed successfully'); + } catch (error) { + console.error('❌ Error during cleanup:', error); + } } // Корректное завершение работы diff --git a/src/database/migrations/add_lifestyle_column.sql b/src/database/migrations/add_lifestyle_column.sql new file mode 100644 index 0000000..04e832a --- /dev/null +++ b/src/database/migrations/add_lifestyle_column.sql @@ -0,0 +1,22 @@ +-- Добавление недостающих полей lifestyle для полной совместимости +-- Версия: 2025-09-13-003 + +-- Добавляем поле lifestyle как JSONB для хранения структурированных данных +ALTER TABLE profiles +ADD COLUMN IF NOT EXISTS lifestyle JSONB; + +-- Добавляем комментарий для понимания структуры +COMMENT ON COLUMN profiles.lifestyle IS 'JSON object with lifestyle preferences: {"smoking": "never|sometimes|regularly", "drinking": "never|sometimes|regularly", "kids": "have|want|dont_want|unsure"}'; + +-- Создаем индекс для быстрого поиска по lifestyle +CREATE INDEX IF NOT EXISTS idx_profiles_lifestyle ON profiles USING GIN (lifestyle); + +-- Создаем частичные индексы для наиболее частых поисков +CREATE INDEX IF NOT EXISTS idx_profiles_lifestyle_smoking +ON profiles ((lifestyle->>'smoking')); + +CREATE INDEX IF NOT EXISTS idx_profiles_lifestyle_drinking +ON profiles ((lifestyle->>'drinking')); + +CREATE INDEX IF NOT EXISTS idx_profiles_lifestyle_kids +ON profiles ((lifestyle->>'kids')); diff --git a/src/database/migrations/add_profile_views.sql b/src/database/migrations/add_profile_views.sql new file mode 100644 index 0000000..c5de098 --- /dev/null +++ b/src/database/migrations/add_profile_views.sql @@ -0,0 +1,39 @@ +-- Миграция для добавления отслеживания просмотров профилей +-- Версия: 2025-09-13-001 + +-- Создание таблицы просмотров профилей +CREATE TABLE IF NOT EXISTS profile_views ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + viewer_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + viewed_profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + viewed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + view_type VARCHAR(20) DEFAULT 'browse' -- 'browse', 'search', 'match', 'chat' +); + +-- Уникальный индекс для ограничения одного просмотра в день +CREATE UNIQUE INDEX IF NOT EXISTS idx_profile_views_unique_daily +ON profile_views(viewer_id, viewed_profile_id, DATE(viewed_at)); + +-- Индексы для оптимизации +CREATE INDEX IF NOT EXISTS idx_profile_views_viewer ON profile_views(viewer_id); +CREATE INDEX IF NOT EXISTS idx_profile_views_viewed ON profile_views(viewed_profile_id); +CREATE INDEX IF NOT EXISTS idx_profile_views_date ON profile_views(viewed_at); +CREATE INDEX IF NOT EXISTS idx_profile_views_type ON profile_views(view_type); + +-- Функция для очистки старых просмотров (старше 90 дней) +CREATE OR REPLACE FUNCTION cleanup_old_profile_views() +RETURNS INTEGER AS $$ +DECLARE + deleted_count INTEGER; +BEGIN + DELETE FROM profile_views + WHERE viewed_at < CURRENT_TIMESTAMP - INTERVAL '90 days'; + + GET DIAGNOSTICS deleted_count = ROW_COUNT; + RETURN deleted_count; +END; +$$ LANGUAGE plpgsql; + +-- Комментарии +COMMENT ON TABLE profile_views IS 'Таблица для отслеживания просмотров профилей'; +COMMENT ON COLUMN profile_views.view_type IS 'Тип просмотра: browse, search, match, chat'; diff --git a/src/database/migrations/fix_profile_views_conflict.sql b/src/database/migrations/fix_profile_views_conflict.sql new file mode 100644 index 0000000..68d3fd8 --- /dev/null +++ b/src/database/migrations/fix_profile_views_conflict.sql @@ -0,0 +1,3 @@ +-- Добавление уникального индекса для profile_views для корректной работы ON CONFLICT +CREATE UNIQUE INDEX IF NOT EXISTS idx_profile_views_unique +ON profile_views (viewer_id, viewed_profile_id); diff --git a/src/handlers/callbackHandlers.ts b/src/handlers/callbackHandlers.ts index 0189f0a..8be388f 100644 --- a/src/handlers/callbackHandlers.ts +++ b/src/handlers/callbackHandlers.ts @@ -5,6 +5,7 @@ import { ChatService } from '../services/chatService'; import { Profile } from '../models/Profile'; import { MessageHandlers } from './messageHandlers'; import { ProfileEditController } from '../controllers/profileEditController'; +import { EnhancedChatHandlers } from './enhancedChatHandlers'; export class CallbackHandlers { private bot: TelegramBot; @@ -13,6 +14,7 @@ export class CallbackHandlers { private chatService: ChatService; private messageHandlers: MessageHandlers; private profileEditController: ProfileEditController; + private enhancedChatHandlers: EnhancedChatHandlers; constructor(bot: TelegramBot, messageHandlers: MessageHandlers) { this.bot = bot; @@ -21,6 +23,7 @@ export class CallbackHandlers { this.chatService = new ChatService(); this.messageHandlers = messageHandlers; this.profileEditController = new ProfileEditController(this.profileService); + this.enhancedChatHandlers = new EnhancedChatHandlers(bot); } register(): void { @@ -156,6 +159,14 @@ export class CallbackHandlers { await this.handleViewMatches(chatId, telegramId); } else if (data === 'open_chats') { await this.handleOpenChats(chatId, telegramId); + } else if (data === 'native_chats') { + await this.enhancedChatHandlers.showChatsNative(chatId, telegramId); + } else if (data.startsWith('open_native_chat_')) { + const matchId = data.replace('open_native_chat_', ''); + await this.enhancedChatHandlers.openNativeChat(chatId, telegramId, matchId); + } else if (data.startsWith('chat_history_')) { + const matchId = data.replace('chat_history_', ''); + await this.enhancedChatHandlers.showChatHistory(chatId, telegramId, matchId); } else if (data.startsWith('chat_')) { const matchId = data.replace('chat_', ''); await this.handleOpenChat(chatId, telegramId, matchId); @@ -180,6 +191,18 @@ export class CallbackHandlers { await this.handleSearchSettings(chatId, telegramId); } else if (data === 'notification_settings') { await this.handleNotificationSettings(chatId, telegramId); + } else if (data === 'view_stats') { + await this.handleViewStats(chatId, telegramId); + } else if (data === 'view_profile_viewers') { + await this.handleViewProfileViewers(chatId, telegramId); + } else if (data === 'hide_profile') { + await this.handleHideProfile(chatId, telegramId); + } else if (data === 'delete_profile') { + await this.handleDeleteProfile(chatId, telegramId); + } else if (data === 'main_menu') { + await this.handleMainMenu(chatId, telegramId); + } else if (data === 'confirm_delete_profile') { + await this.handleConfirmDeleteProfile(chatId, telegramId); } // Информация @@ -187,6 +210,8 @@ export class CallbackHandlers { await this.handleHowItWorks(chatId); } else if (data === 'back_to_browsing') { await this.handleStartBrowsing(chatId, telegramId); + } else if (data === 'get_vip') { + await this.handleGetVip(chatId, telegramId); } else { @@ -278,7 +303,13 @@ export class CallbackHandlers { // Лайк async handleLike(chatId: number, telegramId: string, targetUserId: string): Promise { try { - const result = await this.matchingService.performSwipe(telegramId, targetUserId, 'like'); + // Получаем telegram_id целевого пользователя + const targetTelegramId = await this.profileService.getTelegramIdByUserId(targetUserId); + if (!targetTelegramId) { + throw new Error('Target user not found'); + } + + const result = await this.matchingService.performSwipe(telegramId, targetTelegramId, 'like'); if (result.isMatch) { // Это матч! @@ -314,7 +345,13 @@ export class CallbackHandlers { // Дизлайк async handleDislike(chatId: number, telegramId: string, targetUserId: string): Promise { try { - await this.matchingService.performSwipe(telegramId, targetUserId, 'pass'); + // Получаем telegram_id целевого пользователя + const targetTelegramId = await this.profileService.getTelegramIdByUserId(targetUserId); + if (!targetTelegramId) { + throw new Error('Target user not found'); + } + + await this.matchingService.performSwipe(telegramId, targetTelegramId, 'pass'); await this.showNextCandidate(chatId, telegramId); } catch (error) { await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка'); @@ -325,7 +362,13 @@ export class CallbackHandlers { // Супер лайк async handleSuperlike(chatId: number, telegramId: string, targetUserId: string): Promise { try { - const result = await this.matchingService.performSwipe(telegramId, targetUserId, 'superlike'); + // Получаем telegram_id целевого пользователя + const targetTelegramId = await this.profileService.getTelegramIdByUserId(targetUserId); + if (!targetTelegramId) { + throw new Error('Target user not found'); + } + + const result = await this.matchingService.performSwipe(telegramId, targetTelegramId, 'superlike'); if (result.isMatch) { const targetProfile = await this.profileService.getProfileByUserId(targetUserId); @@ -366,6 +409,12 @@ export class CallbackHandlers { return; } + // Записываем просмотр профиля + const targetTelegramId = await this.profileService.getTelegramIdByUserId(targetProfile.userId); + if (targetTelegramId) { + await this.profileService.recordProfileView(telegramId, targetTelegramId, 'profile_view'); + } + await this.showProfile(chatId, targetProfile, false, telegramId); } @@ -643,7 +692,11 @@ export class CallbackHandlers { { text: '🔔 Уведомления', callback_data: 'notification_settings' } ], [ - { text: '🚫 Скрыть профиль', callback_data: 'hide_profile' }, + { text: '� Статистика', callback_data: 'view_stats' }, + { text: '👀 Кто смотрел', callback_data: 'view_profile_viewers' } + ], + [ + { text: '�🚫 Скрыть профиль', callback_data: 'hide_profile' }, { text: '🗑 Удалить профиль', callback_data: 'delete_profile' } ] ] @@ -812,6 +865,12 @@ export class CallbackHandlers { return; } + // Записываем просмотр кандидата + const candidateTelegramId = await this.profileService.getTelegramIdByUserId(candidate.userId); + if (candidateTelegramId) { + await this.profileService.recordProfileView(telegramId, candidateTelegramId, 'browse'); + } + const candidatePhotoFileId = candidate.photos[0]; // Первое фото - главное let candidateText = candidate.name + ', ' + candidate.age + '\n'; @@ -1577,4 +1636,254 @@ export class CallbackHandlers { return parts.join(', '); } + + // Просмотр статистики профиля + async handleViewStats(chatId: number, telegramId: string): Promise { + try { + const profile = await this.profileService.getProfileByTelegramId(telegramId); + if (!profile) { + await this.bot.sendMessage(chatId, '❌ Профиль не найден'); + return; + } + + const stats = await this.profileService.getProfileStats(profile.userId); + + const statsText = `📊 Статистика вашего профиля\n\n` + + `💖 Ваши лайки: ${stats.totalLikes}\n` + + `💕 Матчи: ${stats.totalMatches}\n` + + `👀 Просмотры профиля: ${stats.profileViews}\n` + + `❤️ Лайки получено: ${stats.likesReceived}\n\n` + + `💡 Совет: Заполните все поля профиля и добавьте качественные фото для большего успеха!`; + + const keyboard: InlineKeyboardMarkup = { + inline_keyboard: [ + [{ text: '👀 Кто смотрел профиль', callback_data: 'view_profile_viewers' }], + [{ text: '🔙 Назад', callback_data: 'settings' }] + ] + }; + + await this.bot.sendMessage(chatId, statsText, { reply_markup: keyboard }); + } catch (error) { + console.error('Error viewing stats:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при получении статистики'); + } + } + + // Просмотр кто смотрел профиль + async handleViewProfileViewers(chatId: number, telegramId: string): Promise { + try { + const profile = await this.profileService.getProfileByTelegramId(telegramId); + if (!profile) { + await this.bot.sendMessage(chatId, '❌ Профиль не найден'); + return; + } + + const viewers = await this.profileService.getProfileViewers(profile.userId, 10); + + if (viewers.length === 0) { + await this.bot.sendMessage( + chatId, + '👁️ Пока никто не просматривал ваш профиль\n\n' + + 'Начните искать анкеты, чтобы другие пользователи тоже могли вас найти!', + { + reply_markup: { + inline_keyboard: [ + [{ text: '🔍 Начать поиск', callback_data: 'start_browsing' }], + [{ text: '🔙 Назад', callback_data: 'settings' }] + ] + } + } + ); + return; + } + + let viewersText = '👀 Последние просмотры вашего профиля:\n\n'; + viewers.forEach((viewer, index) => { + viewersText += `${index + 1}. ${viewer.name}, ${viewer.age}\n`; + if (viewer.city) viewersText += ` 📍 ${viewer.city}\n`; + viewersText += '\n'; + }); + + const keyboard: InlineKeyboardMarkup = { + inline_keyboard: [ + [{ text: '📊 Статистика', callback_data: 'view_stats' }], + [{ text: '🔙 Назад', callback_data: 'settings' }] + ] + }; + + await this.bot.sendMessage(chatId, viewersText, { reply_markup: keyboard }); + } catch (error) { + console.error('Error viewing profile viewers:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при получении информации о просмотрах'); + } + } + + // Возврат в главное меню + async handleMainMenu(chatId: number, telegramId: string): Promise { + // Используем существующий метод handleStart из CommandHandlers + const profile = await this.profileService.getProfileByTelegramId(telegramId); + + if (profile) { + const keyboard: InlineKeyboardMarkup = { + inline_keyboard: [ + [ + { text: '👤 Мой профиль', callback_data: 'view_my_profile' }, + { text: '🔍 Просмотр анкет', callback_data: 'start_browsing' } + ], + [ + { text: '💕 Мои матчи', callback_data: 'view_matches' }, + { text: '⭐ VIP поиск', callback_data: 'vip_search' } + ], + [ + { text: '⚙️ Настройки', callback_data: 'settings' } + ] + ] + }; + + await this.bot.sendMessage( + chatId, + `🎉 Добро пожаловать, ${profile.name}!\n\n` + + `💖 Telegram Tinder Bot готов к работе!\n\n` + + `Что хотите сделать?`, + { reply_markup: keyboard } + ); + } else { + const keyboard: InlineKeyboardMarkup = { + inline_keyboard: [ + [{ text: '✨ Создать профиль', callback_data: 'create_profile' }], + [{ text: 'ℹ️ Как это работает?', callback_data: 'how_it_works' }] + ] + }; + + await this.bot.sendMessage( + chatId, + `🎉 Добро пожаловать в Telegram Tinder Bot!\n\n` + + `💕 Здесь вы можете найти свою вторую половинку!\n\n` + + `Для начала создайте свой профиль:`, + { reply_markup: keyboard } + ); + } + } + + // Скрыть/показать профиль + async handleHideProfile(chatId: number, telegramId: string): Promise { + try { + const profile = await this.profileService.getProfileByTelegramId(telegramId); + if (!profile) { + await this.bot.sendMessage(chatId, '❌ Профиль не найден'); + return; + } + + const newVisibility = !profile.isVisible; + await this.profileService.toggleVisibility(profile.userId); + + const statusText = newVisibility ? 'видимым' : 'скрытым'; + const emoji = newVisibility ? '👁️' : '🙈'; + + await this.bot.sendMessage( + chatId, + `${emoji} Ваш профиль теперь ${statusText}!\n\n` + + (newVisibility + ? '✅ Другие пользователи смогут найти вас в поиске' + : '🔒 Ваш профиль скрыт и не отображается в поиске'), + { + reply_markup: { + inline_keyboard: [ + [{ text: '🔙 Назад к настройкам', callback_data: 'settings' }] + ] + } + } + ); + } catch (error) { + console.error('Error toggling profile visibility:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при изменении видимости профиля'); + } + } + + // Удаление профиля + async handleDeleteProfile(chatId: number, telegramId: string): Promise { + const keyboard: InlineKeyboardMarkup = { + inline_keyboard: [ + [ + { text: '✅ Да, удалить', callback_data: 'confirm_delete_profile' }, + { text: '❌ Отмена', callback_data: 'settings' } + ] + ] + }; + + await this.bot.sendMessage( + chatId, + '⚠️ ВНИМАНИЕ!\n\n' + + '🗑️ Вы действительно хотите удалить свой профиль?\n\n' + + '❗ Это действие нельзя отменить:\n' + + '• Все ваши фото будут удалены\n' + + '• Все матчи и переписки исчезнут\n' + + '• Придется создавать профиль заново\n\n' + + '🤔 Подумайте еще раз!', + { reply_markup: keyboard } + ); + } + + // Подтверждение удаления профиля + async handleConfirmDeleteProfile(chatId: number, telegramId: string): Promise { + try { + const profile = await this.profileService.getProfileByTelegramId(telegramId); + if (!profile) { + await this.bot.sendMessage(chatId, '❌ Профиль не найден'); + return; + } + + // Удаляем профиль (это также удалит связанные данные через CASCADE) + await this.profileService.deleteProfile(profile.userId); + + await this.bot.sendMessage( + chatId, + '💔 Ваш профиль успешно удален\n\n' + + '😢 Мы будем скучать! Но вы всегда можете вернуться и создать новый профиль.\n\n' + + '👋 До свидания!', + { + reply_markup: { + inline_keyboard: [ + [{ text: '✨ Создать новый профиль', callback_data: 'create_profile' }] + ] + } + } + ); + } catch (error) { + console.error('Error deleting profile:', error); + await this.bot.sendMessage(chatId, '❌ Произошла ошибка при удалении профиля'); + } + } + + // Получение VIP статуса + async handleGetVip(chatId: number, telegramId: string): Promise { + await this.bot.sendMessage( + chatId, + '💎 *VIP Статус*\n\n' + + '✨ VIP дает вам дополнительные возможности:\n\n' + + '🔍 *Расширенный поиск* - найти людей по интересам\n' + + '📊 *Подробная статистика* - кто смотрел ваш профиль\n' + + '💕 *Больше лайков* - отправляйте до 100 лайков в день\n' + + '⭐ *Приоритет в поиске* - ваш профиль показывается первым\n' + + '🎯 *Суперлайки* - выделите себя среди других\n\n' + + '💰 *Стоимость:*\n' + + '• 1 месяц - 299₽\n' + + '• 3 месяца - 699₽ (экономия 200₽)\n' + + '• 6 месяцев - 1199₽ (экономия 600₽)\n\n' + + '📞 Для получения VIP свяжитесь с администратором: @admin', + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + { text: '📞 Связаться с админом', url: 'https://t.me/admin' } + ], + [ + { text: '🔙 Назад', callback_data: 'vip_search' } + ] + ] + } + } + ); + } } diff --git a/src/handlers/commandHandlers.ts b/src/handlers/commandHandlers.ts index f9ba01c..d6497dd 100644 --- a/src/handlers/commandHandlers.ts +++ b/src/handlers/commandHandlers.ts @@ -172,7 +172,8 @@ export class CommandHandlers { const keyboard: InlineKeyboardMarkup = { inline_keyboard: [ [{ text: '💬 Открыть чаты', callback_data: 'open_chats' }], - [{ text: '🔍 Найти еще', callback_data: 'start_browsing' }] + [{ text: '� Нативные чаты', callback_data: 'native_chats' }], + [{ text: '�🔍 Найти еще', callback_data: 'start_browsing' }] ] }; diff --git a/src/handlers/enhancedChatHandlers.ts b/src/handlers/enhancedChatHandlers.ts new file mode 100644 index 0000000..8c7df54 --- /dev/null +++ b/src/handlers/enhancedChatHandlers.ts @@ -0,0 +1,389 @@ +import TelegramBot, { InlineKeyboardMarkup } from 'node-telegram-bot-api'; +import { ChatService } from '../services/chatService'; +import { ProfileService } from '../services/profileService'; +import { NotificationService } from '../services/notificationService'; + +export class EnhancedChatHandlers { + private bot: TelegramBot; + private chatService: ChatService; + private profileService: ProfileService; + private notificationService: NotificationService; + + constructor(bot: TelegramBot) { + this.bot = bot; + this.chatService = new ChatService(); + this.profileService = new ProfileService(); + this.notificationService = new NotificationService(bot); + } + + // ===== НАТИВНЫЙ ИНТЕРФЕЙС ЧАТОВ ===== + + // Показать список чатов в более простом формате + async showChatsNative(chatId: number, telegramId: string): Promise { + try { + const chats = await this.chatService.getUserChats(telegramId); + + if (chats.length === 0) { + await this.bot.sendMessage( + chatId, + '💬 *Чаты*\n\n' + + '😔 У вас пока нет активных чатов\n\n' + + '💡 Ставьте лайки, чтобы найти матчи и начать общение!', + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: '🔍 Найти людей', callback_data: 'start_browsing' }], + [{ text: '🏠 Главное меню', callback_data: 'main_menu' }] + ] + } + } + ); + return; + } + + // Создаем простые кнопки для каждого чата + const chatButtons: any[][] = []; + + for (const chat of chats.slice(0, 10)) { // Показываем максимум 10 чатов + const unreadBadge = chat.unreadCount > 0 ? ` 🔴${chat.unreadCount}` : ''; + const lastMessagePreview = this.getMessagePreview(chat.lastMessage); + + chatButtons.push([{ + text: `💬 ${chat.otherUserName}${unreadBadge}`, + callback_data: `open_native_chat_${chat.matchId}` + }]); + } + + // Добавляем кнопки управления + chatButtons.push([ + { text: '🔍 Найти людей', callback_data: 'start_browsing' }, + { text: '🏠 Главное меню', callback_data: 'main_menu' } + ]); + + await this.bot.sendMessage( + chatId, + `💬 *Ваши чаты* (${chats.length})\n\n` + + '👆 Выберите чат для общения:', + { + parse_mode: 'Markdown', + reply_markup: { inline_keyboard: chatButtons } + } + ); + } catch (error) { + console.error('Error showing native chats:', error); + await this.bot.sendMessage(chatId, '❌ Ошибка при загрузке чатов'); + } + } + + // Открыть чат в нативном формате + async openNativeChat(chatId: number, telegramId: string, matchId: string): Promise { + try { + const matchInfo = await this.chatService.getMatchInfo(matchId, telegramId); + + if (!matchInfo) { + await this.bot.sendMessage(chatId, '❌ Чат не найден'); + return; + } + + // Отмечаем сообщения как прочитанные + await this.chatService.markMessagesAsRead(matchId, telegramId); + + // Получаем последние сообщения + const messages = await this.chatService.getChatMessages(matchId, 20); + + const otherUserName = matchInfo.otherUserProfile?.name || 'Пользователь'; + + if (messages.length === 0) { + // Первое сообщение в чате + await this.bot.sendMessage( + chatId, + `💕 *Новый матч с ${otherUserName}!*\n\n` + + '🎉 Поздравляем! Вы понравились друг другу!\n\n' + + '💬 *Как начать общение?*\n' + + '• Просто напишите сообщение в этот чат\n' + + '• Ваше сообщение будет доставлено собеседнику\n' + + '• Он получит уведомление о новом сообщении\n\n' + + '💡 Напишите что-нибудь интересное!', + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: '👤 Профиль собеседника', callback_data: `view_chat_profile_${matchId}` }], + [{ text: '← Назад к чатам', callback_data: 'native_chats' }] + ] + } + } + ); + } else { + // Показываем историю сообщений + let chatHistory = `💬 *Чат с ${otherUserName}*\n\n`; + + const currentUserId = await this.profileService.getUserIdByTelegramId(telegramId); + + // Показываем последние 10 сообщений + for (const message of messages.slice(-10)) { + const isFromMe = message.senderId === currentUserId; + const senderIcon = isFromMe ? '✅' : '💌'; + const time = message.createdAt.toLocaleString('ru-RU', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + + chatHistory += `${senderIcon} *${time}*\n`; + chatHistory += `${this.escapeMarkdown(message.content)}\n\n`; + } + + chatHistory += '💡 *Напишите сообщение в этот чат для ответа*'; + + await this.bot.sendMessage( + chatId, + chatHistory, + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [ + { text: '👤 Профиль', callback_data: `view_chat_profile_${matchId}` }, + { text: '📜 История', callback_data: `chat_history_${matchId}` } + ], + [{ text: '💔 Удалить матч', callback_data: `confirm_unmatch_${matchId}` }], + [{ text: '← Назад к чатам', callback_data: 'native_chats' }] + ] + } + } + ); + } + + // Устанавливаем контекст чата для пользователя + this.setUserChatContext(telegramId, matchId); + + } catch (error) { + console.error('Error opening native chat:', error); + await this.bot.sendMessage(chatId, '❌ Ошибка при открытии чата'); + } + } + + // ===== СИСТЕМА УВЕДОМЛЕНИЙ ===== + + // Отправить уведомление о новом сообщении + async sendMessageNotification(receiverTelegramId: string, senderName: string, messagePreview: string, matchId: string): Promise { + try { + const receiverChatId = parseInt(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' }] + ] + } + } + ); + } catch (error) { + console.error('Error sending message notification:', error); + } + } + + // ===== ОБРАБОТКА СООБЩЕНИЙ ===== + + // Обработать входящее сообщение для чата + async handleIncomingChatMessage(msg: any, telegramId: string): Promise { + try { + const currentChatContext = this.getUserChatContext(telegramId); + + if (!currentChatContext) { + return false; // Пользователь не в контексте чата + } + + const { matchId } = currentChatContext; + + // Проверяем, что матч еще активен + const matchInfo = await this.chatService.getMatchInfo(matchId, telegramId); + if (!matchInfo) { + await this.bot.sendMessage(msg.chat.id, '❌ Этот чат больше недоступен'); + this.clearUserChatContext(telegramId); + return true; + } + + // Сохраняем сообщение + const messageId = await this.chatService.sendMessage( + matchId, + telegramId, + msg.text || '[Медиа]', + msg.photo ? 'photo' : 'text', + msg.photo ? msg.photo[msg.photo.length - 1].file_id : undefined + ); + + if (messageId) { + // Подтверждение отправки + await this.bot.sendMessage( + msg.chat.id, + '✅ Сообщение отправлено!', + { + reply_markup: { + inline_keyboard: [ + [{ text: '💬 Продолжить чат', callback_data: `open_native_chat_${matchId}` }], + [{ text: '📱 Все чаты', callback_data: 'native_chats' }] + ] + } + } + ); + + // Отправляем уведомление получателю + const senderProfile = await this.profileService.getProfileByTelegramId(telegramId); + const receiverTelegramId = await this.profileService.getTelegramIdByUserId( + matchInfo.otherUserProfile?.userId || '' + ); + + if (senderProfile && receiverTelegramId) { + const messagePreview = this.getMessagePreview(msg.text || '[Медиа]'); + await this.sendMessageNotification( + receiverTelegramId, + senderProfile.name, + messagePreview, + matchId + ); + } + } else { + await this.bot.sendMessage(msg.chat.id, '❌ Ошибка при отправке сообщения'); + } + + return true; + } catch (error) { + console.error('Error handling incoming chat message:', error); + await this.bot.sendMessage(msg.chat.id, '❌ Ошибка при обработке сообщения'); + return true; + } + } + + // ===== КОНТЕКСТ ЧАТОВ ===== + + private userChatContexts: Map = new Map(); + + private setUserChatContext(telegramId: string, matchId: string): void { + this.userChatContexts.set(telegramId, { + matchId, + timestamp: Date.now() + }); + + // Автоматически очищаем контекст через 30 минут + setTimeout(() => { + const current = this.userChatContexts.get(telegramId); + if (current && current.matchId === matchId) { + this.userChatContexts.delete(telegramId); + } + }, 30 * 60 * 1000); + } + + private getUserChatContext(telegramId: string): { matchId: string } | null { + const context = this.userChatContexts.get(telegramId); + if (!context) return null; + + // Проверяем, что контекст не старше 30 минут + if (Date.now() - context.timestamp > 30 * 60 * 1000) { + this.userChatContexts.delete(telegramId); + return null; + } + + return { matchId: context.matchId }; + } + + private clearUserChatContext(telegramId: string): void { + this.userChatContexts.delete(telegramId); + } + + // ===== ВСПОМОГАТЕЛЬНЫЕ МЕТОДЫ ===== + + private getMessagePreview(message: string | null): string { + if (!message) return '[Пустое сообщение]'; + return message.length > 50 ? message.substring(0, 50) + '...' : message; + } + + private escapeMarkdown(text: string): string { + return text.replace(/[_*[\]()~`>#+=|{}.!-]/g, '\\$&'); + } + + // Показать расширенную историю чата + async showChatHistory(chatId: number, telegramId: string, matchId: string): Promise { + try { + const matchInfo = await this.chatService.getMatchInfo(matchId, telegramId); + + if (!matchInfo) { + await this.bot.sendMessage(chatId, '❌ Чат не найден'); + return; + } + + const messages = await this.chatService.getChatMessages(matchId, 50); + const otherUserName = matchInfo.otherUserProfile?.name || 'Пользователь'; + const currentUserId = await this.profileService.getUserIdByTelegramId(telegramId); + + if (messages.length === 0) { + await this.bot.sendMessage( + chatId, + `📜 *История с ${otherUserName}*\n\n` + + 'История сообщений пуста', + { + parse_mode: 'Markdown', + reply_markup: { + inline_keyboard: [ + [{ text: '← Назад к чату', callback_data: `open_native_chat_${matchId}` }] + ] + } + } + ); + return; + } + + // Разбиваем сообщения на части, если их много + const messagesPerPage = 20; + const totalPages = Math.ceil(messages.length / messagesPerPage); + const currentPage = 1; // Пока показываем только первую страницу + + const startIndex = (currentPage - 1) * messagesPerPage; + const endIndex = startIndex + messagesPerPage; + const pageMessages = messages.slice(startIndex, endIndex); + + let historyText = `📜 *История с ${otherUserName}*\n`; + historyText += `📄 Страница ${currentPage} из ${totalPages}\n\n`; + + for (const message of pageMessages) { + const isFromMe = message.senderId === currentUserId; + const senderIcon = isFromMe ? '✅' : '💌'; + const senderName = isFromMe ? 'Вы' : otherUserName; + const time = message.createdAt.toLocaleString('ru-RU', { + day: '2-digit', + month: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + + historyText += `${senderIcon} **${senderName}** _(${time})_\n`; + historyText += `${this.escapeMarkdown(message.content)}\n\n`; + } + + const keyboard: InlineKeyboardMarkup = { + inline_keyboard: [ + [{ text: '← Назад к чату', callback_data: `open_native_chat_${matchId}` }] + ] + }; + + await this.bot.sendMessage(chatId, historyText, { + parse_mode: 'Markdown', + reply_markup: keyboard + }); + + } catch (error) { + console.error('Error showing chat history:', error); + await this.bot.sendMessage(chatId, '❌ Ошибка при загрузке истории'); + } + } +} diff --git a/src/handlers/messageHandlers.ts b/src/handlers/messageHandlers.ts index 284d03f..68f62e0 100644 --- a/src/handlers/messageHandlers.ts +++ b/src/handlers/messageHandlers.ts @@ -1,6 +1,7 @@ import TelegramBot, { Message, InlineKeyboardMarkup } from 'node-telegram-bot-api'; import { ProfileService } from '../services/profileService'; import { ChatService } from '../services/chatService'; +import { EnhancedChatHandlers } from './enhancedChatHandlers'; // Состояния пользователей для создания профилей interface UserState { @@ -24,6 +25,7 @@ export class MessageHandlers { private bot: TelegramBot; private profileService: ProfileService; private chatService: ChatService; + private enhancedChatHandlers: EnhancedChatHandlers; private userStates: Map = new Map(); private chatStates: Map = new Map(); private profileEditStates: Map = new Map(); @@ -32,6 +34,7 @@ export class MessageHandlers { this.bot = bot; this.profileService = new ProfileService(); this.chatService = new ChatService(); + this.enhancedChatHandlers = new EnhancedChatHandlers(bot); } register(): void { @@ -51,7 +54,12 @@ export class MessageHandlers { const chatState = this.chatStates.get(userId); const profileEditState = this.profileEditStates.get(userId); - // Если пользователь в процессе отправки сообщения в чат + // Проверяем на нативные чаты (прямые сообщения в контексте чата) + if (msg.text && await this.enhancedChatHandlers.handleIncomingChatMessage(msg.chat.id, msg.text)) { + return; // Сообщение обработано как сообщение в чате + } + + // Если пользователь в процессе отправки сообщения в чат (старый способ) if (chatState?.waitingForMessage && msg.text) { await this.handleChatMessage(msg, userId, chatState.matchId); return; diff --git a/src/services/matchingService.ts b/src/services/matchingService.ts index 4572133..4540612 100644 --- a/src/services/matchingService.ts +++ b/src/services/matchingService.ts @@ -44,7 +44,7 @@ export class MatchingService { // Получить профили пользователей const userProfile = await this.profileService.getProfileByTelegramId(telegramId); - const targetProfile = await this.profileService.getProfileByUserId(targetTelegramId); if (!userProfile || !targetProfile) { + const targetProfile = await this.profileService.getProfileByTelegramId(targetTelegramId); if (!userProfile || !targetProfile) { throw new BotError('Profile not found', 'PROFILE_NOT_FOUND', 400); } @@ -82,26 +82,37 @@ export class MatchingService { `, [targetUserId, userId]); if (reciprocalSwipe.rows.length > 0) { - isMatch = true; - const matchId = uuidv4(); - const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].direction === 'super'; + // Проверяем, что матч еще не существует + const existingMatch = await client.query(` + SELECT * FROM matches + WHERE (user1_id = $1 AND user2_id = $2) OR (user1_id = $2 AND user2_id = $1) + `, [userId, targetUserId]); - // Создаем матч - await client.query(` - INSERT INTO matches (id, user1_id, user2_id, matched_at, status) - VALUES ($1, $2, $3, $4, $5) - `, [matchId, userId, targetUserId, new Date(), 'active']); + if (existingMatch.rows.length === 0) { + isMatch = true; + const matchId = uuidv4(); + const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].direction === 'super'; - match = new Match({ - id: matchId, - userId1: userId, - userId2: targetUserId, - createdAt: new Date(), - isActive: true, - isSuperMatch: false, - unreadCount1: 0, - unreadCount2: 0 - }); + // Упорядочиваем пользователей для консистентности + const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId]; + + // Создаем матч + await client.query(` + INSERT INTO matches (id, user1_id, user2_id, matched_at, status) + VALUES ($1, $2, $3, $4, $5) + `, [matchId, user1Id, user2Id, new Date(), 'active']); + + match = new Match({ + id: matchId, + userId1: user1Id, + userId2: user2Id, + createdAt: new Date(), + isActive: true, + isSuperMatch: isSuperMatch, + unreadCount1: 0, + unreadCount2: 0 + }); + } } } }); diff --git a/src/services/notificationService.ts b/src/services/notificationService.ts index 5a528ce..ab60745 100644 --- a/src/services/notificationService.ts +++ b/src/services/notificationService.ts @@ -227,15 +227,65 @@ export class NotificationService { // Проверить, активен ли пользователь в чате private async isUserActiveInChat(userId: string, chatWithUserId: string): Promise { - // TODO: Реализовать проверку активности пользователя - // Можно использовать Redis для хранения состояния активности - return false; + try { + // Проверяем последнее сообщение пользователя в чате + const result = await query(` + SELECT m.created_at + FROM messages m + JOIN matches mt ON m.match_id = mt.id + WHERE (mt.user1_id = $1 OR mt.user2_id = $1) + AND (mt.user1_id = $2 OR mt.user2_id = $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); + + // Считаем активным если последнее сообщение было менее 24 часов назад + return hoursSinceLastMessage < 24; + } catch (error) { + console.error('Error checking user activity:', error); + return false; + } } // Отправить пуш-уведомление (для будущего использования) async sendPushNotification(userId: string, title: string, body: string, data?: any): Promise { - // TODO: Интеграция с Firebase Cloud Messaging или другим сервисом пуш-уведомлений - console.log(`Push notification for ${userId}: ${title} - ${body}`); + try { + // Логируем уведомление + console.log(`📱 Push notification prepared for user ${userId}:`); + console.log(`📋 Title: ${title}`); + console.log(`💬 Body: ${body}`); + if (data) { + console.log(`📊 Data:`, JSON.stringify(data, null, 2)); + } + + // В будущем здесь будет интеграция с Firebase Cloud Messaging + // или другим сервисом пуш-уведомлений: + /* + const message = { + notification: { + title, + body + }, + data: data ? JSON.stringify(data) : undefined, + token: await this.getUserPushToken(userId) + }; + + await admin.messaging().send(message); + console.log(`✅ Push notification sent to user ${userId}`); + */ + + } catch (error) { + console.error(`❌ Error preparing push notification for user ${userId}:`, error); + } } // Получить настройки уведомлений пользователя @@ -300,7 +350,7 @@ export class NotificationService { // Получаем запланированные уведомления const result = await query(` SELECT * FROM scheduled_notifications - WHERE scheduled_at <= $1 AND sent = false + WHERE scheduled_at <= $1 AND processed = false ORDER BY scheduled_at ASC LIMIT 100 `, [new Date()]); @@ -318,10 +368,10 @@ export class NotificationService { // Добавить другие типы уведомлений } - // Отмечаем как отправленное + // Отмечаем как обработанное await query( - 'UPDATE scheduled_notifications SET sent = true, sent_at = $1 WHERE id = $2', - [new Date(), notification.id] + 'UPDATE scheduled_notifications SET processed = true WHERE id = $1', + [notification.id] ); } catch (error) { console.error(`Error processing notification ${notification.id}:`, error); diff --git a/src/services/profileService.ts b/src/services/profileService.ts index d83d73e..7eec66b 100644 --- a/src/services/profileService.ts +++ b/src/services/profileService.ts @@ -95,7 +95,18 @@ export class ProfileService { } return this.mapEntityToProfile(result.rows[0]); - } // Получение UUID пользователя по Telegram ID + } + + // Получение Telegram ID по UUID пользователя + async getTelegramIdByUserId(userId: string): Promise { + const result = await query(` + SELECT telegram_id FROM users WHERE id = $1 + `, [userId]); + + return result.rows.length > 0 ? result.rows[0].telegram_id.toString() : null; + } + + // Получение UUID пользователя по Telegram ID async getUserIdByTelegramId(telegramId: string): Promise { const result = await query(` SELECT id FROM users WHERE telegram_id = $1 @@ -162,7 +173,8 @@ export class ProfileService { case 'photos': case 'interests': updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`); - updateValues.push(JSON.stringify(value)); + // Для PostgreSQL массивы передаем как есть, не как JSON строки + updateValues.push(value); break; case 'location': if (value && typeof value === 'object' && 'latitude' in value) { @@ -336,7 +348,7 @@ export class ProfileService { return { totalLikes: parseInt(likesResult.rows[0].count), totalMatches: parseInt(matchesResult.rows[0].count), - profileViews: 0, // TODO: implement profile views tracking + profileViews: await this.getProfileViewsCount(userId), likesReceived: parseInt(likesReceivedResult.rows[0].count) }; } @@ -499,4 +511,61 @@ export class ProfileService { profile.isVisible = newVisibility; return profile; } + + // Записать просмотр профиля + async recordProfileView(viewerId: string, viewedProfileId: string, viewType: string = 'browse'): Promise { + try { + await query(` + INSERT INTO profile_views (viewer_id, viewed_profile_id, view_type) + VALUES ( + (SELECT id FROM users WHERE telegram_id = $1), + (SELECT id FROM profiles WHERE user_id = (SELECT id FROM users WHERE telegram_id = $2)), + $3 + ) + ON CONFLICT (viewer_id, viewed_profile_id) DO UPDATE + SET viewed_at = CURRENT_TIMESTAMP, view_type = EXCLUDED.view_type + `, [viewerId, viewedProfileId, viewType]); + } catch (error) { + console.error('Error recording profile view:', error); + } + } + + // Получить количество просмотров профиля + async getProfileViewsCount(userId: string): Promise { + try { + const result = await query(` + SELECT COUNT(*) as count + FROM profile_views pv + JOIN profiles p ON pv.viewed_profile_id = p.id + WHERE p.user_id = $1 + `, [userId]); + + return parseInt(result.rows[0].count) || 0; + } catch (error) { + console.error('Error getting profile views count:', error); + return 0; + } + } + + // Получить список кто просматривал профиль + async getProfileViewers(userId: string, limit: number = 10): Promise { + try { + const result = await query(` + SELECT DISTINCT p.*, u.telegram_id, u.username, u.first_name, u.last_name + FROM profile_views pv + JOIN profiles target_p ON pv.viewed_profile_id = target_p.id + JOIN users viewer_u ON pv.viewer_id = viewer_u.id + JOIN profiles p ON viewer_u.id = p.user_id + JOIN users u ON p.user_id = u.id + WHERE target_p.user_id = $1 + ORDER BY pv.viewed_at DESC + LIMIT $2 + `, [userId, limit]); + + return result.rows.map((row: any) => this.mapEntityToProfile(row)); + } catch (error) { + console.error('Error getting profile viewers:', error); + return []; + } + } } \ No newline at end of file diff --git "a/sword123 psql -h localhost -p 5433 -U postgres -d telegram_tinder_bot -c \\d profiles" "b/sword123 psql -h localhost -p 5433 -U postgres -d telegram_tinder_bot -c \\d profiles" new file mode 100644 index 0000000..0603c2a --- /dev/null +++ "b/sword123 psql -h localhost -p 5433 -U postgres -d telegram_tinder_bot -c \\d profiles" @@ -0,0 +1,59 @@ + Table "public.profiles" + Column | Type | Collation | Nullable | Default +---------------------+--------------------------+-----------+----------+--------------------------------- + id | uuid | | not null | uuid_generate_v4() + user_id | uuid | | not null | + name | character varying(255) | | not null | + age | integer | | not null | + gender | character varying(20) | | not null | + looking_for | character varying(20) | | not null | + bio | text | | | + location | character varying(255) | | | + latitude | numeric(10,8) | | | + longitude | numeric(11,8) | | | + photos | text[] | | | + interests | text[] | | | + education | character varying(255) | | | + occupation | character varying(255) | | | + height | integer | | | + smoking | character varying(20) | | | + drinking | character varying(20) | | | + relationship_type | character varying(30) | | | + verification_status | character varying(20) | | | 'unverified'::character varying + is_active | boolean | | | true + is_visible | boolean | | | true + last_active | timestamp with time zone | | | CURRENT_TIMESTAMP + created_at | timestamp with time zone | | | CURRENT_TIMESTAMP + updated_at | timestamp with time zone | | | CURRENT_TIMESTAMP + hobbies | text | | | + religion | character varying(100) | | | + dating_goal | character varying(20) | | | + has_kids | character varying(20) | | | +Indexes: + "profiles_pkey" PRIMARY KEY, btree (id) + "idx_profiles_active" btree (is_active, is_visible) + "idx_profiles_age_gender" btree (age, gender, looking_for) + "idx_profiles_dating_goal" btree (dating_goal) + "idx_profiles_has_kids" btree (has_kids) + "idx_profiles_location" btree (latitude, longitude) WHERE latitude IS NOT NULL AND longitude IS NOT NULL + "idx_profiles_religion" btree (religion) + "idx_profiles_user_id" btree (user_id) + "profiles_user_id_key" UNIQUE CONSTRAINT, btree (user_id) +Check constraints: + "profiles_age_check" CHECK (age >= 18 AND age <= 100) + "profiles_dating_goal_check" CHECK (dating_goal::text = ANY (ARRAY['serious'::character varying, 'casual'::character varying, 'friends'::character varying, 'unsure'::character varying]::text[])) + "profiles_drinking_check" CHECK (drinking::text = ANY (ARRAY['never'::character varying, 'sometimes'::character varying, 'regularly'::character varying]::text[])) + "profiles_gender_check" CHECK (gender::text = ANY (ARRAY['male'::character varying, 'female'::character varying, 'other'::character varying]::text[])) + "profiles_has_kids_check" CHECK (has_kids::text = ANY (ARRAY['have'::character varying, 'want'::character varying, 'dont_want'::character varying, 'unsure'::character varying]::text[])) + "profiles_looking_for_check" CHECK (looking_for::text = ANY (ARRAY['male'::character varying, 'female'::character varying, 'both'::character varying]::text[])) + "profiles_relationship_type_check" CHECK (relationship_type::text = ANY (ARRAY['casual'::character varying, 'serious'::character varying, 'friendship'::character varying, 'anything'::character varying]::text[])) + "profiles_smoking_check" CHECK (smoking::text = ANY (ARRAY['never'::character varying, 'sometimes'::character varying, 'regularly'::character varying]::text[])) + "profiles_verification_status_check" CHECK (verification_status::text = ANY (ARRAY['unverified'::character varying, 'pending'::character varying, 'verified'::character varying]::text[])) +Foreign-key constraints: + "profiles_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +Referenced by: + TABLE "profile_views" CONSTRAINT "profile_views_viewed_profile_id_fkey" FOREIGN KEY (viewed_profile_id) REFERENCES profiles(id) ON DELETE CASCADE +Triggers: + profiles_updated_at BEFORE UPDATE ON profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at() + update_profiles_updated_at BEFORE UPDATE ON profiles FOR EACH ROW EXECUTE FUNCTION update_updated_at_column() +