457 lines
13 KiB
JavaScript
457 lines
13 KiB
JavaScript
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
|
||
};
|