From 4cf29c97b91caea52fd023017a642626ec63f823 Mon Sep 17 00:00:00 2001 From: DenAntonov Date: Thu, 12 Jun 2025 16:21:46 +0300 Subject: [PATCH] add chats api --- .../routers/kfu-m-24-1/sber_mobile/chats.js | 216 ++++++++++- .../kfu-m-24-1/sber_mobile/messages.js | 204 ++++++++++- .../kfu-m-24-1/sber_mobile/socket-chat.js | 340 ++++++++++++++++++ 3 files changed, 752 insertions(+), 8 deletions(-) create mode 100644 server/routers/kfu-m-24-1/sber_mobile/socket-chat.js diff --git a/server/routers/kfu-m-24-1/sber_mobile/chats.js b/server/routers/kfu-m-24-1/sber_mobile/chats.js index 983f0dd..a20d9df 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/chats.js +++ b/server/routers/kfu-m-24-1/sber_mobile/chats.js @@ -3,12 +3,33 @@ const { getSupabaseClient } = require('./supabaseClient'); // Получить все чаты по дому router.get('/chats', async (req, res) => { + console.log('🏠 [Server] GET /chats запрос получен'); + console.log('🏠 [Server] Query параметры:', req.query); + const supabase = getSupabaseClient(); const { building_id } = req.query; - if (!building_id) return res.status(400).json({ error: 'building_id required' }); - const { data, error } = await supabase.from('chats').select('*').eq('building_id', building_id); - if (error) return res.status(400).json({ error: error.message }); - res.json(data); + + if (!building_id) { + console.log('❌ [Server] Ошибка: building_id обязателен'); + return res.status(400).json({ error: 'building_id required' }); + } + + try { + console.log('🔍 [Server] Выполняем запрос к Supabase для здания:', building_id); + + const { data, error } = await supabase.from('chats').select('*').eq('building_id', building_id); + + if (error) { + console.log('❌ [Server] Ошибка Supabase:', error); + return res.status(400).json({ error: error.message }); + } + + console.log('✅ [Server] Чаты получены:', data?.length || 0, 'шт.'); + res.json(data || []); + } catch (err) { + console.log('❌ [Server] Неожиданная ошибка:', err); + res.status(500).json({ error: 'Internal server error' }); + } }); // Получить все чаты по квартире (через building_id) @@ -25,4 +46,191 @@ router.get('/chats/by-apartment', async (req, res) => { res.json(data); }); +// Создать новый чат +router.post('/chats', async (req, res) => { + const supabase = getSupabaseClient(); + const { building_id, name } = req.body; + + if (!building_id) { + return res.status(400).json({ error: 'building_id is required' }); + } + + const { data, error } = await supabase + .from('chats') + .insert({ building_id, name }) + .select() + .single(); + + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Получить конкретный чат по ID +router.get('/chats/:chat_id', async (req, res) => { + const supabase = getSupabaseClient(); + const { chat_id } = req.params; + + const { data, error } = await supabase + .from('chats') + .select('*') + .eq('id', chat_id) + .single(); + + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Обновить чат +router.put('/chats/:chat_id', async (req, res) => { + const supabase = getSupabaseClient(); + const { chat_id } = req.params; + const { name } = req.body; + + const { data, error } = await supabase + .from('chats') + .update({ name }) + .eq('id', chat_id) + .select() + .single(); + + if (error) return res.status(400).json({ error: error.message }); + res.json(data); +}); + +// Удалить чат +router.delete('/chats/:chat_id', async (req, res) => { + const supabase = getSupabaseClient(); + const { chat_id } = req.params; + + const { error } = await supabase + .from('chats') + .delete() + .eq('id', chat_id); + + if (error) return res.status(400).json({ error: error.message }); + res.json({ success: true, message: 'Chat deleted successfully' }); +}); + +// Получить статистику чата (количество сообщений, участников и т.д.) +router.get('/chats/:chat_id/stats', async (req, res) => { + const supabase = getSupabaseClient(); + const { chat_id } = req.params; + + try { + // Получаем количество сообщений + const { count: messageCount, error: messageError } = await supabase + .from('messages') + .select('*', { count: 'exact', head: true }) + .eq('chat_id', chat_id); + + if (messageError) throw messageError; + + // Получаем информацию о чате с домом + const { data: chatInfo, error: chatError } = await supabase + .from('chats') + .select(` + *, + buildings ( + id, + name, + address, + apartments ( + apartment_residents ( + user_id + ) + ) + ) + `) + .eq('id', chat_id) + .single(); + + if (chatError) throw chatError; + + // Собираем уникальные user_id жителей дома + const userIds = new Set(); + chatInfo.buildings.apartments.forEach(apartment => { + apartment.apartment_residents.forEach(resident => { + userIds.add(resident.user_id); + }); + }); + + // Получаем профили всех жителей + let uniqueResidents = []; + if (userIds.size > 0) { + const { data: profiles } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .in('id', Array.from(userIds)); + + uniqueResidents = profiles || []; + } + + res.json({ + chat_id, + chat_name: chatInfo.name, + building: { + id: chatInfo.buildings.id, + name: chatInfo.buildings.name, + address: chatInfo.buildings.address + }, + message_count: messageCount || 0, + total_residents: uniqueResidents.length, + residents: uniqueResidents + }); + } catch (error) { + res.status(400).json({ error: error.message }); + } +}); + +// Получить последнее сообщение в чате +router.get('/chats/:chat_id/last-message', async (req, res) => { + console.log('💬 [Server] GET /chats/:chat_id/last-message запрос получен'); + console.log('💬 [Server] Chat ID:', req.params.chat_id); + + const supabase = getSupabaseClient(); + const { chat_id } = req.params; + + try { + console.log('🔍 [Server] Выполняем запрос последнего сообщения для чата:', chat_id); + + // Получаем последнее сообщение + const { data: lastMessage, error } = await supabase + .from('messages') + .select('*') + .eq('chat_id', chat_id) + .order('created_at', { ascending: false }) + .limit(1) + .single(); + + let data = null; + + if (error && error.code === 'PGRST116') { + console.log('ℹ️ [Server] Сообщений в чате нет (PGRST116)'); + data = null; + } else if (error) { + console.log('❌ [Server] Ошибка Supabase при получении последнего сообщения:', error); + return res.status(400).json({ error: error.message }); + } else if (lastMessage) { + // Получаем профиль пользователя для сообщения + const { data: userProfile } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', lastMessage.user_id) + .single(); + + // Объединяем сообщение с профилем + data = { + ...lastMessage, + user_profiles: userProfile || null + }; + console.log('✅ [Server] Последнее сообщение получено для чата:', chat_id); + } + + res.json(data); + } catch (err) { + console.log('❌ [Server] Неожиданная ошибка при получении последнего сообщения:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/messages.js b/server/routers/kfu-m-24-1/sber_mobile/messages.js index b1a8188..6c0bfa3 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/messages.js +++ b/server/routers/kfu-m-24-1/sber_mobile/messages.js @@ -1,14 +1,210 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); -// Получить все сообщения в чате +// Получить все сообщения в чате с информацией о пользователе router.get('/messages', async (req, res) => { + console.log('📬 [Server] GET /messages запрос получен'); + console.log('📬 [Server] Query параметры:', req.query); + const supabase = getSupabaseClient(); - const { chat_id } = req.query; - if (!chat_id) return res.status(400).json({ error: 'chat_id required' }); - const { data, error } = await supabase.from('messages').select('*').eq('chat_id', chat_id); + const { chat_id, limit = 50, offset = 0 } = req.query; + + if (!chat_id) { + console.log('❌ [Server] Ошибка: chat_id обязателен'); + return res.status(400).json({ error: 'chat_id required' }); + } + + try { + console.log('🔍 [Server] Выполняем запрос к Supabase для чата:', chat_id); + + // Получаем сообщения + const { data: messages, error } = await supabase + .from('messages') + .select('*') + .eq('chat_id', chat_id) + .order('created_at', { ascending: false }) + .limit(limit) + .range(offset, offset + limit - 1); + + if (error) { + console.log('❌ [Server] Ошибка получения сообщений:', error); + return res.status(400).json({ error: error.message }); + } + + // Получаем профили пользователей для всех уникальных user_id + let data = messages || []; + if (data.length > 0) { + const userIds = [...new Set(data.map(msg => msg.user_id))]; + console.log('👥 [Server] Получаем профили для пользователей:', userIds); + + const { data: profiles, error: profilesError } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .in('id', userIds); + + if (!profilesError && profiles) { + // Объединяем сообщения с профилями + data = data.map(msg => ({ + ...msg, + user_profiles: profiles.find(profile => profile.id === msg.user_id) || null + })); + console.log('✅ [Server] Профили пользователей добавлены к сообщениям'); + } else { + console.log('⚠️ [Server] Ошибка получения профилей пользователей:', profilesError); + } + } + + console.log('✅ [Server] Сообщения получены:', data?.length || 0, 'шт.'); + res.json(data?.reverse() || []); // Возвращаем в хронологическом порядке + } catch (err) { + console.log('❌ [Server] Неожиданная ошибка:', err); + res.status(500).json({ error: 'Internal server error' }); + } +}); + +// Создать новое сообщение +router.post('/messages', async (req, res) => { + const supabase = getSupabaseClient(); + const { chat_id, user_id, text } = req.body; + + if (!chat_id || !user_id || !text) { + return res.status(400).json({ + error: 'chat_id, user_id, and text are required' + }); + } + + // Создаем сообщение + const { data: newMessage, error } = await supabase + .from('messages') + .insert({ chat_id, user_id, text }) + .select('*') + .single(); + if (error) return res.status(400).json({ error: error.message }); + + // Получаем профиль пользователя + const { data: userProfile } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', user_id) + .single(); + + // Объединяем сообщение с профилем + const data = { + ...newMessage, + user_profiles: userProfile || null + }; + res.json(data); }); +// Получить конкретное сообщение +router.get('/messages/:message_id', async (req, res) => { + const supabase = getSupabaseClient(); + const { message_id } = req.params; + + // Получаем сообщение + const { data: message, error } = await supabase + .from('messages') + .select('*') + .eq('id', message_id) + .single(); + + if (error) return res.status(400).json({ error: error.message }); + + // Получаем профиль пользователя + const { data: userProfile } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', message.user_id) + .single(); + + // Объединяем сообщение с профилем + const data = { + ...message, + user_profiles: userProfile || null + }; + + res.json(data); +}); + +// Получить последние сообщения для каждого чата (для списка чатов) +router.get('/chats/last-messages', async (req, res) => { + const supabase = getSupabaseClient(); + const { building_id } = req.query; + + if (!building_id) { + return res.status(400).json({ error: 'building_id required' }); + } + + // Получаем чаты и их последние сообщения через обычные запросы + const { data: chats, error: chatsError } = await supabase + .from('chats') + .select('*') + .eq('building_id', building_id); + + if (chatsError) return res.status(400).json({ error: chatsError.message }); + + // Для каждого чата получаем последнее сообщение + const chatsWithMessages = await Promise.all( + chats.map(async (chat) => { + const { data: lastMessage } = await supabase + .from('messages') + .select(` + *, + user_profiles:user_id ( + id, + full_name, + avatar_url + ) + `) + .eq('chat_id', chat.id) + .order('created_at', { ascending: false }) + .limit(1) + .single(); + + return { + ...chat, + last_message: lastMessage || null + }; + }) + ); + + res.json(chatsWithMessages); +}); + +// Удалить сообщение (только для автора) +router.delete('/messages/:message_id', async (req, res) => { + const supabase = getSupabaseClient(); + const { message_id } = req.params; + const { user_id } = req.body; + + if (!user_id) { + return res.status(400).json({ error: 'user_id required' }); + } + + // Проверяем, что пользователь является автором сообщения + const { data: message, error: fetchError } = await supabase + .from('messages') + .select('user_id') + .eq('id', message_id) + .single(); + + if (fetchError) return res.status(400).json({ error: fetchError.message }); + + if (message.user_id !== user_id) { + return res.status(403).json({ error: 'You can only delete your own messages' }); + } + + const { error } = await supabase + .from('messages') + .delete() + .eq('id', message_id); + + if (error) return res.status(400).json({ error: error.message }); + res.json({ success: true, message: 'Message deleted successfully' }); +}); + + + module.exports = router; \ No newline at end of file diff --git a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js b/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js new file mode 100644 index 0000000..d35c2a2 --- /dev/null +++ b/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js @@ -0,0 +1,340 @@ +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 +}; \ No newline at end of file