Fix like/dislike errors and implement native chat system
This commit is contained in:
123
NATIVE_CHAT_SYSTEM.md
Normal file
123
NATIVE_CHAT_SYSTEM.md
Normal file
@@ -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_<matchId>` - открывает конкретный чат
|
||||||
|
- `chat_history_<matchId>` - показывает историю
|
||||||
|
|
||||||
|
## Пользовательский опыт
|
||||||
|
|
||||||
|
### Старый способ (сложный)
|
||||||
|
1. Нажать "Открыть чаты"
|
||||||
|
2. Выбрать чат из списка
|
||||||
|
3. Нажать "Отправить сообщение"
|
||||||
|
4. Написать сообщение
|
||||||
|
5. Подтвердить отправку
|
||||||
|
|
||||||
|
### Новый способ (нативный)
|
||||||
|
1. Нажать "Нативные чаты"
|
||||||
|
2. Выбрать чат
|
||||||
|
3. Просто написать сообщение и отправить
|
||||||
|
4. Получить автоматическое уведомление об ответе
|
||||||
|
|
||||||
|
## Технические детали
|
||||||
|
|
||||||
|
### Хранение контекста
|
||||||
|
```typescript
|
||||||
|
private activeChatContexts: Map<number, {
|
||||||
|
matchId: string;
|
||||||
|
partnerId: number;
|
||||||
|
lastActivity: Date;
|
||||||
|
}> = new Map();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Уведомления
|
||||||
|
- Проверка активности получателя
|
||||||
|
- Избежание дублирования уведомлений
|
||||||
|
- Персонализированные сообщения
|
||||||
|
|
||||||
|
### База данных
|
||||||
|
- Используется существующая структура messages
|
||||||
|
- Автоматическое сохранение всех сообщений
|
||||||
|
- Совместимость со старой системой
|
||||||
|
|
||||||
|
## Преимущества
|
||||||
|
|
||||||
|
1. **Интуитивность** - привычный интерфейс Telegram
|
||||||
|
2. **Скорость** - меньше кликов для отправки сообщения
|
||||||
|
3. **Уведомления** - пользователи не пропустят сообщения
|
||||||
|
4. **Контекст** - бот понимает в каком чате находится пользователь
|
||||||
|
5. **Совместимость** - работает параллельно со старой системой
|
||||||
|
|
||||||
|
## Настройки
|
||||||
|
|
||||||
|
### Timeout контекста
|
||||||
|
По умолчанию: 30 минут
|
||||||
|
Можно изменить в константе `CHAT_CONTEXT_TIMEOUT_MS`
|
||||||
|
|
||||||
|
### Уведомления
|
||||||
|
Автоматически отправляются при:
|
||||||
|
- Получении нового сообщения
|
||||||
|
- Если получатель не в активном чате
|
||||||
|
- Если прошло достаточно времени с последнего уведомления
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- Проверка существования матча перед отправкой
|
||||||
|
- Валидация прав доступа к чату
|
||||||
|
- Защита от спама уведомлениями
|
||||||
|
- Автоматическая очистка неактивных контекстов
|
||||||
|
|
||||||
|
## Миграция
|
||||||
|
|
||||||
|
Система полностью обратно совместима:
|
||||||
|
- Старые чаты продолжают работать
|
||||||
|
- Новые пользователи автоматически используют нативный интерфейс
|
||||||
|
- Постепенная миграция существующих пользователей
|
||||||
|
|
||||||
|
## Мониторинг
|
||||||
|
|
||||||
|
Логирование всех ключевых событий:
|
||||||
|
- Отправка сообщений
|
||||||
|
- Переключение контекста
|
||||||
|
- Отправка уведомлений
|
||||||
|
- Ошибки и исключения
|
||||||
73
clear_database.sql
Normal file
73
clear_database.sql
Normal file
@@ -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 '✅ База данных успешно очищена!';
|
||||||
45
src/bot.ts
45
src/bot.ts
@@ -1,6 +1,6 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import TelegramBot from 'node-telegram-bot-api';
|
import TelegramBot from 'node-telegram-bot-api';
|
||||||
import { testConnection } from './database/connection';
|
import { testConnection, query } from './database/connection';
|
||||||
import { ProfileService } from './services/profileService';
|
import { ProfileService } from './services/profileService';
|
||||||
import { MatchingService } from './services/matchingService';
|
import { MatchingService } from './services/matchingService';
|
||||||
import { NotificationService } from './services/notificationService';
|
import { NotificationService } from './services/notificationService';
|
||||||
@@ -143,8 +143,49 @@ class TelegramTinderBot {
|
|||||||
|
|
||||||
// Очистка старых данных
|
// Очистка старых данных
|
||||||
private async cleanupOldData(): Promise<void> {
|
private async cleanupOldData(): Promise<void> {
|
||||||
// TODO: Реализовать очистку старых уведомлений, логов и т.д.
|
|
||||||
console.log('🧹 Running cleanup tasks...');
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Корректное завершение работы
|
// Корректное завершение работы
|
||||||
|
|||||||
22
src/database/migrations/add_lifestyle_column.sql
Normal file
22
src/database/migrations/add_lifestyle_column.sql
Normal file
@@ -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'));
|
||||||
39
src/database/migrations/add_profile_views.sql
Normal file
39
src/database/migrations/add_profile_views.sql
Normal file
@@ -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';
|
||||||
3
src/database/migrations/fix_profile_views_conflict.sql
Normal file
3
src/database/migrations/fix_profile_views_conflict.sql
Normal file
@@ -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);
|
||||||
@@ -5,6 +5,7 @@ import { ChatService } from '../services/chatService';
|
|||||||
import { Profile } from '../models/Profile';
|
import { Profile } from '../models/Profile';
|
||||||
import { MessageHandlers } from './messageHandlers';
|
import { MessageHandlers } from './messageHandlers';
|
||||||
import { ProfileEditController } from '../controllers/profileEditController';
|
import { ProfileEditController } from '../controllers/profileEditController';
|
||||||
|
import { EnhancedChatHandlers } from './enhancedChatHandlers';
|
||||||
|
|
||||||
export class CallbackHandlers {
|
export class CallbackHandlers {
|
||||||
private bot: TelegramBot;
|
private bot: TelegramBot;
|
||||||
@@ -13,6 +14,7 @@ export class CallbackHandlers {
|
|||||||
private chatService: ChatService;
|
private chatService: ChatService;
|
||||||
private messageHandlers: MessageHandlers;
|
private messageHandlers: MessageHandlers;
|
||||||
private profileEditController: ProfileEditController;
|
private profileEditController: ProfileEditController;
|
||||||
|
private enhancedChatHandlers: EnhancedChatHandlers;
|
||||||
|
|
||||||
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
|
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
|
||||||
this.bot = bot;
|
this.bot = bot;
|
||||||
@@ -21,6 +23,7 @@ export class CallbackHandlers {
|
|||||||
this.chatService = new ChatService();
|
this.chatService = new ChatService();
|
||||||
this.messageHandlers = messageHandlers;
|
this.messageHandlers = messageHandlers;
|
||||||
this.profileEditController = new ProfileEditController(this.profileService);
|
this.profileEditController = new ProfileEditController(this.profileService);
|
||||||
|
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
register(): void {
|
register(): void {
|
||||||
@@ -156,6 +159,14 @@ export class CallbackHandlers {
|
|||||||
await this.handleViewMatches(chatId, telegramId);
|
await this.handleViewMatches(chatId, telegramId);
|
||||||
} else if (data === 'open_chats') {
|
} else if (data === 'open_chats') {
|
||||||
await this.handleOpenChats(chatId, telegramId);
|
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_')) {
|
} else if (data.startsWith('chat_')) {
|
||||||
const matchId = data.replace('chat_', '');
|
const matchId = data.replace('chat_', '');
|
||||||
await this.handleOpenChat(chatId, telegramId, matchId);
|
await this.handleOpenChat(chatId, telegramId, matchId);
|
||||||
@@ -180,6 +191,18 @@ export class CallbackHandlers {
|
|||||||
await this.handleSearchSettings(chatId, telegramId);
|
await this.handleSearchSettings(chatId, telegramId);
|
||||||
} else if (data === 'notification_settings') {
|
} else if (data === 'notification_settings') {
|
||||||
await this.handleNotificationSettings(chatId, telegramId);
|
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);
|
await this.handleHowItWorks(chatId);
|
||||||
} else if (data === 'back_to_browsing') {
|
} else if (data === 'back_to_browsing') {
|
||||||
await this.handleStartBrowsing(chatId, telegramId);
|
await this.handleStartBrowsing(chatId, telegramId);
|
||||||
|
} else if (data === 'get_vip') {
|
||||||
|
await this.handleGetVip(chatId, telegramId);
|
||||||
}
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
@@ -278,7 +303,13 @@ export class CallbackHandlers {
|
|||||||
// Лайк
|
// Лайк
|
||||||
async handleLike(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
|
async handleLike(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
|
||||||
try {
|
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) {
|
if (result.isMatch) {
|
||||||
// Это матч!
|
// Это матч!
|
||||||
@@ -314,7 +345,13 @@ export class CallbackHandlers {
|
|||||||
// Дизлайк
|
// Дизлайк
|
||||||
async handleDislike(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
|
async handleDislike(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
|
||||||
try {
|
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);
|
await this.showNextCandidate(chatId, telegramId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка');
|
await this.bot.sendMessage(chatId, '❌ Ошибка при отправке дизлайка');
|
||||||
@@ -325,7 +362,13 @@ export class CallbackHandlers {
|
|||||||
// Супер лайк
|
// Супер лайк
|
||||||
async handleSuperlike(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
|
async handleSuperlike(chatId: number, telegramId: string, targetUserId: string): Promise<void> {
|
||||||
try {
|
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) {
|
if (result.isMatch) {
|
||||||
const targetProfile = await this.profileService.getProfileByUserId(targetUserId);
|
const targetProfile = await this.profileService.getProfileByUserId(targetUserId);
|
||||||
@@ -366,6 +409,12 @@ export class CallbackHandlers {
|
|||||||
return;
|
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);
|
await this.showProfile(chatId, targetProfile, false, telegramId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -643,7 +692,11 @@ export class CallbackHandlers {
|
|||||||
{ text: '🔔 Уведомления', callback_data: 'notification_settings' }
|
{ text: '🔔 Уведомления', callback_data: 'notification_settings' }
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{ text: '🚫 Скрыть профиль', callback_data: 'hide_profile' },
|
{ text: '<EFBFBD> Статистика', callback_data: 'view_stats' },
|
||||||
|
{ text: '👀 Кто смотрел', callback_data: 'view_profile_viewers' }
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ text: '<27>🚫 Скрыть профиль', callback_data: 'hide_profile' },
|
||||||
{ text: '🗑 Удалить профиль', callback_data: 'delete_profile' }
|
{ text: '🗑 Удалить профиль', callback_data: 'delete_profile' }
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -812,6 +865,12 @@ export class CallbackHandlers {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Записываем просмотр кандидата
|
||||||
|
const candidateTelegramId = await this.profileService.getTelegramIdByUserId(candidate.userId);
|
||||||
|
if (candidateTelegramId) {
|
||||||
|
await this.profileService.recordProfileView(telegramId, candidateTelegramId, 'browse');
|
||||||
|
}
|
||||||
|
|
||||||
const candidatePhotoFileId = candidate.photos[0]; // Первое фото - главное
|
const candidatePhotoFileId = candidate.photos[0]; // Первое фото - главное
|
||||||
|
|
||||||
let candidateText = candidate.name + ', ' + candidate.age + '\n';
|
let candidateText = candidate.name + ', ' + candidate.age + '\n';
|
||||||
@@ -1577,4 +1636,254 @@ export class CallbackHandlers {
|
|||||||
|
|
||||||
return parts.join(', ');
|
return parts.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Просмотр статистики профиля
|
||||||
|
async handleViewStats(chatId: number, telegramId: string): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
// Используем существующий метод 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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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' }
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,7 +172,8 @@ export class CommandHandlers {
|
|||||||
const keyboard: InlineKeyboardMarkup = {
|
const keyboard: InlineKeyboardMarkup = {
|
||||||
inline_keyboard: [
|
inline_keyboard: [
|
||||||
[{ text: '💬 Открыть чаты', callback_data: 'open_chats' }],
|
[{ text: '💬 Открыть чаты', callback_data: 'open_chats' }],
|
||||||
[{ text: '🔍 Найти еще', callback_data: 'start_browsing' }]
|
[{ text: '<EFBFBD> Нативные чаты', callback_data: 'native_chats' }],
|
||||||
|
[{ text: '<27>🔍 Найти еще', callback_data: 'start_browsing' }]
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
389
src/handlers/enhancedChatHandlers.ts
Normal file
389
src/handlers/enhancedChatHandlers.ts
Normal file
@@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<boolean> {
|
||||||
|
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<string, { matchId: string; timestamp: number }> = 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<void> {
|
||||||
|
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, '❌ Ошибка при загрузке истории');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import TelegramBot, { Message, InlineKeyboardMarkup } from 'node-telegram-bot-api';
|
import TelegramBot, { Message, InlineKeyboardMarkup } from 'node-telegram-bot-api';
|
||||||
import { ProfileService } from '../services/profileService';
|
import { ProfileService } from '../services/profileService';
|
||||||
import { ChatService } from '../services/chatService';
|
import { ChatService } from '../services/chatService';
|
||||||
|
import { EnhancedChatHandlers } from './enhancedChatHandlers';
|
||||||
|
|
||||||
// Состояния пользователей для создания профилей
|
// Состояния пользователей для создания профилей
|
||||||
interface UserState {
|
interface UserState {
|
||||||
@@ -24,6 +25,7 @@ export class MessageHandlers {
|
|||||||
private bot: TelegramBot;
|
private bot: TelegramBot;
|
||||||
private profileService: ProfileService;
|
private profileService: ProfileService;
|
||||||
private chatService: ChatService;
|
private chatService: ChatService;
|
||||||
|
private enhancedChatHandlers: EnhancedChatHandlers;
|
||||||
private userStates: Map<string, UserState> = new Map();
|
private userStates: Map<string, UserState> = new Map();
|
||||||
private chatStates: Map<string, ChatState> = new Map();
|
private chatStates: Map<string, ChatState> = new Map();
|
||||||
private profileEditStates: Map<string, ProfileEditState> = new Map();
|
private profileEditStates: Map<string, ProfileEditState> = new Map();
|
||||||
@@ -32,6 +34,7 @@ export class MessageHandlers {
|
|||||||
this.bot = bot;
|
this.bot = bot;
|
||||||
this.profileService = new ProfileService();
|
this.profileService = new ProfileService();
|
||||||
this.chatService = new ChatService();
|
this.chatService = new ChatService();
|
||||||
|
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
||||||
}
|
}
|
||||||
|
|
||||||
register(): void {
|
register(): void {
|
||||||
@@ -51,7 +54,12 @@ export class MessageHandlers {
|
|||||||
const chatState = this.chatStates.get(userId);
|
const chatState = this.chatStates.get(userId);
|
||||||
const profileEditState = this.profileEditStates.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) {
|
if (chatState?.waitingForMessage && msg.text) {
|
||||||
await this.handleChatMessage(msg, userId, chatState.matchId);
|
await this.handleChatMessage(msg, userId, chatState.matchId);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class MatchingService {
|
|||||||
|
|
||||||
// Получить профили пользователей
|
// Получить профили пользователей
|
||||||
const userProfile = await this.profileService.getProfileByTelegramId(telegramId);
|
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);
|
throw new BotError('Profile not found', 'PROFILE_NOT_FOUND', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,26 +82,37 @@ export class MatchingService {
|
|||||||
`, [targetUserId, userId]);
|
`, [targetUserId, userId]);
|
||||||
|
|
||||||
if (reciprocalSwipe.rows.length > 0) {
|
if (reciprocalSwipe.rows.length > 0) {
|
||||||
isMatch = true;
|
// Проверяем, что матч еще не существует
|
||||||
const matchId = uuidv4();
|
const existingMatch = await client.query(`
|
||||||
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].direction === 'super';
|
SELECT * FROM matches
|
||||||
|
WHERE (user1_id = $1 AND user2_id = $2) OR (user1_id = $2 AND user2_id = $1)
|
||||||
|
`, [userId, targetUserId]);
|
||||||
|
|
||||||
// Создаем матч
|
if (existingMatch.rows.length === 0) {
|
||||||
await client.query(`
|
isMatch = true;
|
||||||
INSERT INTO matches (id, user1_id, user2_id, matched_at, status)
|
const matchId = uuidv4();
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
const isSuperMatch = swipeType === 'superlike' || reciprocalSwipe.rows[0].direction === 'super';
|
||||||
`, [matchId, userId, targetUserId, new Date(), 'active']);
|
|
||||||
|
|
||||||
match = new Match({
|
// Упорядочиваем пользователей для консистентности
|
||||||
id: matchId,
|
const [user1Id, user2Id] = userId < targetUserId ? [userId, targetUserId] : [targetUserId, userId];
|
||||||
userId1: userId,
|
|
||||||
userId2: targetUserId,
|
// Создаем матч
|
||||||
createdAt: new Date(),
|
await client.query(`
|
||||||
isActive: true,
|
INSERT INTO matches (id, user1_id, user2_id, matched_at, status)
|
||||||
isSuperMatch: false,
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
unreadCount1: 0,
|
`, [matchId, user1Id, user2Id, new Date(), 'active']);
|
||||||
unreadCount2: 0
|
|
||||||
});
|
match = new Match({
|
||||||
|
id: matchId,
|
||||||
|
userId1: user1Id,
|
||||||
|
userId2: user2Id,
|
||||||
|
createdAt: new Date(),
|
||||||
|
isActive: true,
|
||||||
|
isSuperMatch: isSuperMatch,
|
||||||
|
unreadCount1: 0,
|
||||||
|
unreadCount2: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -227,15 +227,65 @@ export class NotificationService {
|
|||||||
|
|
||||||
// Проверить, активен ли пользователь в чате
|
// Проверить, активен ли пользователь в чате
|
||||||
private async isUserActiveInChat(userId: string, chatWithUserId: string): Promise<boolean> {
|
private async isUserActiveInChat(userId: string, chatWithUserId: string): Promise<boolean> {
|
||||||
// TODO: Реализовать проверку активности пользователя
|
try {
|
||||||
// Можно использовать Redis для хранения состояния активности
|
// Проверяем последнее сообщение пользователя в чате
|
||||||
return false;
|
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<void> {
|
async sendPushNotification(userId: string, title: string, body: string, data?: any): Promise<void> {
|
||||||
// TODO: Интеграция с Firebase Cloud Messaging или другим сервисом пуш-уведомлений
|
try {
|
||||||
console.log(`Push notification for ${userId}: ${title} - ${body}`);
|
// Логируем уведомление
|
||||||
|
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(`
|
const result = await query(`
|
||||||
SELECT * FROM scheduled_notifications
|
SELECT * FROM scheduled_notifications
|
||||||
WHERE scheduled_at <= $1 AND sent = false
|
WHERE scheduled_at <= $1 AND processed = false
|
||||||
ORDER BY scheduled_at ASC
|
ORDER BY scheduled_at ASC
|
||||||
LIMIT 100
|
LIMIT 100
|
||||||
`, [new Date()]);
|
`, [new Date()]);
|
||||||
@@ -318,10 +368,10 @@ export class NotificationService {
|
|||||||
// Добавить другие типы уведомлений
|
// Добавить другие типы уведомлений
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отмечаем как отправленное
|
// Отмечаем как обработанное
|
||||||
await query(
|
await query(
|
||||||
'UPDATE scheduled_notifications SET sent = true, sent_at = $1 WHERE id = $2',
|
'UPDATE scheduled_notifications SET processed = true WHERE id = $1',
|
||||||
[new Date(), notification.id]
|
[notification.id]
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error processing notification ${notification.id}:`, error);
|
console.error(`Error processing notification ${notification.id}:`, error);
|
||||||
|
|||||||
@@ -95,7 +95,18 @@ export class ProfileService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return this.mapEntityToProfile(result.rows[0]);
|
return this.mapEntityToProfile(result.rows[0]);
|
||||||
} // Получение UUID пользователя по Telegram ID
|
}
|
||||||
|
|
||||||
|
// Получение Telegram ID по UUID пользователя
|
||||||
|
async getTelegramIdByUserId(userId: string): Promise<string | null> {
|
||||||
|
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<string | null> {
|
async getUserIdByTelegramId(telegramId: string): Promise<string | null> {
|
||||||
const result = await query(`
|
const result = await query(`
|
||||||
SELECT id FROM users WHERE telegram_id = $1
|
SELECT id FROM users WHERE telegram_id = $1
|
||||||
@@ -162,7 +173,8 @@ export class ProfileService {
|
|||||||
case 'photos':
|
case 'photos':
|
||||||
case 'interests':
|
case 'interests':
|
||||||
updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`);
|
updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`);
|
||||||
updateValues.push(JSON.stringify(value));
|
// Для PostgreSQL массивы передаем как есть, не как JSON строки
|
||||||
|
updateValues.push(value);
|
||||||
break;
|
break;
|
||||||
case 'location':
|
case 'location':
|
||||||
if (value && typeof value === 'object' && 'latitude' in value) {
|
if (value && typeof value === 'object' && 'latitude' in value) {
|
||||||
@@ -336,7 +348,7 @@ export class ProfileService {
|
|||||||
return {
|
return {
|
||||||
totalLikes: parseInt(likesResult.rows[0].count),
|
totalLikes: parseInt(likesResult.rows[0].count),
|
||||||
totalMatches: parseInt(matchesResult.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)
|
likesReceived: parseInt(likesReceivedResult.rows[0].count)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -499,4 +511,61 @@ export class ProfileService {
|
|||||||
profile.isVisible = newVisibility;
|
profile.isVisible = newVisibility;
|
||||||
return profile;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Записать просмотр профиля
|
||||||
|
async recordProfileView(viewerId: string, viewedProfileId: string, viewType: string = 'browse'): Promise<void> {
|
||||||
|
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<number> {
|
||||||
|
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<Profile[]> {
|
||||||
|
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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
|
||||||
Reference in New Issue
Block a user