geo detection
This commit is contained in:
@@ -34,7 +34,7 @@ class TelegramTinderBot {
|
||||
this.localizationService = LocalizationService.getInstance();
|
||||
|
||||
this.commandHandlers = new CommandHandlers(this.bot);
|
||||
this.messageHandlers = new MessageHandlers(this.bot);
|
||||
this.messageHandlers = new MessageHandlers(this.bot, this.notificationService);
|
||||
this.callbackHandlers = new CallbackHandlers(this.bot, this.messageHandlers);
|
||||
this.notificationHandlers = new NotificationHandlers(this.bot);
|
||||
|
||||
|
||||
@@ -31,7 +31,9 @@ export class CallbackHandlers {
|
||||
this.bot = bot;
|
||||
this.profileService = new ProfileService();
|
||||
this.matchingService = new MatchingService();
|
||||
this.chatService = new ChatService();
|
||||
// Получаем notificationService из messageHandlers (если есть)
|
||||
const notificationService = (messageHandlers as any).notificationService;
|
||||
this.chatService = new ChatService(notificationService);
|
||||
this.messageHandlers = messageHandlers;
|
||||
this.profileEditController = new ProfileEditController(this.profileService);
|
||||
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
||||
@@ -86,6 +88,107 @@ export class CallbackHandlers {
|
||||
await this.handleEditHobbies(chatId, telegramId);
|
||||
} else if (data === 'edit_city') {
|
||||
await this.handleEditCity(chatId, telegramId);
|
||||
} else if (data === 'confirm_city') {
|
||||
try {
|
||||
const states = (this.messageHandlers as any).userStates as Map<string, any>;
|
||||
const userState = states ? states.get(telegramId) : null;
|
||||
if (userState && userState.step === 'confirm_city') {
|
||||
// Подтверждаем город и переводим пользователя к вводу био
|
||||
userState.step = 'waiting_bio';
|
||||
console.log(`User ${telegramId} confirmed city: ${userState.data.city}`);
|
||||
// Убираем inline-кнопки из сообщения с подтверждением
|
||||
try {
|
||||
// clear inline keyboard
|
||||
await this.bot.editMessageReplyMarkup({ inline_keyboard: [] } as any, { chat_id: chatId, message_id: query.message?.message_id });
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
await this.bot.sendMessage(chatId, `✅ Город подтверждён: *${userState.data.city}*\n\n📝 Теперь расскажите немного о себе (био):`, { parse_mode: 'Markdown' });
|
||||
} else {
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Контекст не найден. Повторите, пожалуйста.' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error confirming city via callback:', error);
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Ошибка при подтверждении города' });
|
||||
}
|
||||
} else if (data === 'edit_city_manual') {
|
||||
try {
|
||||
const states = (this.messageHandlers as any).userStates as Map<string, any>;
|
||||
const userState = states ? states.get(telegramId) : null;
|
||||
if (userState) {
|
||||
userState.step = 'waiting_city';
|
||||
console.log(`User ${telegramId} chose to enter city manually`);
|
||||
try {
|
||||
// clear inline keyboard
|
||||
await this.bot.editMessageReplyMarkup({ inline_keyboard: [] } as any, { chat_id: chatId, message_id: query.message?.message_id });
|
||||
} catch (e) { }
|
||||
await this.bot.sendMessage(chatId, '✏️ Введите название вашего города вручную:', { reply_markup: { remove_keyboard: true } });
|
||||
} else {
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Контекст не найден. Повторите, пожалуйста.' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching to manual city input via callback:', error);
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Ошибка' });
|
||||
}
|
||||
} else if (data === 'confirm_city_edit') {
|
||||
try {
|
||||
const editState = this.messageHandlers.profileEditStates.get(telegramId);
|
||||
if (editState && editState.field === 'city' && editState.tempCity) {
|
||||
console.log(`User ${telegramId} confirmed city edit: ${editState.tempCity}`);
|
||||
// Обновляем город в профиле
|
||||
await this.messageHandlers.updateProfileField(telegramId, 'city', editState.tempCity);
|
||||
// Очищаем состояние
|
||||
this.messageHandlers.clearProfileEditState(telegramId);
|
||||
// Убираем inline-кнопки
|
||||
try {
|
||||
await this.bot.editMessageReplyMarkup({ inline_keyboard: [] } as any, { chat_id: chatId, message_id: query.message?.message_id });
|
||||
} catch (e) { }
|
||||
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 });
|
||||
}, 500);
|
||||
} else {
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Контекст не найден. Повторите, пожалуйста.' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error confirming city edit via callback:', error);
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Ошибка при подтверждении города' });
|
||||
}
|
||||
} else if (data === 'edit_city_manual_edit') {
|
||||
try {
|
||||
const editState = this.messageHandlers.profileEditStates.get(telegramId);
|
||||
if (editState && editState.field === 'city') {
|
||||
console.log(`User ${telegramId} chose to re-enter city during edit`);
|
||||
// Очищаем временный город, но оставляем состояние редактирования
|
||||
delete editState.tempCity;
|
||||
try {
|
||||
await this.bot.editMessageReplyMarkup({ inline_keyboard: [] } as any, { chat_id: chatId, message_id: query.message?.message_id });
|
||||
} catch (e) { }
|
||||
await this.bot.sendMessage(chatId, '✏️ Введите название вашего города вручную или отправьте геолокацию:', {
|
||||
reply_markup: {
|
||||
keyboard: [
|
||||
[{ text: '📍 Отправить геолокацию', request_location: true }]
|
||||
],
|
||||
resize_keyboard: true,
|
||||
one_time_keyboard: true
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Контекст не найден. Повторите, пожалуйста.' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error switching to re-enter city during edit via callback:', error);
|
||||
await this.bot.answerCallbackQuery(query.id, { text: 'Ошибка' });
|
||||
}
|
||||
} else if (data === 'edit_job') {
|
||||
await this.handleEditJob(chatId, telegramId);
|
||||
} else if (data === 'edit_education') {
|
||||
@@ -184,6 +287,12 @@ export class CallbackHandlers {
|
||||
await this.likeBackHandler.handleLikeBack(chatId, telegramId, targetUserId);
|
||||
}
|
||||
|
||||
// Быстрый переход в чат из уведомлений
|
||||
else if (data.startsWith('open_chat:')) {
|
||||
const matchId = data.replace('open_chat:', '');
|
||||
await this.enhancedChatHandlers.openNativeChat(chatId, telegramId, matchId);
|
||||
}
|
||||
|
||||
// Матчи и чаты
|
||||
else if (data === 'view_matches') {
|
||||
await this.handleViewMatches(chatId, telegramId);
|
||||
@@ -975,14 +1084,22 @@ export class CallbackHandlers {
|
||||
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.hobbies) {
|
||||
let hobbiesArray: string[] = [];
|
||||
if (typeof profile.hobbies === 'string') {
|
||||
hobbiesArray = profile.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
|
||||
} else if (Array.isArray(profile.hobbies)) {
|
||||
hobbiesArray = (profile.hobbies as string[]).filter((hobby: string) => hobby && hobby.trim());
|
||||
}
|
||||
|
||||
if (hobbiesArray.length > 0) {
|
||||
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
|
||||
profileText += '\n🎯 ' + formattedHobbies + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (profile.interests.length > 0) {
|
||||
profileText += '\n<EFBFBD> Интересы: ' + profile.interests.join(', ');
|
||||
profileText += '\n💡 Интересы: ' + profile.interests.join(', ');
|
||||
}
|
||||
|
||||
let keyboard: InlineKeyboardMarkup;
|
||||
@@ -1192,8 +1309,15 @@ export class CallbackHandlers {
|
||||
// Редактирование города
|
||||
async handleEditCity(chatId: number, telegramId: string): Promise<void> {
|
||||
this.messageHandlers.setWaitingForInput(parseInt(telegramId), 'city');
|
||||
await this.bot.sendMessage(chatId, '🏙️ *Введите ваш город:*\n\nНапример: Москва', {
|
||||
parse_mode: 'Markdown'
|
||||
await this.bot.sendMessage(chatId, '🏙️ *Укажите ваш город:*\n\nВыберите один из вариантов:', {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
keyboard: [
|
||||
[{ text: '📍 Отправить геолокацию', request_location: true }]
|
||||
],
|
||||
resize_keyboard: true,
|
||||
one_time_keyboard: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1762,10 +1886,18 @@ export class CallbackHandlers {
|
||||
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.hobbies) {
|
||||
let hobbiesArray: string[] = [];
|
||||
if (typeof candidate.hobbies === 'string') {
|
||||
hobbiesArray = candidate.hobbies.split(',').map(hobby => hobby.trim()).filter(hobby => hobby);
|
||||
} else if (Array.isArray(candidate.hobbies)) {
|
||||
hobbiesArray = (candidate.hobbies as string[]).filter((hobby: string) => hobby && hobby.trim());
|
||||
}
|
||||
|
||||
if (hobbiesArray.length > 0) {
|
||||
const formattedHobbies = hobbiesArray.map(hobby => '#' + hobby).join(' ');
|
||||
candidateText += '\n🎯 ' + formattedHobbies + '\n';
|
||||
}
|
||||
}
|
||||
|
||||
if (candidate.interests.length > 0) {
|
||||
|
||||
@@ -11,9 +11,9 @@ export class EnhancedChatHandlers {
|
||||
|
||||
constructor(bot: TelegramBot) {
|
||||
this.bot = bot;
|
||||
this.chatService = new ChatService();
|
||||
this.profileService = new ProfileService();
|
||||
this.notificationService = new NotificationService(bot);
|
||||
this.chatService = new ChatService(this.notificationService);
|
||||
}
|
||||
|
||||
// ===== НАТИВНЫЙ ИНТЕРФЕЙС ЧАТОВ =====
|
||||
|
||||
@@ -2,6 +2,7 @@ import TelegramBot, { Message, InlineKeyboardMarkup } from 'node-telegram-bot-ap
|
||||
import { ProfileService } from '../services/profileService';
|
||||
import { ChatService } from '../services/chatService';
|
||||
import { EnhancedChatHandlers } from './enhancedChatHandlers';
|
||||
import { KakaoMapService } from '../services/kakaoMapService';
|
||||
|
||||
// Состояния пользователей для создания профилей
|
||||
interface UserState {
|
||||
@@ -19,6 +20,7 @@ interface ChatState {
|
||||
interface ProfileEditState {
|
||||
waitingForInput: boolean;
|
||||
field: string;
|
||||
tempCity?: string; // Временное хранение города для подтверждения
|
||||
}
|
||||
|
||||
export class MessageHandlers {
|
||||
@@ -26,15 +28,27 @@ export class MessageHandlers {
|
||||
private profileService: ProfileService;
|
||||
private chatService: ChatService;
|
||||
private enhancedChatHandlers: EnhancedChatHandlers;
|
||||
private notificationService: any;
|
||||
private kakaoMapService: KakaoMapService | null = null;
|
||||
private userStates: Map<string, UserState> = new Map();
|
||||
private chatStates: Map<string, ChatState> = new Map();
|
||||
private profileEditStates: Map<string, ProfileEditState> = new Map();
|
||||
public profileEditStates: Map<string, ProfileEditState> = new Map();
|
||||
|
||||
constructor(bot: TelegramBot) {
|
||||
constructor(bot: TelegramBot, notificationService?: any) {
|
||||
this.bot = bot;
|
||||
this.profileService = new ProfileService();
|
||||
this.chatService = new ChatService();
|
||||
this.notificationService = notificationService;
|
||||
this.chatService = new ChatService(notificationService);
|
||||
this.enhancedChatHandlers = new EnhancedChatHandlers(bot);
|
||||
|
||||
// Инициализируем Kakao Maps, если есть API ключ
|
||||
const kakaoApiKey = process.env.KAKAO_REST_API_KEY || process.env.KAKAO_MAP_REST_KEY;
|
||||
if (kakaoApiKey) {
|
||||
this.kakaoMapService = new KakaoMapService(kakaoApiKey);
|
||||
console.log('✅ Kakao Maps service initialized');
|
||||
} else {
|
||||
console.warn('⚠️ KAKAO_REST_API_KEY or KAKAO_MAP_REST_KEY not found, location features will be limited');
|
||||
}
|
||||
}
|
||||
|
||||
register(): void {
|
||||
@@ -55,7 +69,7 @@ export class MessageHandlers {
|
||||
const profileEditState = this.profileEditStates.get(userId);
|
||||
|
||||
// Проверяем на нативные чаты (прямые сообщения в контексте чата)
|
||||
if (msg.text && await this.enhancedChatHandlers.handleIncomingChatMessage(msg.chat.id, msg.text)) {
|
||||
if (await this.enhancedChatHandlers.handleIncomingChatMessage(msg, userId)) {
|
||||
return; // Сообщение обработано как сообщение в чате
|
||||
}
|
||||
|
||||
@@ -127,22 +141,68 @@ export class MessageHandlers {
|
||||
userState.data.age = age;
|
||||
userState.step = 'waiting_city';
|
||||
|
||||
await this.bot.sendMessage(chatId, '📍 Прекрасно! В каком городе вы живете?');
|
||||
// Запрашиваем геолокацию или текст
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'📍 Прекрасно! В каком городе вы живете?\n\n' +
|
||||
'💡 Вы можете:\n' +
|
||||
'• Отправить геолокацию 📍 (кнопка ниже)\n' +
|
||||
'• Написать название города вручную',
|
||||
{
|
||||
reply_markup: {
|
||||
keyboard: [
|
||||
[{ text: '📍 Отправить геолокацию', request_location: true }]
|
||||
],
|
||||
one_time_keyboard: true,
|
||||
resize_keyboard: true
|
||||
}
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'waiting_city':
|
||||
if (!msg.text) {
|
||||
await this.bot.sendMessage(chatId, '❌ Пожалуйста, отправьте название города');
|
||||
// Обработка геолокации
|
||||
if (msg.location) {
|
||||
await this.handleLocationForCity(msg, userId, userState);
|
||||
return;
|
||||
}
|
||||
|
||||
userState.data.city = msg.text.trim();
|
||||
userState.step = 'waiting_bio';
|
||||
// Обработка текста
|
||||
if (!msg.text) {
|
||||
await this.bot.sendMessage(chatId, '❌ Пожалуйста, отправьте название города или геолокацию');
|
||||
return;
|
||||
}
|
||||
|
||||
const cityInput = msg.text.trim();
|
||||
console.log(`User ${userId} entered city manually: ${cityInput}`);
|
||||
|
||||
// Временно сохраняем город и запрашиваем подтверждение
|
||||
userState.data.city = cityInput;
|
||||
userState.step = 'confirm_city';
|
||||
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'📝 Теперь расскажите немного о себе (био):\n\n' +
|
||||
'💡 Например: хобби, интересы, что ищете в отношениях и т.д.'
|
||||
chatId,
|
||||
`<EFBFBD> Вы указали город: *${cityInput}*\n\n` +
|
||||
'Подтвердите или введите заново.',
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✅ Подтвердить', callback_data: 'confirm_city' },
|
||||
{ text: '✏️ Ввести заново', callback_data: 'edit_city_manual' }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
break;
|
||||
|
||||
case 'confirm_city':
|
||||
// Этот случай обрабатывается через callback_data, но на случай если пользователь напишет текст
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'⚠️ Пожалуйста, используйте кнопки для подтверждения города.'
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -428,6 +488,47 @@ export class MessageHandlers {
|
||||
}
|
||||
value = distance;
|
||||
break;
|
||||
|
||||
case 'hobbies':
|
||||
// Разбиваем строку с запятыми на массив
|
||||
if (typeof value === 'string') {
|
||||
value = value.split(',').map(hobby => hobby.trim()).filter(hobby => hobby.length > 0);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'city':
|
||||
// Обработка города: поддержка геолокации и текстового ввода
|
||||
if (msg.location) {
|
||||
// Обработка геолокации
|
||||
await this.handleLocationForCityEdit(msg, userId);
|
||||
return; // Выходим из функции, так как требуется подтверждение
|
||||
} else if (msg.text) {
|
||||
// Обработка текстового ввода города
|
||||
const cityInput = msg.text.trim();
|
||||
console.log(`User ${userId} entered city manually during edit: ${cityInput}`);
|
||||
// Сохраняем временно в состояние редактирования
|
||||
const editState = this.profileEditStates.get(userId);
|
||||
if (editState) {
|
||||
editState.tempCity = cityInput;
|
||||
}
|
||||
// Требуем подтверждения
|
||||
await this.bot.sendMessage(chatId, `📍 Вы указали город: *${cityInput}*\n\nПодтвердите или введите заново.`, {
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✅ Подтвердить', callback_data: 'confirm_city_edit' },
|
||||
{ text: '✏️ Ввести заново', callback_data: 'edit_city_manual_edit' }
|
||||
]
|
||||
]
|
||||
}
|
||||
});
|
||||
return; // Выходим, ждем подтверждения
|
||||
} else {
|
||||
isValid = false;
|
||||
errorMessage = '❌ Пожалуйста, отправьте название города или геолокацию';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
@@ -537,4 +638,194 @@ export class MessageHandlers {
|
||||
await this.profileService.updateProfile(profile.userId, updates);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка геолокации для определения города
|
||||
private async handleLocationForCity(msg: Message, userId: string, userState: UserState): Promise<void> {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
if (!msg.location) {
|
||||
await this.bot.sendMessage(chatId, '❌ Не удалось получить геолокацию');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Показываем индикатор загрузки
|
||||
await this.bot.sendChatAction(chatId, 'typing');
|
||||
|
||||
if (!this.kakaoMapService) {
|
||||
// Если Kakao Maps не настроен, используем координаты как есть
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'⚠️ Автоматическое определение города недоступно.\n' +
|
||||
'Пожалуйста, введите название вашего города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Показываем индикатор загрузки
|
||||
await this.bot.sendChatAction(chatId, 'typing');
|
||||
|
||||
if (!this.kakaoMapService) {
|
||||
// Если Kakao Maps не настроен, используем координаты как есть
|
||||
console.warn(`KakaoMaps not configured - user ${userId} sent coords: ${msg.location.latitude},${msg.location.longitude}`);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'⚠️ Автоматическое определение города недоступно.\n' +
|
||||
'Пожалуйста, введите название вашего города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Логируем входные координаты
|
||||
console.log(`Processing coordinates for user ${userId}: lat=${msg.location.latitude}, lon=${msg.location.longitude}`);
|
||||
|
||||
// Получаем адрес через Kakao Maps
|
||||
const address = await this.kakaoMapService.getAddressFromCoordinates(
|
||||
msg.location.latitude,
|
||||
msg.location.longitude
|
||||
);
|
||||
|
||||
if (!address) {
|
||||
console.warn(`KakaoMaps returned no address for user ${userId} coords: ${msg.location.latitude},${msg.location.longitude}`);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'❌ Не удалось определить город по вашей геолокации.\n' +
|
||||
'Пожалуйста, введите название города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем название города для сохранения
|
||||
const cityName = this.kakaoMapService.getCityNameForProfile(address);
|
||||
const displayAddress = this.kakaoMapService.formatAddressForDisplay(address);
|
||||
|
||||
// Логируем результат
|
||||
console.log(`KakaoMaps resolved for user ${userId}: city=${cityName}, address=${displayAddress}`);
|
||||
|
||||
// Временно сохраняем город (пока не подтвержден пользователем)
|
||||
userState.data.city = cityName;
|
||||
userState.step = 'confirm_city';
|
||||
|
||||
// Отправляем пользователю информацию с кнопками подтверждения
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
`✅ Мы распознали местоположение: *${displayAddress}*\n\n` +
|
||||
'Пожалуйста, подтвердите город проживания или введите название вручную.',
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✅ Подтвердить', callback_data: 'confirm_city' },
|
||||
{ text: '✏️ Ввести вручную', callback_data: 'edit_city_manual' }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'📝 Теперь расскажите немного о себе (био):\n\n' +
|
||||
'💡 Например: хобби, интересы, что ищете в отношениях и т.д.'
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling location for city:', error);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'❌ Произошла ошибка при определении города.\n' +
|
||||
'Пожалуйста, введите название города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка геолокации для определения города при редактировании профиля
|
||||
private async handleLocationForCityEdit(msg: Message, userId: string): Promise<void> {
|
||||
const chatId = msg.chat.id;
|
||||
|
||||
if (!msg.location) {
|
||||
await this.bot.sendMessage(chatId, '❌ Не удалось получить геолокацию');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Показываем индикатор загрузки
|
||||
await this.bot.sendChatAction(chatId, 'typing');
|
||||
|
||||
if (!this.kakaoMapService) {
|
||||
console.warn(`KakaoMaps not configured - user ${userId} sent coords during edit: ${msg.location.latitude},${msg.location.longitude}`);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'⚠️ Автоматическое определение города недоступно.\n' +
|
||||
'Пожалуйста, введите название вашего города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Логируем входные координаты
|
||||
console.log(`Processing coordinates for user ${userId} during edit: lat=${msg.location.latitude}, lon=${msg.location.longitude}`);
|
||||
|
||||
// Получаем адрес через Kakao Maps
|
||||
const address = await this.kakaoMapService.getAddressFromCoordinates(
|
||||
msg.location.latitude,
|
||||
msg.location.longitude
|
||||
);
|
||||
|
||||
if (!address) {
|
||||
console.warn(`KakaoMaps returned no address for user ${userId} during edit coords: ${msg.location.latitude},${msg.location.longitude}`);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'❌ Не удалось определить город по вашей геолокации.\n' +
|
||||
'Пожалуйста, введите название города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем название города для сохранения
|
||||
const cityName = this.kakaoMapService.getCityNameForProfile(address);
|
||||
const displayAddress = this.kakaoMapService.formatAddressForDisplay(address);
|
||||
|
||||
// Логируем результат
|
||||
console.log(`KakaoMaps resolved for user ${userId} during edit: city=${cityName}, address=${displayAddress}`);
|
||||
|
||||
// Временно сохраняем город в состояние редактирования
|
||||
const editState = this.profileEditStates.get(userId);
|
||||
if (editState) {
|
||||
editState.tempCity = cityName;
|
||||
}
|
||||
|
||||
// Отправляем пользователю информацию с кнопками подтверждения
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
`✅ Мы распознали местоположение: *${displayAddress}*\n\n` +
|
||||
'Пожалуйста, подтвердите город проживания или введите название вручную.',
|
||||
{
|
||||
parse_mode: 'Markdown',
|
||||
reply_markup: {
|
||||
inline_keyboard: [
|
||||
[
|
||||
{ text: '✅ Подтвердить', callback_data: 'confirm_city_edit' },
|
||||
{ text: '✏️ Ввести вручную', callback_data: 'edit_city_manual_edit' }
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error handling location for city during edit:', error);
|
||||
await this.bot.sendMessage(
|
||||
chatId,
|
||||
'❌ Произошла ошибка при определении города.\n' +
|
||||
'Пожалуйста, введите название города вручную.',
|
||||
{ reply_markup: { remove_keyboard: true } }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export class ChatService {
|
||||
private profileService: ProfileService;
|
||||
private notificationService: any; // Добавим позже правильный тип
|
||||
|
||||
constructor() {
|
||||
constructor(notificationService?: any) {
|
||||
this.profileService = new ProfileService();
|
||||
this.notificationService = notificationService;
|
||||
}
|
||||
|
||||
// Получить все чаты (матчи) пользователя
|
||||
@@ -149,7 +151,7 @@ export class ChatService {
|
||||
}
|
||||
|
||||
const row = messageResult.rows[0];
|
||||
return new Message({
|
||||
const message = new Message({
|
||||
id: row.id,
|
||||
matchId: row.match_id,
|
||||
senderId: row.sender_id,
|
||||
@@ -158,6 +160,26 @@ export class ChatService {
|
||||
isRead: row.is_read,
|
||||
createdAt: new Date(row.created_at)
|
||||
});
|
||||
|
||||
// Определяем получателя (второго участника матча)
|
||||
const match = matchResult.rows[0];
|
||||
const receiverId = match.user_id_1 === senderId ? match.user_id_2 : match.user_id_1;
|
||||
|
||||
// Отправляем уведомление получателю о новом сообщении
|
||||
if (this.notificationService) {
|
||||
try {
|
||||
await this.notificationService.sendMessageNotification(
|
||||
receiverId,
|
||||
senderId,
|
||||
content,
|
||||
matchId
|
||||
);
|
||||
} catch (notifError) {
|
||||
console.error('Error sending message notification:', notifError);
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
} catch (error) {
|
||||
console.error('Error sending message:', error);
|
||||
return null;
|
||||
|
||||
162
src/services/kakaoMapService.ts
Normal file
162
src/services/kakaoMapService.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export interface KakaoCoordinates {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export interface KakaoAddress {
|
||||
addressName: string; // Полный адрес
|
||||
region1: string; // Область (예: 서울특별시)
|
||||
region2: string; // Район (예: 강남구)
|
||||
region3: string; // Подрайон (예: 역삼동)
|
||||
city?: string; // Город (обработанный)
|
||||
}
|
||||
|
||||
export class KakaoMapService {
|
||||
private readonly apiKey: string;
|
||||
private readonly baseUrl = 'https://dapi.kakao.com/v2/local';
|
||||
|
||||
constructor(apiKey: string) {
|
||||
if (!apiKey) {
|
||||
throw new Error('Kakao Maps API key is required');
|
||||
}
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить адрес по координатам (Reverse Geocoding)
|
||||
*/
|
||||
async getAddressFromCoordinates(latitude: number, longitude: number): Promise<KakaoAddress | null> {
|
||||
try {
|
||||
const response = await axios.get(`${this.baseUrl}/geo/coord2address.json`, {
|
||||
headers: {
|
||||
'Authorization': `KakaoAK ${this.apiKey}`
|
||||
},
|
||||
params: {
|
||||
x: longitude,
|
||||
y: latitude,
|
||||
input_coord: 'WGS84'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.data && response.data.documents && response.data.documents.length > 0) {
|
||||
const doc = response.data.documents[0];
|
||||
const address = doc.address || doc.road_address;
|
||||
|
||||
if (!address) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Извлекаем город из региона
|
||||
const city = this.extractCity(address.region_1depth_name, address.region_2depth_name);
|
||||
|
||||
return {
|
||||
addressName: address.address_name || '',
|
||||
region1: address.region_1depth_name || '',
|
||||
region2: address.region_2depth_name || '',
|
||||
region3: address.region_3depth_name || '',
|
||||
city
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error getting address from Kakao Maps:', error);
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.error('Response:', error.response?.data);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Извлечь название города из регионов
|
||||
* Примеры:
|
||||
* - 서울특별시 → Seoul
|
||||
* - 경기도 수원시 → Suwon
|
||||
* - 부산광역시 → Busan
|
||||
*/
|
||||
private extractCity(region1: string, region2: string): string {
|
||||
// Убираем суффиксы типов административных единиц
|
||||
const cleanRegion1 = region1
|
||||
.replace('특별시', '') // Особый город (Сеул)
|
||||
.replace('광역시', '') // Метрополитен
|
||||
.replace('특별자치시', '') // Особый автономный город
|
||||
.replace('도', '') // Провинция
|
||||
.trim();
|
||||
|
||||
const cleanRegion2 = region2
|
||||
.replace('시', '') // Город
|
||||
.replace('군', '') // Округ
|
||||
.trim();
|
||||
|
||||
// Если region1 - это город (Сеул, Пусан, и т.д.), возвращаем его
|
||||
const specialCities = ['서울', '부산', '대구', '인천', '광주', '대전', '울산', '세종'];
|
||||
if (specialCities.some(city => region1.includes(city))) {
|
||||
return this.translateCityName(cleanRegion1);
|
||||
}
|
||||
|
||||
// Иначе возвращаем region2 (город в провинции)
|
||||
if (cleanRegion2) {
|
||||
return this.translateCityName(cleanRegion2);
|
||||
}
|
||||
|
||||
// Если не удалось определить, возвращаем очищенный region1
|
||||
return this.translateCityName(cleanRegion1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Перевод названий городов на английский/латиницу
|
||||
*/
|
||||
private translateCityName(koreanName: string): string {
|
||||
const cityMap: { [key: string]: string } = {
|
||||
'서울': 'Seoul',
|
||||
'부산': 'Busan',
|
||||
'대구': 'Daegu',
|
||||
'인천': 'Incheon',
|
||||
'광주': 'Gwangju',
|
||||
'대전': 'Daejeon',
|
||||
'울산': 'Ulsan',
|
||||
'세종': 'Sejong',
|
||||
'수원': 'Suwon',
|
||||
'성남': 'Seongnam',
|
||||
'고양': 'Goyang',
|
||||
'용인': 'Yongin',
|
||||
'창원': 'Changwon',
|
||||
'청주': 'Cheongju',
|
||||
'전주': 'Jeonju',
|
||||
'천안': 'Cheonan',
|
||||
'안산': 'Ansan',
|
||||
'안양': 'Anyang',
|
||||
'제주': 'Jeju',
|
||||
'평택': 'Pyeongtaek',
|
||||
'화성': 'Hwaseong',
|
||||
'포항': 'Pohang',
|
||||
'진주': 'Jinju',
|
||||
'강릉': 'Gangneung',
|
||||
'김해': 'Gimhae',
|
||||
'춘천': 'Chuncheon',
|
||||
'원주': 'Wonju'
|
||||
};
|
||||
|
||||
return cityMap[koreanName] || koreanName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматировать адрес для отображения пользователю
|
||||
*/
|
||||
formatAddressForDisplay(address: KakaoAddress): string {
|
||||
if (address.city) {
|
||||
return `${address.city}, ${address.region1}`;
|
||||
}
|
||||
return `${address.region2}, ${address.region1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить краткое название города для сохранения в профиле
|
||||
*/
|
||||
getCityNameForProfile(address: KakaoAddress): string {
|
||||
return address.city || address.region2;
|
||||
}
|
||||
}
|
||||
@@ -427,15 +427,20 @@ export class MatchingService {
|
||||
const userId = userProfile.userId;
|
||||
|
||||
// Определяем, каким должен быть пол показываемых профилей
|
||||
let targetGender: string;
|
||||
let targetGender: string | null;
|
||||
let genderCondition: string;
|
||||
|
||||
if (userProfile.interestedIn === 'male' || userProfile.interestedIn === 'female') {
|
||||
// Конкретный пол
|
||||
targetGender = userProfile.interestedIn;
|
||||
genderCondition = `AND p.gender = '${targetGender}'`;
|
||||
} else {
|
||||
// Если "both" или другое значение, показываем противоположный пол
|
||||
targetGender = userProfile.gender === 'male' ? 'female' : 'male';
|
||||
// Если "both" - показываем всех кроме своего пола
|
||||
targetGender = null;
|
||||
genderCondition = `AND p.gender != '${userProfile.gender}'`;
|
||||
}
|
||||
|
||||
console.log(`[DEBUG] Определен целевой пол для поиска: ${targetGender}`);
|
||||
console.log(`[DEBUG] Определен целевой пол для поиска: ${targetGender || 'any (except self)'}, условие: ${genderCondition}`);
|
||||
|
||||
// Получаем список просмотренных профилей из новой таблицы profile_views
|
||||
// и добавляем также профили из свайпов для полной совместимости
|
||||
@@ -490,7 +495,7 @@ export class MatchingService {
|
||||
FROM profiles p
|
||||
JOIN users u ON p.user_id = u.id
|
||||
WHERE p.is_visible = true
|
||||
AND p.gender = '${targetGender}'
|
||||
${genderCondition}
|
||||
${excludeCondition}
|
||||
`;
|
||||
|
||||
@@ -502,14 +507,14 @@ export class MatchingService {
|
||||
console.log(`[DEBUG] Найдено ${availableProfilesCount} доступных профилей`);
|
||||
|
||||
// Используем определенный ранее targetGender для поиска
|
||||
console.log(`[DEBUG] Поиск кандидата для gender=${targetGender}, возраст: ${userProfile.searchPreferences.minAge}-${userProfile.searchPreferences.maxAge}`);
|
||||
console.log(`[DEBUG] Поиск кандидата для gender=${targetGender || 'any'}, возраст: ${userProfile.searchPreferences.minAge}-${userProfile.searchPreferences.maxAge}`);
|
||||
|
||||
const candidateQuery = `
|
||||
SELECT 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.gender = '${targetGender}'
|
||||
${genderCondition}
|
||||
AND p.age BETWEEN ${userProfile.searchPreferences.minAge} AND ${userProfile.searchPreferences.maxAge}
|
||||
${excludeCondition}
|
||||
ORDER BY RANDOM()
|
||||
|
||||
@@ -1118,10 +1118,10 @@ export class NotificationService {
|
||||
|
||||
const lastMessageTime = new Date(result.rows[0].created_at);
|
||||
const now = new Date();
|
||||
const hoursSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / (1000 * 60 * 60);
|
||||
const secondsSinceLastMessage = (now.getTime() - lastMessageTime.getTime()) / 1000;
|
||||
|
||||
// Считаем активным если последнее сообщение было менее 10 минут назад
|
||||
return hoursSinceLastMessage < (10 / 60);
|
||||
// Считаем активным если последнее сообщение было менее 30 секунд назад
|
||||
return secondsSinceLastMessage < 30;
|
||||
} catch (error) {
|
||||
console.error('Error checking user activity:', error);
|
||||
return false;
|
||||
|
||||
@@ -46,16 +46,32 @@ export class ProfileService {
|
||||
updatedAt: now
|
||||
});
|
||||
|
||||
// Сохранение в базу данных
|
||||
// Сохранение в базу данных (UPSERT для избежания конфликта с триггером)
|
||||
await query(`
|
||||
INSERT INTO profiles (
|
||||
id, user_id, name, age, gender, interested_in, bio, photos,
|
||||
city, education, job, height, religion, dating_goal,
|
||||
is_verified, 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)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
age = EXCLUDED.age,
|
||||
gender = EXCLUDED.gender,
|
||||
interested_in = EXCLUDED.interested_in,
|
||||
bio = EXCLUDED.bio,
|
||||
photos = EXCLUDED.photos,
|
||||
city = EXCLUDED.city,
|
||||
education = EXCLUDED.education,
|
||||
job = EXCLUDED.job,
|
||||
height = EXCLUDED.height,
|
||||
religion = EXCLUDED.religion,
|
||||
dating_goal = EXCLUDED.dating_goal,
|
||||
is_verified = EXCLUDED.is_verified,
|
||||
is_visible = EXCLUDED.is_visible,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`, [
|
||||
profileId, userId, profile.name, profile.age, profile.gender, profile.interestedIn,
|
||||
profile.bio, JSON.stringify(profile.photos), profile.city, profile.education, profile.job,
|
||||
profile.bio, profile.photos, profile.city, profile.education, profile.job,
|
||||
profile.height, profile.religion, profile.datingGoal,
|
||||
profile.isVerified, profile.isVisible, profile.createdAt, profile.updatedAt
|
||||
]);
|
||||
@@ -168,9 +184,10 @@ export class ProfileService {
|
||||
switch (key) {
|
||||
case 'photos':
|
||||
case 'interests':
|
||||
case 'hobbies':
|
||||
updateFields.push(`${this.camelToSnake(key)} = $${paramIndex++}`);
|
||||
// Для PostgreSQL массивы должны быть преобразованы в JSON-строку
|
||||
updateValues.push(JSON.stringify(value));
|
||||
// PostgreSQL принимает нативные массивы
|
||||
updateValues.push(Array.isArray(value) ? value : [value]);
|
||||
break;
|
||||
case 'location':
|
||||
// Пропускаем обработку местоположения, так как колонки location нет
|
||||
|
||||
Reference in New Issue
Block a user