add sockets and change subscription

This commit is contained in:
DenAntonov
2025-06-12 21:04:12 +03:00
parent bffa3fa2a3
commit 24ff712306
6 changed files with 199 additions and 83 deletions

View File

@@ -20,7 +20,9 @@ import gamehubRouter from './routers/gamehub'
import escRouter from './routers/esc' import escRouter from './routers/esc'
import connectmeRouter from './routers/connectme' import connectmeRouter from './routers/connectme'
import questioneerRouter from './routers/questioneer' 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() export const app = express()
@@ -66,6 +68,15 @@ const initServer = async () => {
const server = setIo(app) 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 = { const sess = {
secret: "super-secret-key", secret: "super-secret-key",
resave: true, resave: true,

View File

@@ -5,7 +5,14 @@ let io = null
export const setIo = (app) => { export const setIo = (app) => {
const server = createServer(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 return server
} }

View File

@@ -3,31 +3,22 @@ const { getSupabaseClient } = require('./supabaseClient');
// Получить все чаты по дому // Получить все чаты по дому
router.get('/chats', async (req, res) => { router.get('/chats', async (req, res) => {
console.log('🏠 [Server] GET /chats запрос получен');
console.log('🏠 [Server] Query параметры:', req.query);
const supabase = getSupabaseClient(); const supabase = getSupabaseClient();
const { building_id } = req.query; const { building_id } = req.query;
if (!building_id) { if (!building_id) {
console.log('❌ [Server] Ошибка: building_id обязателен');
return res.status(400).json({ error: 'building_id required' }); return res.status(400).json({ error: 'building_id required' });
} }
try { try {
console.log('🔍 [Server] Выполняем запрос к Supabase для здания:', building_id);
const { data, error } = await supabase.from('chats').select('*').eq('building_id', building_id); const { data, error } = await supabase.from('chats').select('*').eq('building_id', building_id);
if (error) { if (error) {
console.log('❌ [Server] Ошибка Supabase:', error);
return res.status(400).json({ error: error.message }); return res.status(400).json({ error: error.message });
} }
console.log('✅ [Server] Чаты получены:', data?.length || 0, 'шт.');
res.json(data || []); res.json(data || []);
} catch (err) { } catch (err) {
console.log('❌ [Server] Неожиданная ошибка:', err);
res.status(500).json({ error: 'Internal server error' }); 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) => { 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 supabase = getSupabaseClient();
const { chat_id } = req.params; const { chat_id } = req.params;
try { try {
console.log('🔍 [Server] Выполняем запрос последнего сообщения для чата:', chat_id);
// Получаем последнее сообщение // Получаем последнее сообщение
const { data: lastMessage, error } = await supabase const { data: lastMessage, error } = await supabase
.from('messages') .from('messages')
@@ -205,10 +191,8 @@ router.get('/chats/:chat_id/last-message', async (req, res) => {
let data = null; let data = null;
if (error && error.code === 'PGRST116') { if (error && error.code === 'PGRST116') {
console.log(' [Server] Сообщений в чате нет (PGRST116)');
data = null; data = null;
} else if (error) { } else if (error) {
console.log('❌ [Server] Ошибка Supabase при получении последнего сообщения:', error);
return res.status(400).json({ error: error.message }); return res.status(400).json({ error: error.message });
} else if (lastMessage) { } else if (lastMessage) {
// Получаем профиль пользователя для сообщения // Получаем профиль пользователя для сообщения
@@ -223,12 +207,10 @@ router.get('/chats/:chat_id/last-message', async (req, res) => {
...lastMessage, ...lastMessage,
user_profiles: userProfile || null user_profiles: userProfile || null
}; };
console.log('✅ [Server] Последнее сообщение получено для чата:', chat_id);
} }
res.json(data); res.json(data);
} catch (err) { } catch (err) {
console.log('❌ [Server] Неожиданная ошибка при получении последнего сообщения:', err);
res.status(500).json({ error: 'Internal server error' }); res.status(500).json({ error: 'Internal server error' });
} }
}); });

View File

@@ -15,6 +15,7 @@ const buildingsRouter = require('./buildings');
const userApartmentsRouter = require('./user_apartments'); const userApartmentsRouter = require('./user_apartments');
const avatarRouter = require('./media'); const avatarRouter = require('./media');
const supportRouter = require('./supportApi'); const supportRouter = require('./supportApi');
const { getIo } = require('../../../io');
module.exports = router; module.exports = router;

View File

@@ -1,64 +1,59 @@
const router = require('express').Router(); const router = require('express').Router();
const { getSupabaseClient } = require('./supabaseClient'); const { getSupabaseClient } = require('./supabaseClient');
const { getIo } = require('../../../io'); // Импортируем Socket.IO
// Получить все сообщения в чате с информацией о пользователе // Получить все сообщения в чате с информацией о пользователе
router.get('/messages', async (req, res) => { router.get('/messages', async (req, res) => {
console.log('📬 [Server] GET /messages запрос получен'); try {
console.log('📬 [Server] Query параметры:', req.query);
const supabase = getSupabaseClient();
const { chat_id, limit = 50, offset = 0 } = req.query; const { chat_id, limit = 50, offset = 0 } = req.query;
if (!chat_id) { if (!chat_id) {
console.log('❌ [Server] Ошибка: chat_id обязателен'); return res.status(400).json({ error: 'chat_id is required' });
return res.status(400).json({ error: 'chat_id required' });
} }
try { const supabase = getSupabaseClient();
console.log('🔍 [Server] Выполняем запрос к Supabase для чата:', chat_id);
// Получаем сообщения const { data, error } = await supabase
const { data: messages, error } = await supabase
.from('messages') .from('messages')
.select('*') .select(`
*,
user_profiles (
id,
full_name,
avatar_url
)
`)
.eq('chat_id', chat_id) .eq('chat_id', chat_id)
.order('created_at', { ascending: false }) .order('created_at', { ascending: true })
.limit(limit)
.range(offset, offset + limit - 1); .range(offset, offset + limit - 1);
if (error) { if (error) {
console.log('❌ [Server] Ошибка получения сообщений:', error); return res.status(500).json({ error: 'Failed to fetch messages' });
return res.status(400).json({ error: error.message });
} }
// Получаем профили пользователей для всех уникальных user_id // Получаем уникальные ID пользователей из сообщений, у которых нет профиля
let data = messages || []; const messagesWithoutProfiles = data.filter(msg => !msg.user_profiles);
if (data.length > 0) { const userIds = [...new Set(messagesWithoutProfiles.map(msg => msg.user_id))];
const userIds = [...new Set(data.map(msg => msg.user_id))];
console.log('👥 [Server] Получаем профили для пользователей:', userIds);
if (userIds.length > 0) {
const { data: profiles, error: profilesError } = await supabase const { data: profiles, error: profilesError } = await supabase
.from('user_profiles') .from('user_profiles')
.select('id, full_name, avatar_url') .select('id, full_name, avatar_url')
.in('id', userIds); .in('id', userIds);
if (!profilesError && profiles) { if (!profilesError && profiles) {
// Объединяем сообщения с профилями // Добавляем профили к сообщениям
data = data.map(msg => ({ data.forEach(message => {
...msg, if (!message.user_profiles) {
user_profiles: profiles.find(profile => profile.id === msg.user_id) || null message.user_profiles = profiles.find(profile => profile.id === message.user_id) || null;
})); }
console.log('✅ [Server] Профили пользователей добавлены к сообщениям'); });
} else {
console.log('⚠️ [Server] Ошибка получения профилей пользователей:', profilesError);
} }
} }
console.log('✅ [Server] Сообщения получены:', data?.length || 0, 'шт.'); res.json(data);
res.json(data?.reverse() || []); // Возвращаем в хронологическом порядке
} catch (err) { } catch (err) {
console.log('❌ [Server] Неожиданная ошибка:', err); res.status(500).json({ error: 'Unexpected error occurred' });
res.status(500).json({ error: 'Internal server error' });
} }
}); });
@@ -95,6 +90,9 @@ router.post('/messages', async (req, res) => {
user_profiles: userProfile || null user_profiles: userProfile || null
}; };
// Отправка через Socket.IO теперь происходит автоматически через Supabase Real-time подписку
// Это предотвращает дублирование сообщений
res.json(data); res.json(data);
}); });

View File

@@ -5,13 +5,29 @@ class ChatSocketHandler {
this.io = io; this.io = io;
this.onlineUsers = new Map(); // Хранение онлайн пользователей: socket.id -> user info this.onlineUsers = new Map(); // Хранение онлайн пользователей: socket.id -> user info
this.chatRooms = new Map(); // Хранение участников комнат: chat_id -> Set(socket.id) this.chatRooms = new Map(); // Хранение участников комнат: chat_id -> Set(socket.id)
this.realtimeSubscription = null; // Ссылка на подписку для управления
this.setupSocketHandlers(); this.setupSocketHandlers();
try {
this.setupRealtimeSubscription(); // Добавляем Real-time подписки
} catch (error) {
// Ignore error
}
// Запускаем тестирование через 2 секунды после инициализации
setTimeout(() => {
this.testRealtimeConnection();
}, 2000);
// Проверяем статус подписки через 5 секунд
setTimeout(() => {
this.checkSubscriptionStatus();
}, 5000);
} }
setupSocketHandlers() { setupSocketHandlers() {
this.io.on('connection', (socket) => { this.io.on('connection', (socket) => {
console.log(`User connected: ${socket.id}`);
// Аутентификация пользователя // Аутентификация пользователя
socket.on('authenticate', async (data) => { socket.on('authenticate', async (data) => {
await this.handleAuthentication(socket, data); await this.handleAuthentication(socket, data);
@@ -84,10 +100,7 @@ class ChatSocketHandler {
message: 'Successfully authenticated', message: 'Successfully authenticated',
user: userProfile user: userProfile
}); });
console.log(`User ${user_id} authenticated with socket ${socket.id}`);
} catch (error) { } catch (error) {
console.error('Authentication error:', error);
socket.emit('auth_error', { message: 'Authentication failed' }); socket.emit('auth_error', { message: 'Authentication failed' });
} }
} }
@@ -105,7 +118,6 @@ class ChatSocketHandler {
socket.emit('error', { message: 'chat_id is required' }); socket.emit('error', { message: 'chat_id is required' });
return; return;
} }
// Проверяем, что чат существует и пользователь имеет доступ к нему // Проверяем, что чат существует и пользователь имеет доступ к нему
const supabase = getSupabaseClient(); const supabase = getSupabaseClient();
const { data: chat, error } = await supabase const { data: chat, error } = await supabase
@@ -140,7 +152,6 @@ class ChatSocketHandler {
socket.emit('error', { message: 'Access denied to this chat' }); socket.emit('error', { message: 'Access denied to this chat' });
return; return;
} }
// Добавляем сокет в комнату // Добавляем сокет в комнату
socket.join(chat_id); socket.join(chat_id);
@@ -148,7 +159,10 @@ class ChatSocketHandler {
if (!this.chatRooms.has(chat_id)) { if (!this.chatRooms.has(chat_id)) {
this.chatRooms.set(chat_id, new Set()); this.chatRooms.set(chat_id, new Set());
} }
const participantsBefore = this.chatRooms.get(chat_id).size;
this.chatRooms.get(chat_id).add(socket.id); this.chatRooms.get(chat_id).add(socket.id);
const participantsAfter = this.chatRooms.get(chat_id).size;
socket.emit('joined_chat', { socket.emit('joined_chat', {
chat_id, chat_id,
@@ -158,15 +172,13 @@ class ChatSocketHandler {
// Уведомляем других участников о подключении // Уведомляем других участников о подключении
const userInfo = this.onlineUsers.get(socket.id); const userInfo = this.onlineUsers.get(socket.id);
socket.to(chat_id).emit('user_joined', { socket.to(chat_id).emit('user_joined', {
chat_id, chat_id,
user: userInfo?.profile, user: userInfo?.profile,
timestamp: new Date() timestamp: new Date()
}); });
console.log(`User ${socket.user_id} joined chat ${chat_id}`);
} catch (error) { } catch (error) {
console.error('Join chat error:', error);
socket.emit('error', { message: 'Failed to join chat' }); socket.emit('error', { message: 'Failed to join chat' });
} }
} }
@@ -196,7 +208,7 @@ class ChatSocketHandler {
timestamp: new Date() timestamp: new Date()
}); });
console.log(`User ${socket.user_id} left chat ${chat_id}`);
} }
async handleSendMessage(socket, data) { async handleSendMessage(socket, data) {
@@ -243,9 +255,7 @@ class ChatSocketHandler {
timestamp: new Date() timestamp: new Date()
}); });
console.log(`Message sent to chat ${chat_id} by user ${socket.user_id}`);
} catch (error) { } catch (error) {
console.error('Send message error:', error);
socket.emit('error', { message: 'Failed to send message' }); socket.emit('error', { message: 'Failed to send message' });
} }
} }
@@ -277,7 +287,6 @@ class ChatSocketHandler {
} }
handleDisconnect(socket) { handleDisconnect(socket) {
console.log(`User disconnected: ${socket.id}`);
// Удаляем пользователя из всех комнат // Удаляем пользователя из всех комнат
this.chatRooms.forEach((participants, chat_id) => { this.chatRooms.forEach((participants, chat_id) => {
@@ -326,11 +335,119 @@ class ChatSocketHandler {
timestamp: new Date() 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 для чатов // Функция инициализации Socket.IO для чатов
function initializeChatSocket(io) { function initializeChatSocket(io) {
const chatHandler = new ChatSocketHandler(io); const chatHandler = new ChatSocketHandler(io);
return chatHandler; return chatHandler;
} }