const { getSupabaseClient } = require('./supabaseClient'); class ChatSocketHandler { constructor(io) { this.io = io; this.onlineUsers = new Map(); // Хранение онлайн пользователей: socket.id -> user info this.chatRooms = new Map(); // Хранение участников комнат: chat_id -> Set(socket.id) this.setupSocketHandlers(); } setupSocketHandlers() { this.io.on('connection', (socket) => { console.log(`User connected: ${socket.id}`); // Аутентификация пользователя socket.on('authenticate', async (data) => { await this.handleAuthentication(socket, data); }); // Присоединение к чату socket.on('join_chat', async (data) => { await this.handleJoinChat(socket, data); }); // Покидание чата socket.on('leave_chat', (data) => { this.handleLeaveChat(socket, data); }); // Отправка сообщения socket.on('send_message', async (data) => { await this.handleSendMessage(socket, data); }); // Пользователь начал печатать socket.on('typing_start', (data) => { this.handleTypingStart(socket, data); }); // Пользователь закончил печатать socket.on('typing_stop', (data) => { this.handleTypingStop(socket, data); }); // Отключение пользователя socket.on('disconnect', () => { this.handleDisconnect(socket); }); }); } async handleAuthentication(socket, data) { try { const { user_id, token } = data; if (!user_id) { socket.emit('auth_error', { message: 'user_id is required' }); return; } // Получаем информацию о пользователе из базы данных const supabase = getSupabaseClient(); const { data: userProfile, error } = await supabase .from('user_profiles') .select('*') .eq('id', user_id) .single(); if (error) { socket.emit('auth_error', { message: 'User not found' }); return; } // Сохраняем информацию о пользователе this.onlineUsers.set(socket.id, { user_id, socket_id: socket.id, profile: userProfile, last_seen: new Date() }); socket.user_id = user_id; socket.emit('authenticated', { message: 'Successfully authenticated', user: userProfile }); console.log(`User ${user_id} authenticated with socket ${socket.id}`); } catch (error) { console.error('Authentication error:', error); socket.emit('auth_error', { message: 'Authentication failed' }); } } async handleJoinChat(socket, data) { try { const { chat_id } = data; if (!socket.user_id) { socket.emit('error', { message: 'Not authenticated' }); return; } if (!chat_id) { socket.emit('error', { message: 'chat_id is required' }); return; } // Проверяем, что чат существует и пользователь имеет доступ к нему const supabase = getSupabaseClient(); const { data: chat, error } = await supabase .from('chats') .select(` *, buildings ( management_company_id, apartments ( apartment_residents ( user_id ) ) ) `) .eq('id', chat_id) .single(); if (error || !chat) { socket.emit('error', { message: 'Chat not found' }); return; } // Проверяем доступ пользователя к чату через квартиры в доме const hasAccess = chat.buildings.apartments.some(apartment => apartment.apartment_residents.some(resident => resident.user_id === socket.user_id ) ); if (!hasAccess) { socket.emit('error', { message: 'Access denied to this chat' }); return; } // Добавляем сокет в комнату socket.join(chat_id); // Обновляем список участников комнаты if (!this.chatRooms.has(chat_id)) { this.chatRooms.set(chat_id, new Set()); } this.chatRooms.get(chat_id).add(socket.id); socket.emit('joined_chat', { chat_id, chat: chat, message: 'Successfully joined chat' }); // Уведомляем других участников о подключении const userInfo = this.onlineUsers.get(socket.id); socket.to(chat_id).emit('user_joined', { chat_id, user: userInfo?.profile, timestamp: new Date() }); console.log(`User ${socket.user_id} joined chat ${chat_id}`); } catch (error) { console.error('Join chat error:', error); socket.emit('error', { message: 'Failed to join chat' }); } } handleLeaveChat(socket, data) { const { chat_id } = data; if (!chat_id) return; socket.leave(chat_id); // Удаляем из списка участников if (this.chatRooms.has(chat_id)) { this.chatRooms.get(chat_id).delete(socket.id); // Если комната пуста, удаляем её if (this.chatRooms.get(chat_id).size === 0) { this.chatRooms.delete(chat_id); } } // Уведомляем других участников об отключении const userInfo = this.onlineUsers.get(socket.id); socket.to(chat_id).emit('user_left', { chat_id, user: userInfo?.profile, timestamp: new Date() }); console.log(`User ${socket.user_id} left chat ${chat_id}`); } async handleSendMessage(socket, data) { try { const { chat_id, text } = data; if (!socket.user_id) { socket.emit('error', { message: 'Not authenticated' }); return; } if (!chat_id || !text) { socket.emit('error', { message: 'chat_id and text are required' }); return; } // Сохраняем сообщение в базу данных const supabase = getSupabaseClient(); const { data: message, error } = await supabase .from('messages') .insert({ chat_id, user_id: socket.user_id, text }) .select(` *, user_profiles ( id, full_name, avatar_url ) `) .single(); if (error) { socket.emit('error', { message: 'Failed to save message' }); return; } // Отправляем сообщение всем участникам чата this.io.to(chat_id).emit('new_message', { message, timestamp: new Date() }); console.log(`Message sent to chat ${chat_id} by user ${socket.user_id}`); } catch (error) { console.error('Send message error:', error); socket.emit('error', { message: 'Failed to send message' }); } } handleTypingStart(socket, data) { const { chat_id } = data; if (!socket.user_id || !chat_id) return; const userInfo = this.onlineUsers.get(socket.id); socket.to(chat_id).emit('user_typing_start', { chat_id, user: userInfo?.profile, timestamp: new Date() }); } handleTypingStop(socket, data) { const { chat_id } = data; if (!socket.user_id || !chat_id) return; const userInfo = this.onlineUsers.get(socket.id); socket.to(chat_id).emit('user_typing_stop', { chat_id, user: userInfo?.profile, timestamp: new Date() }); } handleDisconnect(socket) { console.log(`User disconnected: ${socket.id}`); // Удаляем пользователя из всех комнат this.chatRooms.forEach((participants, chat_id) => { if (participants.has(socket.id)) { participants.delete(socket.id); // Уведомляем других участников об отключении const userInfo = this.onlineUsers.get(socket.id); socket.to(chat_id).emit('user_left', { chat_id, user: userInfo?.profile, timestamp: new Date() }); // Если комната пуста, удаляем её if (participants.size === 0) { this.chatRooms.delete(chat_id); } } }); // Удаляем пользователя из списка онлайн this.onlineUsers.delete(socket.id); } // Получение списка онлайн пользователей в чате getOnlineUsersInChat(chat_id) { const participants = this.chatRooms.get(chat_id) || new Set(); const onlineUsers = []; participants.forEach(socketId => { const userInfo = this.onlineUsers.get(socketId); if (userInfo) { onlineUsers.push(userInfo.profile); } }); return onlineUsers; } // Отправка системного сообщения в чат async sendSystemMessage(chat_id, text) { this.io.to(chat_id).emit('system_message', { chat_id, text, timestamp: new Date() }); } } // Функция инициализации Socket.IO для чатов function initializeChatSocket(io) { const chatHandler = new ChatSocketHandler(io); return chatHandler; } module.exports = { ChatSocketHandler, initializeChatSocket };