Files
multy-stub/server/routers/kfu-m-24-1/sber_mobile/socket-chat.js
2025-06-12 21:04:12 +03:00

457 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.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) => {
// Аутентификация пользователя
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
});
} catch (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());
}
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,
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()
});
} catch (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()
});
}
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()
});
} catch (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) {
// Удаляем пользователя из всех комнат
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()
});
}
// Тестирование 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;
}
module.exports = {
ChatSocketHandler,
initializeChatSocket
};