diff --git a/server/index.ts b/server/index.ts index d39f65f..3d4f58e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -20,7 +20,9 @@ import gamehubRouter from './routers/gamehub' import escRouter from './routers/esc' import connectmeRouter from './routers/connectme' import questioneerRouter from './routers/questioneer' -import { setIo } from './io' +import { setIo, getIo } from './io' +// Импорт обработчика чата +const { initializeChatSocket } = require('./routers/kfu-m-24-1/sber_mobile/socket-chat') export const app = express() @@ -65,6 +67,15 @@ const initServer = async () => { console.log('warming up 🔥') const server = setIo(app) + + // Инициализация Socket.IO для чата + const io = getIo() + if (io) { + const chatHandler = initializeChatSocket(io) + // Сохраняем ссылку на chat handler для доступа из эндпоинтов + io.chatHandler = chatHandler + console.log('✅ Socket.IO для чата инициализирован') + } const sess = { secret: "super-secret-key", diff --git a/server/io.ts b/server/io.ts index 71833d6..4121b06 100644 --- a/server/io.ts +++ b/server/io.ts @@ -5,7 +5,14 @@ let io = null export const setIo = (app) => { const server = createServer(app) - io = new Server(server, {}) + io = new Server(server, { + cors: { + origin: "*", + methods: ["GET", "POST"], + credentials: false + }, + transports: ['websocket', 'polling'] + }) return server } 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 a20d9df..047ac37 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/chats.js +++ b/server/routers/kfu-m-24-1/sber_mobile/chats.js @@ -3,31 +3,22 @@ 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) { - 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' }); } }); @@ -184,15 +175,10 @@ router.get('/chats/:chat_id/stats', async (req, res) => { // Получить последнее сообщение в чате 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') @@ -205,10 +191,8 @@ router.get('/chats/:chat_id/last-message', async (req, res) => { 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) { // Получаем профиль пользователя для сообщения @@ -223,12 +207,10 @@ router.get('/chats/:chat_id/last-message', async (req, res) => { ...lastMessage, user_profiles: userProfile || null }; - console.log('✅ [Server] Последнее сообщение получено для чата:', chat_id); - } + } - res.json(data); + res.json(data); } catch (err) { - console.log('❌ [Server] Неожиданная ошибка при получении последнего сообщения:', err); res.status(500).json({ error: 'Internal server error' }); } }); diff --git a/server/routers/kfu-m-24-1/sber_mobile/index.js b/server/routers/kfu-m-24-1/sber_mobile/index.js index e419be7..4dfad13 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/index.js +++ b/server/routers/kfu-m-24-1/sber_mobile/index.js @@ -15,6 +15,7 @@ const buildingsRouter = require('./buildings'); const userApartmentsRouter = require('./user_apartments'); const avatarRouter = require('./media'); const supportRouter = require('./supportApi'); +const { getIo } = require('../../../io'); module.exports = router; 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 6c0bfa3..f729679 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/messages.js +++ b/server/routers/kfu-m-24-1/sber_mobile/messages.js @@ -1,64 +1,59 @@ const router = require('express').Router(); const { getSupabaseClient } = require('./supabaseClient'); +const { getIo } = require('../../../io'); // Импортируем Socket.IO // Получить все сообщения в чате с информацией о пользователе router.get('/messages', async (req, res) => { - console.log('📬 [Server] GET /messages запрос получен'); - console.log('📬 [Server] Query параметры:', req.query); - - const supabase = getSupabaseClient(); - 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 }); + const { chat_id, limit = 50, offset = 0 } = req.query; + + if (!chat_id) { + return res.status(400).json({ error: 'chat_id is required' }); } + + const supabase = getSupabaseClient(); - // Получаем профили пользователей для всех уникальных 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, error } = await supabase + .from('messages') + .select(` + *, + user_profiles ( + id, + full_name, + avatar_url + ) + `) + .eq('chat_id', chat_id) + .order('created_at', { ascending: true }) + .range(offset, offset + limit - 1); + + if (error) { + return res.status(500).json({ error: 'Failed to fetch messages' }); + } + + // Получаем уникальные ID пользователей из сообщений, у которых нет профиля + const messagesWithoutProfiles = data.filter(msg => !msg.user_profiles); + const userIds = [...new Set(messagesWithoutProfiles.map(msg => msg.user_id))]; + + if (userIds.length > 0) { 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); + // Добавляем профили к сообщениям + data.forEach(message => { + if (!message.user_profiles) { + message.user_profiles = profiles.find(profile => profile.id === message.user_id) || null; + } + }); } - } - - console.log('✅ [Server] Сообщения получены:', data?.length || 0, 'шт.'); - res.json(data?.reverse() || []); // Возвращаем в хронологическом порядке + } + + res.json(data); } catch (err) { - console.log('❌ [Server] Неожиданная ошибка:', err); - res.status(500).json({ error: 'Internal server error' }); + res.status(500).json({ error: 'Unexpected error occurred' }); } }); @@ -94,6 +89,9 @@ router.post('/messages', async (req, res) => { ...newMessage, user_profiles: userProfile || null }; + + // Отправка через Socket.IO теперь происходит автоматически через Supabase Real-time подписку + // Это предотвращает дублирование сообщений res.json(data); }); 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 index d35c2a2..62102c5 100644 --- a/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js +++ b/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js @@ -5,13 +5,29 @@ class ChatSocketHandler { this.io = io; this.onlineUsers = new Map(); // Хранение онлайн пользователей: socket.id -> user info this.chatRooms = new Map(); // Хранение участников комнат: chat_id -> Set(socket.id) + this.realtimeSubscription = null; // Ссылка на подписку для управления + this.setupSocketHandlers(); + + try { + this.setupRealtimeSubscription(); // Добавляем Real-time подписки + } catch (error) { + // Ignore error + } + + // Запускаем тестирование через 2 секунды после инициализации + setTimeout(() => { + this.testRealtimeConnection(); + }, 2000); + + // Проверяем статус подписки через 5 секунд + setTimeout(() => { + this.checkSubscriptionStatus(); + }, 5000); } setupSocketHandlers() { this.io.on('connection', (socket) => { - console.log(`User connected: ${socket.id}`); - // Аутентификация пользователя socket.on('authenticate', async (data) => { await this.handleAuthentication(socket, data); @@ -84,10 +100,7 @@ class ChatSocketHandler { 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' }); } } @@ -105,7 +118,6 @@ class ChatSocketHandler { socket.emit('error', { message: 'chat_id is required' }); return; } - // Проверяем, что чат существует и пользователь имеет доступ к нему const supabase = getSupabaseClient(); const { data: chat, error } = await supabase @@ -140,7 +152,6 @@ class ChatSocketHandler { socket.emit('error', { message: 'Access denied to this chat' }); return; } - // Добавляем сокет в комнату socket.join(chat_id); @@ -148,8 +159,11 @@ class ChatSocketHandler { if (!this.chatRooms.has(chat_id)) { this.chatRooms.set(chat_id, new Set()); } + + const participantsBefore = this.chatRooms.get(chat_id).size; this.chatRooms.get(chat_id).add(socket.id); - + const participantsAfter = this.chatRooms.get(chat_id).size; + socket.emit('joined_chat', { chat_id, chat: chat, @@ -158,15 +172,13 @@ class ChatSocketHandler { // Уведомляем других участников о подключении 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' }); } } @@ -196,7 +208,7 @@ class ChatSocketHandler { timestamp: new Date() }); - console.log(`User ${socket.user_id} left chat ${chat_id}`); + } async handleSendMessage(socket, data) { @@ -243,9 +255,7 @@ class ChatSocketHandler { 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' }); } } @@ -277,7 +287,6 @@ class ChatSocketHandler { } handleDisconnect(socket) { - console.log(`User disconnected: ${socket.id}`); // Удаляем пользователя из всех комнат this.chatRooms.forEach((participants, chat_id) => { @@ -326,11 +335,119 @@ class ChatSocketHandler { timestamp: new Date() }); } + + // Тестирование Real-time подписки + async testRealtimeConnection() { + try { + const supabase = getSupabaseClient(); + if (!supabase) { + return false; + } + + // Создаем тестовый канал для проверки подключения + const testChannel = supabase + .channel('test_connection') + .subscribe((status, error) => { + if (status === 'SUBSCRIBED') { + // Отписываемся от тестового канала + setTimeout(() => { + testChannel.unsubscribe(); + }, 2000); + } + }); + + return true; + } catch (error) { + return false; + } + } + + // Проверка статуса подписки + checkSubscriptionStatus() { + if (this.realtimeSubscription) { + return true; + } else { + return false; + } + } + + setupRealtimeSubscription() { + // Добавляем небольшую задержку, чтобы убедиться, что Supabase клиент инициализирован + setTimeout(() => { + this._doSetupRealtimeSubscription(); + }, 1000); + } + + _doSetupRealtimeSubscription() { + try { + const supabase = getSupabaseClient(); + + if (!supabase) { + return; + } + + // Подписываемся на изменения в таблице messages + const subscription = supabase + .channel('messages_changes') + .on( + 'postgres_changes', + { + event: 'INSERT', + schema: 'public', + table: 'messages' + }, + async (payload) => { + try { + const newMessage = payload.new; + if (!newMessage) { + return; + } + + if (!newMessage.chat_id) { + return; + } + + // Получаем профиль пользователя + const { data: userProfile, error: profileError } = await supabase + .from('user_profiles') + .select('id, full_name, avatar_url') + .eq('id', newMessage.user_id) + .single(); + + // Объединяем сообщение с профилем + const messageWithProfile = { + ...newMessage, + user_profiles: userProfile || null + }; + + // Проверяем, есть ли участники в чате + const chatRoomParticipants = this.chatRooms.get(newMessage.chat_id); + + // Отправляем сообщение через Socket.IO всем участникам чата + this.io.to(newMessage.chat_id).emit('new_message', { + message: messageWithProfile, + timestamp: new Date() + }); + } catch (callbackError) { + // Ignore error + } + } + ) + .subscribe(); + + // Сохраняем ссылку на подписку для возможности отписки + this.realtimeSubscription = subscription; + + } catch (error) { + // Ignore error + } + } } // Функция инициализации Socket.IO для чатов function initializeChatSocket(io) { const chatHandler = new ChatSocketHandler(io); + return chatHandler; }