feat: add VIP search option and profile editing functionality

- Added a new button for ' VIP поиск' in command handlers.
- Implemented profile editing states and methods in message handlers.
- Enhanced profile model to include hobbies, religion, dating goals, and lifestyle preferences.
- Updated profile service to handle new fields and ensure proper database interactions.
- Introduced a VIP function in matching service to find candidates based on dating goals.
This commit is contained in:
2025-09-12 22:13:26 +09:00
parent 17efb2fb53
commit 8893b4ad22
9 changed files with 1528 additions and 50 deletions

View File

@@ -0,0 +1,294 @@
import { ProfileService } from '../services/profileService';
import { Profile } from '../models/Profile';
import TelegramBot from 'node-telegram-bot-api';
export class ProfileEditController {
private profileService: ProfileService;
constructor(profileService: ProfileService) {
this.profileService = profileService;
}
// Показать главное меню редактирования профиля
async showProfileEditMenu(bot: TelegramBot, chatId: number, telegramId: number): Promise<void> {
try {
const profile = await this.profileService.getProfileByTelegramId(telegramId.toString());
if (!profile) {
await bot.sendMessage(chatId, '❌ Профиль не найден. Сначала создайте профиль.');
return;
}
const keyboard = {
inline_keyboard: [
[
{ text: '📝 Изменить имя', callback_data: 'edit_name' },
{ text: '🎂 Изменить возраст', callback_data: 'edit_age' }
],
[
{ text: '📖 Изменить "О себе"', callback_data: 'edit_bio' },
{ text: '🎯 Хобби', callback_data: 'edit_hobbies' }
],
[
{ text: '📷 Управление фото', callback_data: 'manage_photos' },
{ text: '🏙️ Город', callback_data: 'edit_city' }
],
[
{ text: '💼 Работа', callback_data: 'edit_job' },
{ text: '🎓 Образование', callback_data: 'edit_education' }
],
[
{ text: '📏 Рост', callback_data: 'edit_height' },
{ text: '🕊️ Религия', callback_data: 'edit_religion' }
],
[
{ text: '💕 Цель знакомства', callback_data: 'edit_dating_goal' },
{ text: '🚬 Образ жизни', callback_data: 'edit_lifestyle' }
],
[
{ text: '⚙️ Настройки поиска', callback_data: 'edit_search_preferences' }
],
[
{ text: '👀 Предпросмотр профиля', callback_data: 'preview_profile' }
],
[
{ text: '⬅️ Назад в главное меню', callback_data: 'main_menu' }
]
]
};
const profileText = this.getProfileSummary(profile);
await bot.sendMessage(chatId,
`🛠️ *Редактирование профиля*\n\n${profileText}\n\n*Выберите что хотите изменить:*`,
{
parse_mode: 'Markdown',
reply_markup: keyboard
}
);
} catch (error) {
console.error('Error showing profile edit menu:', error);
await bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке профиля.');
}
}
// Показать меню управления фотографиями
async showPhotoManagementMenu(bot: TelegramBot, chatId: number, telegramId: number): Promise<void> {
try {
const profile = await this.profileService.getProfileByTelegramId(telegramId.toString());
if (!profile) {
await bot.sendMessage(chatId, '❌ Профиль не найден.');
return;
}
const keyboard = {
inline_keyboard: [
[
{ text: ' Добавить фото', callback_data: 'add_photo' }
],
[
{ text: '🗑️ Удалить фото', callback_data: 'delete_photo' },
{ text: '⭐ Главное фото', callback_data: 'set_main_photo' }
],
[
{ text: '⬅️ Назад', callback_data: 'edit_profile' }
]
]
};
let photoText = '📷 *Управление фотографиями*\n\n';
if (profile.photos.length === 0) {
photoText += 'У вас пока нет фотографий.\n';
} else {
photoText += `Количество фото: ${profile.photos.length}/9\n`;
if (profile.getMainPhoto()) {
photoText += '⭐ Главное фото установлено\n';
}
}
await bot.sendMessage(chatId, photoText, {
parse_mode: 'Markdown',
reply_markup: keyboard
});
// Если есть фото, покажем их
if (profile.photos.length > 0) {
for (let i = 0; i < Math.min(profile.photos.length, 3); i++) {
const caption = i === 0 ? '⭐ Главное фото' : `Фото ${i + 1}`;
await bot.sendPhoto(chatId, profile.photos[i], { caption });
}
}
} catch (error) {
console.error('Error showing photo management menu:', error);
await bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке фотографий.');
}
}
// Предпросмотр профиля
async showProfilePreview(bot: TelegramBot, chatId: number, telegramId: number): Promise<void> {
try {
const profile = await this.profileService.getProfileByTelegramId(telegramId.toString());
if (!profile) {
await bot.sendMessage(chatId, '❌ Профиль не найден.');
return;
}
const displayProfile = profile.getDisplayProfile();
let previewText = `👤 *${displayProfile.name}, ${displayProfile.age}*\n\n`;
if (displayProfile.bio) {
previewText += `📖 *О себе:*\n${displayProfile.bio}\n\n`;
}
if (displayProfile.hobbies) {
previewText += `🎯 *Хобби:* ${displayProfile.hobbies}\n\n`;
}
if (displayProfile.city) {
previewText += `🏙️ *Город:* ${displayProfile.city}\n`;
}
if (displayProfile.job) {
previewText += `💼 *Работа:* ${displayProfile.job}\n`;
}
if (displayProfile.education) {
previewText += `🎓 *Образование:* ${displayProfile.education}\n`;
}
if (displayProfile.height) {
previewText += `📏 *Рост:* ${displayProfile.height} см\n`;
}
if (displayProfile.religion) {
previewText += `🕊️ *Религия:* ${displayProfile.religion}\n`;
}
if (displayProfile.datingGoal) {
const goalText = this.getDatingGoalText(displayProfile.datingGoal);
previewText += `💕 *Цель знакомства:* ${goalText}\n`;
}
if (displayProfile.lifestyle) {
previewText += `\n🚬 *Образ жизни:*\n`;
if (displayProfile.lifestyle.smoking) {
previewText += `Курение: ${this.getLifestyleText('smoking', displayProfile.lifestyle.smoking)}\n`;
}
if (displayProfile.lifestyle.drinking) {
previewText += `Алкоголь: ${this.getLifestyleText('drinking', displayProfile.lifestyle.drinking)}\n`;
}
if (displayProfile.lifestyle.kids) {
previewText += `Дети: ${this.getLifestyleText('kids', displayProfile.lifestyle.kids)}\n`;
}
}
const keyboard = {
inline_keyboard: [
[
{ text: '✏️ Редактировать', callback_data: 'edit_profile' },
{ text: '⬅️ Главное меню', callback_data: 'main_menu' }
]
]
};
// Отправляем главное фото, если есть
if (displayProfile.photos.length > 0) {
await bot.sendPhoto(chatId, displayProfile.photos[0], {
caption: previewText,
parse_mode: 'Markdown',
reply_markup: keyboard
});
} else {
await bot.sendMessage(chatId, previewText + '\n\n⚠ *Добавьте фотографии для лучшего результата!*', {
parse_mode: 'Markdown',
reply_markup: keyboard
});
}
} catch (error) {
console.error('Error showing profile preview:', error);
await bot.sendMessage(chatId, '❌ Произошла ошибка при загрузке предпросмотра.');
}
}
// Получить краткую сводку профиля
private getProfileSummary(profile: Profile): string {
let summary = `👤 *${profile.name}, ${profile.age}*\n`;
const completeness = this.calculateProfileCompleteness(profile);
summary += `📊 Заполненность профиля: ${completeness}%\n\n`;
summary += `📷 Фото: ${profile.photos.length}/9\n`;
summary += `📖 О себе: ${profile.bio ? '✅' : '❌'}\n`;
summary += `🎯 Хобби: ${profile.hobbies ? '✅' : '❌'}\n`;
summary += `🏙️ Город: ${profile.city ? '✅' : '❌'}\n`;
summary += `💼 Работа: ${profile.job ? '✅' : '❌'}\n`;
return summary;
}
// Рассчитать процент заполненности профиля
private calculateProfileCompleteness(profile: Profile): number {
const fields = [
profile.name,
profile.age,
profile.photos.length > 0,
profile.bio,
profile.hobbies,
profile.city,
profile.job,
profile.education,
profile.height,
profile.religion,
profile.datingGoal
];
const filledFields = fields.filter(field =>
field !== null && field !== undefined && field !== ''
).length;
return Math.round((filledFields / fields.length) * 100);
}
// Получить текст цели знакомства
private getDatingGoalText(goal: string): string {
const goals: { [key: string]: string } = {
'serious': 'Серьёзные отношения',
'casual': 'Лёгкие отношения',
'friends': 'Дружба',
'unsure': 'Пока не определился',
'one_night': 'Отношения на одну ночь',
'fwb': 'Друзья с привилегиями',
'marriage_abroad': 'Брак с переездом',
'sugar': 'Спонсорство',
'polyamory': 'Полиамория'
};
return goals[goal] || goal;
}
// Получить текст образа жизни
private getLifestyleText(type: string, value: string): string {
const lifestyleTexts: { [key: string]: { [key: string]: string } } = {
smoking: {
'never': 'Не курю',
'sometimes': 'Иногда',
'regularly': 'Регулярно'
},
drinking: {
'never': 'Не пью',
'sometimes': 'Иногда',
'regularly': 'Регулярно'
},
kids: {
'have': 'Есть дети',
'want': 'Хочу детей',
'dont_want': 'Не хочу детей',
'unsure': 'Пока не знаю'
}
};
return lifestyleTexts[type]?.[value] || value;
}
}

View File

@@ -0,0 +1,40 @@
-- Add new profile fields for enhanced profile management
-- Add hobbies field
ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS hobbies TEXT;
-- Add religion field
ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS religion VARCHAR(100);
-- Add dating goal field
ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS dating_goal VARCHAR(20)
CHECK (dating_goal IN ('serious', 'casual', 'friends', 'unsure'));
-- Add lifestyle preferences
ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS has_kids VARCHAR(20)
CHECK (has_kids IN ('have', 'want', 'dont_want', 'unsure'));
-- Create indexes for better performance
CREATE INDEX IF NOT EXISTS idx_profiles_dating_goal ON profiles(dating_goal);
CREATE INDEX IF NOT EXISTS idx_profiles_religion ON profiles(religion);
CREATE INDEX IF NOT EXISTS idx_profiles_has_kids ON profiles(has_kids);
-- Update updated_at timestamp function
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ language 'plpgsql';
-- Apply trigger to profiles table if not exists
DROP TRIGGER IF EXISTS update_profiles_updated_at ON profiles;
CREATE TRIGGER update_profiles_updated_at
BEFORE UPDATE ON profiles
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();

View File

@@ -0,0 +1,46 @@
-- Миграция для добавления таблицы запланированных уведомлений
-- Версия: 2025-09-12-002
-- Создание таблицы запланированных уведомлений
CREATE TABLE IF NOT EXISTS scheduled_notifications (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL, -- 'inactivity_reminder', 'likes_summary', 'match_reminder', etc.
scheduled_at TIMESTAMP WITH TIME ZONE NOT NULL,
sent BOOLEAN DEFAULT FALSE,
data JSONB, -- Дополнительные данные для уведомления
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
-- Индексы для оптимизации запросов
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_user_id ON scheduled_notifications(user_id);
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_scheduled_at ON scheduled_notifications(scheduled_at) WHERE sent = FALSE;
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_type ON scheduled_notifications(type);
CREATE INDEX IF NOT EXISTS idx_scheduled_notifications_sent ON scheduled_notifications(sent);
-- Триггер для обновления updated_at
CREATE TRIGGER scheduled_notifications_updated_at BEFORE UPDATE ON scheduled_notifications
FOR EACH ROW EXECUTE FUNCTION update_updated_at();
-- Функция для очистки старых уведомлений (старше 30 дней)
CREATE OR REPLACE FUNCTION cleanup_old_notifications()
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
DELETE FROM scheduled_notifications
WHERE sent = TRUE
AND created_at < CURRENT_TIMESTAMP - INTERVAL '30 days';
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RETURN deleted_count;
END;
$$ LANGUAGE plpgsql;
-- Комментарии к таблице и колонкам
COMMENT ON TABLE scheduled_notifications IS 'Таблица для хранения запланированных уведомлений';
COMMENT ON COLUMN scheduled_notifications.type IS 'Тип уведомления: inactivity_reminder, likes_summary, match_reminder и т.д.';
COMMENT ON COLUMN scheduled_notifications.scheduled_at IS 'Время, когда должно быть отправлено уведомление';
COMMENT ON COLUMN scheduled_notifications.sent IS 'Флаг, указывающий было ли отправлено уведомление';
COMMENT ON COLUMN scheduled_notifications.data IS 'JSON данные для кастомизации уведомления';

View File

@@ -4,6 +4,7 @@ import { MatchingService } from '../services/matchingService';
import { ChatService } from '../services/chatService';
import { Profile } from '../models/Profile';
import { MessageHandlers } from './messageHandlers';
import { ProfileEditController } from '../controllers/profileEditController';
export class CallbackHandlers {
private bot: TelegramBot;
@@ -11,6 +12,7 @@ export class CallbackHandlers {
private matchingService: MatchingService;
private chatService: ChatService;
private messageHandlers: MessageHandlers;
private profileEditController: ProfileEditController;
constructor(bot: TelegramBot, messageHandlers: MessageHandlers) {
this.bot = bot;
@@ -18,6 +20,7 @@ export class CallbackHandlers {
this.matchingService = new MatchingService();
this.chatService = new ChatService();
this.messageHandlers = messageHandlers;
this.profileEditController = new ProfileEditController(this.profileService);
}
register(): void {
@@ -44,11 +47,91 @@ export class CallbackHandlers {
await this.handleEditProfile(chatId, telegramId);
} else if (data === 'manage_photos') {
await this.handleManagePhotos(chatId, telegramId);
} else if (data === 'preview_profile') {
await this.handlePreviewProfile(chatId, telegramId);
}
// Редактирование полей профиля
else if (data === 'edit_name') {
await this.handleEditName(chatId, telegramId);
} else if (data === 'edit_age') {
await this.handleEditAge(chatId, telegramId);
} else if (data === 'edit_bio') {
await this.handleEditBio(chatId, telegramId);
} else if (data === 'edit_hobbies') {
await this.handleEditHobbies(chatId, telegramId);
} else if (data === 'edit_city') {
await this.handleEditCity(chatId, telegramId);
} else if (data === 'edit_job') {
await this.handleEditJob(chatId, telegramId);
} else if (data === 'edit_education') {
await this.handleEditEducation(chatId, telegramId);
} else if (data === 'edit_height') {
await this.handleEditHeight(chatId, telegramId);
} else if (data === 'edit_religion') {
await this.handleEditReligion(chatId, telegramId);
} else if (data === 'edit_dating_goal') {
await this.handleEditDatingGoal(chatId, telegramId);
} else if (data === 'edit_lifestyle') {
await this.handleEditLifestyle(chatId, telegramId);
} else if (data === 'edit_search_preferences') {
await this.handleEditSearchPreferences(chatId, telegramId);
}
// Управление фотографиями
else if (data === 'add_photo') {
await this.handleAddPhoto(chatId, telegramId);
} else if (data === 'delete_photo') {
await this.handleDeletePhoto(chatId, telegramId);
} else if (data === 'set_main_photo') {
await this.handleSetMainPhoto(chatId, telegramId);
} else if (data.startsWith('delete_photo_')) {
const photoIndex = parseInt(data.replace('delete_photo_', ''));
await this.handleDeletePhotoByIndex(chatId, telegramId, photoIndex);
} else if (data.startsWith('set_main_photo_')) {
const photoIndex = parseInt(data.replace('set_main_photo_', ''));
await this.handleSetMainPhotoByIndex(chatId, telegramId, photoIndex);
}
// Цели знакомства
else if (data.startsWith('set_dating_goal_')) {
const goal = data.replace('set_dating_goal_', '');
await this.handleSetDatingGoal(chatId, telegramId, goal);
}
// Образ жизни
else if (data === 'edit_smoking') {
await this.handleEditSmoking(chatId, telegramId);
} else if (data === 'edit_drinking') {
await this.handleEditDrinking(chatId, telegramId);
} else if (data === 'edit_kids') {
await this.handleEditKids(chatId, telegramId);
} else if (data.startsWith('set_smoking_')) {
const value = data.replace('set_smoking_', '');
await this.handleSetLifestyle(chatId, telegramId, 'smoking', value);
} else if (data.startsWith('set_drinking_')) {
const value = data.replace('set_drinking_', '');
await this.handleSetLifestyle(chatId, telegramId, 'drinking', value);
} else if (data.startsWith('set_kids_')) {
const value = data.replace('set_kids_', '');
await this.handleSetLifestyle(chatId, telegramId, 'kids', value);
}
// Настройки поиска
else if (data === 'edit_age_range') {
await this.handleEditAgeRange(chatId, telegramId);
} else if (data === 'edit_distance') {
await this.handleEditDistance(chatId, telegramId);
}
// Просмотр анкет и свайпы
else if (data === 'start_browsing') {
await this.handleStartBrowsing(chatId, telegramId);
} else if (data === 'vip_search') {
await this.handleVipSearch(chatId, telegramId);
} else if (data.startsWith('search_by_goal_')) {
const goal = data.replace('search_by_goal_', '');
await this.handleSearchByGoal(chatId, telegramId, goal);
} else if (data === 'next_candidate') {
await this.handleNextCandidate(chatId, telegramId);
} else if (data.startsWith('like_')) {
@@ -167,52 +250,12 @@ export class CallbackHandlers {
// Редактирование профиля
async handleEditProfile(chatId: number, telegramId: string): Promise<void> {
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[
{ text: '📝 Имя', callback_data: 'edit_name' },
{ text: '📅 Возраст', callback_data: 'edit_age' }
],
[
{ text: '📍 Город', callback_data: 'edit_city' },
{ text: '💼 Работа', callback_data: 'edit_job' }
],
[
{ text: '📖 О себе', callback_data: 'edit_bio' },
{ text: '🎯 Интересы', callback_data: 'edit_interests' }
],
[{ text: '👈 Назад к профилю', callback_data: 'view_my_profile' }]
]
};
await this.bot.sendMessage(
chatId,
'✏️ Что хотите изменить в профиле?',
{ reply_markup: keyboard }
);
await this.profileEditController.showProfileEditMenu(this.bot, chatId, parseInt(telegramId));
}
// Управление фотографиями
async handleManagePhotos(chatId: number, telegramId: string): Promise<void> {
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[
{ text: '📷 Добавить фото', callback_data: 'add_photo' },
{ text: '🗑 Удалить фото', callback_data: 'delete_photo' }
],
[
{ text: '⭐ Сделать главным', callback_data: 'set_main_photo' },
{ text: '🔄 Изменить порядок', callback_data: 'reorder_photos' }
],
[{ text: '👈 Назад к профилю', callback_data: 'view_my_profile' }]
]
};
await this.bot.sendMessage(
chatId,
'📸 Управление фотографиями\n\nВыберите действие:',
{ reply_markup: keyboard }
);
await this.profileEditController.showPhotoManagementMenu(this.bot, chatId, parseInt(telegramId));
}
// Начать просмотр анкет
@@ -669,10 +712,33 @@ export class CallbackHandlers {
if (profile.job) profileText += '💼 ' + profile.job + '\n';
if (profile.education) profileText += '🎓 ' + profile.education + '\n';
if (profile.height) profileText += '📏 ' + profile.height + ' см\n';
if (profile.religion) profileText += '🕊️ ' + profile.religion + '\n';
// Цель знакомства
if (profile.datingGoal) {
const goalText = this.getDatingGoalText(profile.datingGoal);
profileText += '💕 ' + goalText + '\n';
}
// Образ жизни
if (profile.lifestyle) {
const lifestyleText = this.getLifestyleText(profile.lifestyle);
if (lifestyleText) {
profileText += lifestyleText + '\n';
}
}
profileText += '\n📝 ' + (profile.bio || 'Описание не указано') + '\n';
// Хобби с хэштегами
if (profile.hobbies && profile.hobbies.trim()) {
const hobbiesArray = profile.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
profileText += '\n🎯 ' + formattedHobbies + '\n';
}
if (profile.interests.length > 0) {
profileText += '\n🎯 Интересы: ' + profile.interests.join(', ');
profileText += '\n<EFBFBD> Интересы: ' + profile.interests.join(', ');
}
let keyboard: InlineKeyboardMarkup;
@@ -752,13 +818,18 @@ export class CallbackHandlers {
candidateText += '📍 ' + (candidate.city || 'Не указан') + '\n';
if (candidate.job) candidateText += '💼 ' + candidate.job + '\n';
if (candidate.education) candidateText += '🎓 ' + candidate.education + '\n';
if (candidate.height) candidateText += '<EFBFBD><EFBFBD> ' + candidate.height + ' см\n';
if (candidate.height) candidateText += '📏 ' + candidate.height + ' см\n';
if (candidate.religion) candidateText += '🕊️ ' + candidate.religion + '\n';
candidateText += '\n📝 ' + (candidate.bio || 'Описание отсутствует') + '\n';
if (candidate.interests.length > 0) {
candidateText += '\n🎯 Интересы: ' + candidate.interests.join(', ');
}
if (candidate.hobbies) {
candidateText += '\n🎮 Хобби: ' + candidate.hobbies;
}
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[
@@ -799,4 +870,711 @@ export class CallbackHandlers {
});
}
}
// ===== НОВЫЕ МЕТОДЫ ДЛЯ РЕДАКТИРОВАНИЯ ПРОФИЛЯ =====
// Предпросмотр профиля
async handlePreviewProfile(chatId: number, telegramId: string): Promise<void> {
await this.profileEditController.showProfilePreview(this.bot, chatId, parseInt(telegramId));
}
// Редактирование имени
async handleEditName(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'name');
await this.bot.sendMessage(chatId, '📝 *Введите ваше новое имя:*\n\nНапример: Анна', {
parse_mode: 'Markdown'
});
}
// Редактирование возраста
async handleEditAge(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'age');
await this.bot.sendMessage(chatId, '🎂 *Введите ваш возраст:*\n\nВозраст должен быть от 18 до 100 лет', {
parse_mode: 'Markdown'
});
}
// Редактирование описания "О себе"
async handleEditBio(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'bio');
await this.bot.sendMessage(chatId,
'📖 *Расскажите о себе:*\n\n' +
'Напишите несколько предложений, которые помогут людям лучше вас узнать.\n\n' +
'_Максимум 500 символов_', {
parse_mode: 'Markdown'
});
}
// Редактирование хобби
async handleEditHobbies(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'hobbies');
await this.bot.sendMessage(chatId,
'🎯 *Введите ваши хобби через запятую:*\n\n' +
'Например: футбол, чтение, путешествия, кулинария\n\n' +
'_В анкете они будут отображаться как хэштеги: #футбол #чтение #путешествия_', {
parse_mode: 'Markdown'
});
}
// Редактирование города
async handleEditCity(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'city');
await this.bot.sendMessage(chatId, '🏙️ *Введите ваш город:*\n\nНапример: Москва', {
parse_mode: 'Markdown'
});
}
// Редактирование работы
async handleEditJob(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'job');
await this.bot.sendMessage(chatId, '💼 *Введите вашу профессию или место работы:*\n\nНапример: Дизайнер в IT-компании', {
parse_mode: 'Markdown'
});
}
// Редактирование образования
async handleEditEducation(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'education');
await this.bot.sendMessage(chatId, '🎓 *Введите ваше образование:*\n\nНапример: МГУ, факультет журналистики', {
parse_mode: 'Markdown'
});
}
// Редактирование роста
async handleEditHeight(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'height');
await this.bot.sendMessage(chatId, '📏 *Введите ваш рост в сантиметрах:*\n\nНапример: 175', {
parse_mode: 'Markdown'
});
}
// Редактирование религии
async handleEditReligion(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'religion');
await this.bot.sendMessage(chatId, '🕊️ *Введите вашу религию или напишите "нет":*\n\nНапример: православие, ислам, атеизм, нет', {
parse_mode: 'Markdown'
});
}
// Редактирование цели знакомства
async handleEditDatingGoal(chatId: number, telegramId: string): Promise<void> {
const keyboard = {
inline_keyboard: [
[
{ text: '💕 Серьёзные отношения', callback_data: 'set_dating_goal_serious' },
{ text: '🎉 Лёгкие отношения', callback_data: 'set_dating_goal_casual' }
],
[
{ text: '👥 Дружба', callback_data: 'set_dating_goal_friends' },
{ text: '🔥 Одна ночь', callback_data: 'set_dating_goal_one_night' }
],
[
{ text: '😏 FWB', callback_data: 'set_dating_goal_fwb' },
{ text: '💎 Спонсорство', callback_data: 'set_dating_goal_sugar' }
],
[
{ text: '💍 Брак с переездом', callback_data: 'set_dating_goal_marriage_abroad' },
{ text: '💫 Полиамория', callback_data: 'set_dating_goal_polyamory' }
],
[
{ text: '🤷‍♀️ Пока не определился', callback_data: 'set_dating_goal_unsure' }
],
[
{ text: '⬅️ Назад', callback_data: 'edit_profile' }
]
]
};
await this.bot.sendMessage(chatId, '💕 *Выберите цель знакомства:*', {
parse_mode: 'Markdown',
reply_markup: keyboard
});
}
// Редактирование образа жизни
async handleEditLifestyle(chatId: number, telegramId: string): Promise<void> {
const keyboard = {
inline_keyboard: [
[
{ text: '🚬 Курение', callback_data: 'edit_smoking' },
{ text: '🍷 Алкоголь', callback_data: 'edit_drinking' }
],
[
{ text: '👶 Отношение к детям', callback_data: 'edit_kids' }
],
[
{ text: '⬅️ Назад', callback_data: 'edit_profile' }
]
]
};
await this.bot.sendMessage(chatId, '🚬 *Выберите что хотите изменить в образе жизни:*', {
parse_mode: 'Markdown',
reply_markup: keyboard
});
}
// Редактирование предпочтений поиска
async handleEditSearchPreferences(chatId: number, telegramId: string): Promise<void> {
const keyboard = {
inline_keyboard: [
[
{ text: '🔢 Возрастной диапазон', callback_data: 'edit_age_range' },
{ text: '📍 Максимальное расстояние', callback_data: 'edit_distance' }
],
[
{ text: '⬅️ Назад', callback_data: 'edit_profile' }
]
]
};
await this.bot.sendMessage(chatId, '⚙️ *Настройки поиска:*', {
parse_mode: 'Markdown',
reply_markup: keyboard
});
}
// Добавление фото
async handleAddPhoto(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'photo');
await this.bot.sendMessage(chatId, '📷 *Отправьте фотографию:*\n\nМаксимум 9 фотографий в профиле', {
parse_mode: 'Markdown'
});
}
// Удаление фото
async handleDeletePhoto(chatId: number, telegramId: string): Promise<void> {
try {
const profile = await this.profileService.getProfileByTelegramId(telegramId);
if (!profile || profile.photos.length === 0) {
await this.bot.sendMessage(chatId, '❌ У вас нет фотографий для удаления');
return;
}
const keyboard = {
inline_keyboard: [
...profile.photos.map((photo, index) => [
{ text: `🗑️ Удалить фото ${index + 1}`, callback_data: `delete_photo_${index}` }
]),
[{ text: '⬅️ Назад', callback_data: 'manage_photos' }]
]
};
await this.bot.sendMessage(chatId, '🗑️ *Выберите фото для удаления:*', {
parse_mode: 'Markdown',
reply_markup: keyboard
});
} catch (error) {
console.error('Error in handleDeletePhoto:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
}
}
// Установка главного фото
async handleSetMainPhoto(chatId: number, telegramId: string): Promise<void> {
try {
const profile = await this.profileService.getProfileByTelegramId(telegramId);
if (!profile || profile.photos.length <= 1) {
await this.bot.sendMessage(chatId, '❌ У вас недостаточно фотографий');
return;
}
const keyboard = {
inline_keyboard: [
...profile.photos.map((photo, index) => [
{ text: `⭐ Сделать главным фото ${index + 1}`, callback_data: `set_main_photo_${index}` }
]),
[{ text: '⬅️ Назад', callback_data: 'manage_photos' }]
]
};
await this.bot.sendMessage(chatId, '⭐ *Выберите главное фото:*', {
parse_mode: 'Markdown',
reply_markup: keyboard
});
} catch (error) {
console.error('Error in handleSetMainPhoto:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
}
}
// ===== НОВЫЕ МЕТОДЫ ДЛЯ РАСШИРЕННОГО РЕДАКТИРОВАНИЯ =====
// Удаление фото по индексу
async handleDeletePhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise<void> {
try {
const profile = await this.profileService.getProfileByTelegramId(telegramId);
if (!profile || photoIndex >= profile.photos.length) {
await this.bot.sendMessage(chatId, '❌ Фото не найдено');
return;
}
profile.removePhoto(profile.photos[photoIndex]);
await this.profileService.updateProfile(profile.userId, {
photos: profile.photos
});
await this.bot.sendMessage(chatId, '✅ Фото удалено!');
setTimeout(() => {
this.handleManagePhotos(chatId, telegramId);
}, 1000);
} catch (error) {
console.error('Error deleting photo:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
}
}
// Установка главного фото по индексу
async handleSetMainPhotoByIndex(chatId: number, telegramId: string, photoIndex: number): Promise<void> {
try {
const profile = await this.profileService.getProfileByTelegramId(telegramId);
if (!profile || photoIndex >= profile.photos.length) {
await this.bot.sendMessage(chatId, '❌ Фото не найдено');
return;
}
profile.setMainPhoto(profile.photos[photoIndex]);
await this.profileService.updateProfile(profile.userId, {
photos: profile.photos
});
await this.bot.sendMessage(chatId, '✅ Главное фото установлено!');
setTimeout(() => {
this.handleManagePhotos(chatId, telegramId);
}, 1000);
} catch (error) {
console.error('Error setting main photo:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
}
}
// Установка цели знакомства
async handleSetDatingGoal(chatId: number, telegramId: string, goal: string): Promise<void> {
try {
const profile = await this.profileService.getProfileByTelegramId(telegramId);
if (!profile) {
await this.bot.sendMessage(chatId, '❌ Профиль не найден');
return;
}
await this.profileService.updateProfile(profile.userId, {
datingGoal: goal as any
});
const goalTexts: { [key: string]: string } = {
'serious': 'Серьёзные отношения',
'casual': 'Лёгкие отношения',
'friends': 'Дружба',
'unsure': 'Пока не определился',
'one_night': 'Отношения на одну ночь',
'fwb': 'Друзья с привилегиями',
'marriage_abroad': 'Брак с переездом',
'sugar': 'Спонсорство',
'polyamory': 'Полиамория'
};
await this.bot.sendMessage(chatId, `✅ Цель знакомства установлена: ${goalTexts[goal]}`);
setTimeout(() => {
this.profileEditController.showProfileEditMenu(this.bot, chatId, parseInt(telegramId));
}, 1500);
} catch (error) {
console.error('Error setting dating goal:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
}
}
// Редактирование курения
async handleEditSmoking(chatId: number, telegramId: string): Promise<void> {
const keyboard = {
inline_keyboard: [
[
{ text: '🚭 Не курю', callback_data: 'set_smoking_never' },
{ text: '🚬 Иногда', callback_data: 'set_smoking_sometimes' }
],
[
{ text: '🚬 Регулярно', callback_data: 'set_smoking_regularly' }
],
[
{ text: '⬅️ Назад', callback_data: 'edit_lifestyle' }
]
]
};
await this.bot.sendMessage(chatId, '🚬 *Выберите ваше отношение к курению:*', {
parse_mode: 'Markdown',
reply_markup: keyboard
});
}
// Редактирование алкоголя
async handleEditDrinking(chatId: number, telegramId: string): Promise<void> {
const keyboard = {
inline_keyboard: [
[
{ text: '🚫 Не пью', callback_data: 'set_drinking_never' },
{ text: '🍷 Иногда', callback_data: 'set_drinking_sometimes' }
],
[
{ text: '🍺 Регулярно', callback_data: 'set_drinking_regularly' }
],
[
{ text: '⬅️ Назад', callback_data: 'edit_lifestyle' }
]
]
};
await this.bot.sendMessage(chatId, '🍷 *Выберите ваше отношение к алкоголю:*', {
parse_mode: 'Markdown',
reply_markup: keyboard
});
}
// Редактирование отношения к детям
async handleEditKids(chatId: number, telegramId: string): Promise<void> {
const keyboard = {
inline_keyboard: [
[
{ text: '👶 Есть дети', callback_data: 'set_kids_have' },
{ text: '💕 Хочу детей', callback_data: 'set_kids_want' }
],
[
{ text: '🚫 Не хочу детей', callback_data: 'set_kids_dont_want' },
{ text: '🤷‍♀️ Пока не знаю', callback_data: 'set_kids_unsure' }
],
[
{ text: '⬅️ Назад', callback_data: 'edit_lifestyle' }
]
]
};
await this.bot.sendMessage(chatId, '👶 *Выберите ваше отношение к детям:*', {
parse_mode: 'Markdown',
reply_markup: keyboard
});
}
// Установка параметра образа жизни
async handleSetLifestyle(chatId: number, telegramId: string, type: string, value: string): Promise<void> {
try {
const profile = await this.profileService.getProfileByTelegramId(telegramId);
if (!profile) {
await this.bot.sendMessage(chatId, '❌ Профиль не найден');
return;
}
const lifestyle = profile.lifestyle || {};
lifestyle[type as keyof typeof lifestyle] = value as any;
await this.profileService.updateProfile(profile.userId, {
lifestyle: lifestyle
});
const typeTexts: { [key: string]: string } = {
'smoking': 'курение',
'drinking': 'алкоголь',
'kids': 'отношение к детям'
};
const valueTexts: { [key: string]: { [key: string]: string } } = {
smoking: { 'never': 'не курю', 'sometimes': 'иногда', 'regularly': 'регулярно' },
drinking: { 'never': 'не пью', 'sometimes': 'иногда', 'regularly': 'регулярно' },
kids: { 'have': 'есть дети', 'want': 'хочу детей', 'dont_want': 'не хочу детей', 'unsure': 'пока не знаю' }
};
const typeText = typeTexts[type] || type;
const valueText = valueTexts[type]?.[value] || value;
await this.bot.sendMessage(chatId, `${typeText}: ${valueText}`);
setTimeout(() => {
this.profileEditController.showProfileEditMenu(this.bot, chatId, parseInt(telegramId));
}, 1500);
} catch (error) {
console.error('Error setting lifestyle:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
}
}
// Редактирование возрастного диапазона
async handleEditAgeRange(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'age_range');
await this.bot.sendMessage(chatId,
'🔢 *Введите возрастной диапазон:*\n\n' +
'Формат: минимальный-максимальный возраст\n' +
'Например: 18-35', {
parse_mode: 'Markdown'
});
}
// Редактирование максимального расстояния
async handleEditDistance(chatId: number, telegramId: string): Promise<void> {
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'distance');
await this.bot.sendMessage(chatId,
'📍 *Введите максимальное расстояние для поиска:*\n\n' +
'В километрах (например: 50)', {
parse_mode: 'Markdown'
});
}
// ===== VIP ФУНКЦИИ =====
// VIP поиск по целям знакомства
async handleVipSearch(chatId: number, telegramId: string): Promise<void> {
try {
// Проверяем VIP статус пользователя
const user = await this.profileService.getUserByTelegramId(telegramId);
if (!user || !user.isPremium) {
const keyboard = {
inline_keyboard: [
[
{ text: '💎 Получить VIP', callback_data: 'get_vip' },
{ text: '⬅️ Назад', callback_data: 'main_menu' }
]
]
};
await this.bot.sendMessage(chatId,
'🔒 *VIP Поиск*\n\n' +
'Эта функция доступна только для VIP пользователей!\n\n' +
'✨ *VIP возможности:*\n' +
'• Поиск по целям знакомства\n' +
'• Расширенные фильтры\n' +
'• Приоритет в показе анкет\n' +
'• Безлимитные суперлайки', {
parse_mode: 'Markdown',
reply_markup: keyboard
});
return;
}
const keyboard = {
inline_keyboard: [
[
{ text: '💕 Серьёзные отношения', callback_data: 'search_by_goal_serious' },
{ text: '🎉 Лёгкие отношения', callback_data: 'search_by_goal_casual' }
],
[
{ text: '👥 Дружба', callback_data: 'search_by_goal_friends' },
{ text: '🔥 Одна ночь', callback_data: 'search_by_goal_one_night' }
],
[
{ text: '😏 FWB', callback_data: 'search_by_goal_fwb' },
{ text: '💎 Спонсорство', callback_data: 'search_by_goal_sugar' }
],
[
{ text: '💍 Брак с переездом', callback_data: 'search_by_goal_marriage_abroad' },
{ text: '💫 Полиамория', callback_data: 'search_by_goal_polyamory' }
],
[
{ text: '🎲 Все цели', callback_data: 'start_browsing' }
],
[
{ text: '⬅️ Главное меню', callback_data: 'main_menu' }
]
]
};
await this.bot.sendMessage(chatId,
'🔍 *VIP Поиск по целям знакомства*\n\n' +
'Выберите интересующую вас цель:', {
parse_mode: 'Markdown',
reply_markup: keyboard
});
} catch (error) {
console.error('Error in VIP search:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка');
}
}
// Поиск по конкретной цели
async handleSearchByGoal(chatId: number, telegramId: string, goal: string): Promise<void> {
try {
const profile = await this.profileService.getProfileByTelegramId(telegramId);
if (!profile) {
await this.bot.sendMessage(chatId, '❌ Сначала создайте профиль!');
return;
}
// Получаем кандидатов с определенной целью знакомства
const candidates = await this.matchingService.getCandidatesWithGoal(profile, goal);
if (candidates.length === 0) {
const goalTexts: { [key: string]: string } = {
'serious': 'серьёзные отношения',
'casual': 'лёгкие отношения',
'friends': 'дружбу',
'one_night': 'отношения на одну ночь',
'fwb': 'друзей с привилегиями',
'marriage_abroad': 'брак с переездом',
'sugar': 'спонсорство',
'polyamory': 'полиаморию'
};
const keyboard = {
inline_keyboard: [
[
{ text: '🔍 Другие цели', callback_data: 'vip_search' },
{ text: '🎲 Обычный поиск', callback_data: 'start_browsing' }
],
[
{ text: '⬅️ Главное меню', callback_data: 'main_menu' }
]
]
};
await this.bot.sendMessage(chatId,
`😔 *Пока нет анкет*\n\n` +
`К сожалению, сейчас нет пользователей, которые ищут ${goalTexts[goal] || goal}.\n\n` +
'Попробуйте позже или выберите другую цель!', {
parse_mode: 'Markdown',
reply_markup: keyboard
});
return;
}
// Показываем первого кандидата
const candidate = candidates[0];
await this.displayCandidate(chatId, candidate);
} catch (error) {
console.error('Error searching by goal:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка при поиске');
}
}
// Показ конкретного кандидата (для VIP поиска)
async displayCandidate(chatId: number, candidate: Profile): Promise<void> {
const candidatePhotoFileId = candidate.photos[0]; // Первое фото - главное
let candidateText = candidate.name + ', ' + candidate.age + '\n';
candidateText += '📍 ' + (candidate.city || 'Не указан') + '\n';
if (candidate.job) candidateText += '💼 ' + candidate.job + '\n';
if (candidate.education) candidateText += '🎓 ' + candidate.education + '\n';
if (candidate.height) candidateText += '📏 ' + candidate.height + ' см\n';
if (candidate.religion) candidateText += '🕊️ ' + candidate.religion + '\n';
// Цель знакомства
if (candidate.datingGoal) {
const goalText = this.getDatingGoalText(candidate.datingGoal);
candidateText += '💕 ' + goalText + '\n';
}
// Образ жизни
if (candidate.lifestyle) {
const lifestyleText = this.getLifestyleText(candidate.lifestyle);
if (lifestyleText) {
candidateText += lifestyleText + '\n';
}
}
candidateText += '\n📝 ' + (candidate.bio || 'Описание отсутствует') + '\n';
// Хобби с хэштегами
if (candidate.hobbies && candidate.hobbies.trim()) {
const hobbiesArray = candidate.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
candidateText += '\n🎯 ' + formattedHobbies + '\n';
}
if (candidate.interests.length > 0) {
candidateText += '\n<> Интересы: ' + candidate.interests.join(', ');
}
const keyboard: InlineKeyboardMarkup = {
inline_keyboard: [
[
{ text: '👎 Не нравится', callback_data: 'dislike_' + candidate.userId },
{ text: '💖 Супер лайк', callback_data: 'superlike_' + candidate.userId },
{ text: '👍 Нравится', callback_data: 'like_' + candidate.userId }
],
[
{ text: '👤 Профиль', callback_data: 'view_profile_' + candidate.userId },
{ text: '📸 Еще фото', callback_data: 'more_photos_' + candidate.userId }
],
[
{ text: '⏭ Следующий', callback_data: 'next_candidate' },
{ text: '🔍 VIP поиск', callback_data: 'vip_search' }
]
]
};
// Проверяем, есть ли валидное фото (file_id или URL)
const hasValidPhoto = candidatePhotoFileId &&
(candidatePhotoFileId.startsWith('http') ||
candidatePhotoFileId.startsWith('AgAC') ||
candidatePhotoFileId.length > 20); // file_id обычно длинные
if (hasValidPhoto) {
try {
await this.bot.sendPhoto(chatId, candidatePhotoFileId, {
caption: candidateText,
reply_markup: keyboard
});
} catch (error) {
// Если не удалось отправить фото, отправляем текст
await this.bot.sendMessage(chatId, '🖼 Фото недоступно\n\n' + candidateText, {
reply_markup: keyboard
});
}
} else {
// Отправляем как текстовое сообщение
await this.bot.sendMessage(chatId, '📝 ' + candidateText, {
reply_markup: keyboard
});
}
}
// Получить текст цели знакомства
private getDatingGoalText(goal: string): string {
const goals: { [key: string]: string } = {
'serious': 'Серьёзные отношения',
'casual': 'Лёгкие отношения',
'friends': 'Дружба',
'unsure': 'Пока не определился',
'one_night': 'Отношения на одну ночь',
'fwb': 'Друзья с привилегиями',
'marriage_abroad': 'Брак с переездом',
'sugar': 'Спонсорство',
'polyamory': 'Полиамория'
};
return goals[goal] || goal;
}
// Получить текст образа жизни
private getLifestyleText(lifestyle: any): string {
const parts: string[] = [];
if (lifestyle?.smoking) {
const smokingTexts: { [key: string]: string } = {
'never': 'Не курю',
'sometimes': 'Иногда курю',
'regularly': 'Курю'
};
parts.push('🚬 ' + (smokingTexts[lifestyle.smoking] || lifestyle.smoking));
}
if (lifestyle?.drinking) {
const drinkingTexts: { [key: string]: string } = {
'never': 'Не пью',
'sometimes': 'Иногда пью',
'regularly': 'Пью'
};
parts.push('🍷 ' + (drinkingTexts[lifestyle.drinking] || lifestyle.drinking));
}
if (lifestyle?.kids) {
const kidsTexts: { [key: string]: string } = {
'have': 'Есть дети',
'want': 'Хочу детей',
'dont_want': 'Не хочу детей',
'unsure': 'Пока не знаю'
};
parts.push('👶 ' + (kidsTexts[lifestyle.kids] || lifestyle.kids));
}
return parts.join(', ');
}
}

View File

@@ -40,6 +40,9 @@ export class CommandHandlers {
],
[
{ text: '💕 Мои матчи', callback_data: 'view_matches' },
{ text: '⭐ VIP поиск', callback_data: 'vip_search' }
],
[
{ text: '⚙️ Настройки', callback_data: 'settings' }
]
]

View File

@@ -14,12 +14,19 @@ interface ChatState {
matchId: string;
}
// Состояния пользователей для редактирования профиля
interface ProfileEditState {
waitingForInput: boolean;
field: string;
}
export class MessageHandlers {
private bot: TelegramBot;
private profileService: ProfileService;
private chatService: ChatService;
private userStates: Map<string, UserState> = new Map();
private chatStates: Map<string, ChatState> = new Map();
private profileEditStates: Map<string, ProfileEditState> = new Map();
constructor(bot: TelegramBot) {
this.bot = bot;
@@ -42,6 +49,7 @@ export class MessageHandlers {
const userState = this.userStates.get(userId);
const chatState = this.chatStates.get(userId);
const profileEditState = this.profileEditStates.get(userId);
// Если пользователь в процессе отправки сообщения в чат
if (chatState?.waitingForMessage && msg.text) {
@@ -49,6 +57,12 @@ export class MessageHandlers {
return;
}
// Если пользователь редактирует профиль
if (profileEditState?.waitingForInput) {
await this.handleProfileEdit(msg, userId, profileEditState.field);
return;
}
// Если пользователь в процессе создания профиля
if (userState) {
await this.handleProfileCreation(msg, userId, userState);
@@ -312,4 +326,206 @@ export class MessageHandlers {
await this.bot.sendMessage(msg.chat.id, '❌ Не удалось отправить сообщение. Попробуйте еще раз.');
}
}
// ===== МЕТОДЫ ДЛЯ РЕДАКТИРОВАНИЯ ПРОФИЛЯ =====
// Установить состояние ожидания ввода для редактирования профиля
setWaitingForInput(telegramId: number, field: string): void {
this.profileEditStates.set(telegramId.toString(), {
waitingForInput: true,
field: field
});
}
// Очистить состояние редактирования профиля
clearProfileEditState(userId: string): void {
this.profileEditStates.delete(userId);
}
// Обработка редактирования профиля
async handleProfileEdit(msg: Message, userId: string, field: string): Promise<void> {
const chatId = msg.chat.id;
try {
let value: any = msg.text;
let isValid = true;
let errorMessage = '';
// Валидация в зависимости от поля
switch (field) {
case 'name':
if (!value || value.length < 2 || value.length > 50) {
isValid = false;
errorMessage = 'Имя должно быть от 2 до 50 символов';
}
break;
case 'age':
const age = parseInt(value);
if (isNaN(age) || age < 18 || age > 100) {
isValid = false;
errorMessage = 'Возраст должен быть числом от 18 до 100';
}
value = age;
break;
case 'bio':
if (value && value.length > 500) {
isValid = false;
errorMessage = 'Описание не должно превышать 500 символов';
}
break;
case 'height':
const height = parseInt(value);
if (isNaN(height) || height < 100 || height > 250) {
isValid = false;
errorMessage = 'Рост должен быть числом от 100 до 250 см';
}
value = height;
break;
case 'photo':
if (!msg.photo || !msg.photo.length) {
isValid = false;
errorMessage = 'Отправьте фотографию';
} else {
// Берём фото наибольшего размера
value = msg.photo[msg.photo.length - 1].file_id;
}
break;
case 'age_range':
const ageRangeParts = value.split('-');
if (ageRangeParts.length !== 2) {
isValid = false;
errorMessage = 'Неверный формат. Используйте: минимальный-максимальный возраст (например: 18-35)';
} else {
const minAge = parseInt(ageRangeParts[0]);
const maxAge = parseInt(ageRangeParts[1]);
if (isNaN(minAge) || isNaN(maxAge) || minAge < 18 || maxAge > 100 || minAge >= maxAge) {
isValid = false;
errorMessage = 'Возраст должен быть от 18 до 100, минимальный меньше максимального';
}
value = { minAge, maxAge };
}
break;
case 'distance':
const distance = parseInt(value);
if (isNaN(distance) || distance < 1 || distance > 1000) {
isValid = false;
errorMessage = 'Расстояние должно быть числом от 1 до 1000 км';
}
value = distance;
break;
}
if (!isValid) {
await this.bot.sendMessage(chatId, `${errorMessage}\n\опробуйте еще раз:`);
return;
}
// Обновляем профиль
await this.updateProfileField(userId, field, value);
// Очищаем состояние
this.clearProfileEditState(userId);
// Отправляем подтверждение и возвращаем к меню редактирования
await this.bot.sendMessage(chatId, '✅ Данные успешно обновлены!');
setTimeout(async () => {
const keyboard = {
inline_keyboard: [
[
{ text: '✏️ Продолжить редактирование', callback_data: 'edit_profile' },
{ text: '👀 Предпросмотр', callback_data: 'preview_profile' }
],
[
{ text: '⬅️ Главное меню', callback_data: 'main_menu' }
]
]
};
await this.bot.sendMessage(
chatId,
'🛠️ Что дальше?',
{ reply_markup: keyboard }
);
}, 1000);
} catch (error) {
console.error('Error handling profile edit:', error);
await this.bot.sendMessage(chatId, '❌ Произошла ошибка. Попробуйте еще раз.');
this.clearProfileEditState(userId);
}
}
// Обновление поля профиля
async updateProfileField(userId: string, field: string, value: any): Promise<void> {
const profile = await this.profileService.getProfileByTelegramId(userId);
if (!profile) {
throw new Error('Profile not found');
}
const updates: any = {};
switch (field) {
case 'name':
updates.name = value;
break;
case 'age':
updates.age = value;
break;
case 'bio':
updates.bio = value;
break;
case 'hobbies':
updates.hobbies = value;
break;
case 'city':
// В БД поле называется 'location', но мы используем city в модели
updates.city = value;
break;
case 'job':
// В БД поле называется 'occupation', но мы используем job в модели
updates.job = value;
break;
case 'education':
updates.education = value;
break;
case 'height':
updates.height = value;
break;
case 'religion':
updates.religion = value === 'нет' ? null : value;
break;
case 'age_range':
updates.searchPreferences = {
minAge: value.minAge,
maxAge: value.maxAge,
maxDistance: profile.searchPreferences?.maxDistance || 50
};
break;
case 'distance':
updates.searchPreferences = {
minAge: profile.searchPreferences?.minAge || 18,
maxAge: profile.searchPreferences?.maxAge || 50,
maxDistance: value
};
break;
case 'photo':
// Добавляем фото к существующим
profile.addPhoto(value);
await this.profileService.updateProfile(profile.userId, {
photos: profile.photos
});
return;
}
if (Object.keys(updates).length > 0) {
await this.profileService.updateProfile(profile.userId, updates);
}
}
}

View File

@@ -7,10 +7,18 @@ export interface ProfileData {
bio?: string;
photos: string[]; // Просто массив file_id
interests: string[];
hobbies?: string; // Хобби через запятую
city?: string;
education?: string;
job?: string;
height?: number;
religion?: string;
datingGoal?: 'serious' | 'casual' | 'friends' | 'unsure' | 'one_night' | 'fwb' | 'marriage_abroad' | 'sugar' | 'polyamory';
lifestyle?: {
smoking?: 'never' | 'sometimes' | 'regularly';
drinking?: 'never' | 'sometimes' | 'regularly';
kids?: 'have' | 'want' | 'dont_want' | 'unsure';
};
location?: {
latitude: number;
longitude: number;
@@ -35,10 +43,18 @@ export class Profile {
bio?: string;
photos: string[];
interests: string[];
hobbies?: string;
city?: string;
education?: string;
job?: string;
height?: number;
religion?: string;
datingGoal?: 'serious' | 'casual' | 'friends' | 'unsure' | 'one_night' | 'fwb' | 'marriage_abroad' | 'sugar' | 'polyamory';
lifestyle?: {
smoking?: 'never' | 'sometimes' | 'regularly';
drinking?: 'never' | 'sometimes' | 'regularly';
kids?: 'have' | 'want' | 'dont_want' | 'unsure';
};
location?: {
latitude: number;
longitude: number;
@@ -62,10 +78,14 @@ export class Profile {
this.bio = data.bio;
this.photos = data.photos || [];
this.interests = data.interests || [];
this.hobbies = data.hobbies;
this.city = data.city;
this.education = data.education;
this.job = data.job;
this.height = data.height;
this.religion = data.religion;
this.datingGoal = data.datingGoal;
this.lifestyle = data.lifestyle;
this.location = data.location;
this.searchPreferences = data.searchPreferences || {
minAge: 18,
@@ -109,6 +129,21 @@ export class Profile {
return this.photos[0];
}
// Получить хобби как хэштеги
getFormattedHobbies(): string {
if (!this.hobbies) return '';
return this.hobbies
.split(',')
.map(hobby => `#${hobby.trim()}`)
.join(' ');
}
// Установить хобби из строки
setHobbies(hobbiesString: string): void {
this.hobbies = hobbiesString;
this.updatedAt = new Date();
}
// Получить профиль для показа
getDisplayProfile() {
return {
@@ -118,10 +153,14 @@ export class Profile {
bio: this.bio,
photos: this.photos,
interests: this.interests,
hobbies: this.getFormattedHobbies(),
city: this.city,
education: this.education,
job: this.job,
height: this.height,
religion: this.religion,
datingGoal: this.datingGoal,
lifestyle: this.lifestyle,
isVerified: this.isVerified
};
}

View File

@@ -381,4 +381,34 @@ export class MatchingService {
// Используем ProfileService для правильного маппинга данных
return this.profileService.mapEntityToProfile(candidateData);
}
// VIP функция: поиск кандидатов по цели знакомства
async getCandidatesWithGoal(userProfile: Profile, targetGoal: string): Promise<Profile[]> {
const swipedUsersResult = await query(`
SELECT swiped_id
FROM swipes
WHERE swiper_id = $1
`, [userProfile.userId]);
const swipedUserIds = swipedUsersResult.rows.map((row: any) => row.swiped_id);
swipedUserIds.push(userProfile.userId); // Исключаем себя
let candidateQuery = `
SELECT DISTINCT p.*, u.telegram_id, u.username, u.first_name, u.last_name
FROM profiles p
JOIN users u ON p.user_id = u.id
WHERE p.is_visible = true
AND p.is_active = true
AND p.gender = $1
AND p.dating_goal = $2
AND p.user_id NOT IN (${swipedUserIds.map((_: any, i: number) => `$${i + 3}`).join(', ')})
ORDER BY p.created_at DESC
LIMIT 50
`;
const params = [userProfile.interestedIn, targetGoal, ...swipedUserIds];
const result = await query(candidateQuery, params);
return result.rows.map((row: any) => this.profileService.mapEntityToProfile(row));
}
}

View File

@@ -50,13 +50,15 @@ export class ProfileService {
await query(`
INSERT INTO profiles (
id, user_id, name, age, gender, looking_for, bio, photos, interests,
location, education, occupation, height, latitude, longitude,
verification_status, is_active, is_visible, created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
hobbies, location, education, occupation, height, religion, dating_goal,
latitude, longitude, verification_status, is_active, is_visible,
created_at, updated_at
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23)
`, [
profileId, userId, profile.name, profile.age, profile.gender, profile.interestedIn,
profile.bio, profile.photos, profile.interests,
profile.bio, profile.photos, profile.interests, profile.hobbies,
profile.city, profile.education, profile.job, profile.height,
profile.religion, profile.datingGoal,
profile.location?.latitude, profile.location?.longitude,
'unverified', true, profile.isVisible, profile.createdAt, profile.updatedAt
]);
@@ -106,6 +108,15 @@ export class ProfileService {
return result.rows[0].id;
}
// Получение пользователя по Telegram ID
async getUserByTelegramId(telegramId: string): Promise<any | null> {
const result = await query(`
SELECT * FROM users WHERE telegram_id = $1
`, [parseInt(telegramId)]);
return result.rows.length > 0 ? result.rows[0] : null;
}
// Создание пользователя если не существует
async ensureUser(telegramId: string, userData: any): Promise<string> {
// Используем UPSERT для избежания дублирования
@@ -146,7 +157,7 @@ export class ProfileService {
// Строим динамический запрос обновления
Object.entries(updates).forEach(([key, value]) => {
if (value !== undefined) {
if (value !== undefined && key !== 'updatedAt') { // Исключаем updatedAt из цикла
switch (key) {
case 'photos':
case 'interests':
@@ -175,6 +186,7 @@ export class ProfileService {
return existingProfile;
}
// Всегда добавляем updated_at в конце
updateFields.push(`updated_at = $${paramIndex++}`);
updateValues.push(new Date());
updateValues.push(userId);
@@ -409,10 +421,18 @@ export class ProfileService {
bio: entity.bio,
photos: parsePostgresArray(entity.photos),
interests: parsePostgresArray(entity.interests),
hobbies: entity.hobbies,
city: entity.location || entity.city,
education: entity.education,
job: entity.occupation || entity.job,
height: entity.height,
religion: entity.religion,
datingGoal: entity.dating_goal,
lifestyle: {
smoking: entity.smoking,
drinking: entity.drinking,
kids: entity.has_kids
},
location: entity.latitude && entity.longitude ? {
latitude: entity.latitude,
longitude: entity.longitude
@@ -431,6 +451,18 @@ export class ProfileService {
// Преобразование camelCase в snake_case
private camelToSnake(str: string): string {
// Специальные случаи для некоторых полей
const specialCases: { [key: string]: string } = {
'interestedIn': 'looking_for',
'job': 'occupation',
'city': 'location',
'datingGoal': 'dating_goal'
};
if (specialCases[str]) {
return specialCases[str];
}
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
}