import { Elysia } from "elysia"; import cors from "@elysiajs/cors"; import { authController } from "./controllers/auth"; import { sessionController } from "./controllers/session"; import { companyController } from "./controllers/company"; import { branchController } from "./controllers/branch"; import { serverController } from "./controllers/server"; import { chatController } from "./controllers/chat"; import { serverSessionService } from "./services/serverSession"; import { saveChatMessage } from "./services/chat"; import { Server } from "socket.io"; import { createServer } from "http"; import { AddressInfo } from "net"; const app = new Elysia(); app.use( cors({ origin: true, // credentials: true, }) ); app.use(authController); app.use(sessionController); app.use(companyController); app.use(branchController); app.use(serverController); app.use(chatController); app.listen(process.env.PORT || 3000); console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ); // Setup Socket.IO для WebRTC на отдельном порту const httpServer = createServer(); const io = new Server(httpServer, { cors: { origin: "*", }, }); httpServer.listen(process.env.SOCKET_PORT || 3001, () => { console.log( `🎥 WebRTC Socket.IO server running on port ${ (httpServer.address() as AddressInfo).port }` ); }); interface Room { id: string; participants: Set; } interface User { id: string; roomId?: string; socketId: string; } const rooms = new Map(); const users = new Map(); // Вспомогательные функции function findUserBySocketId(socketId: string): User | undefined { for (const [userId, user] of users.entries()) { if (user.socketId === socketId) { return user; } } return undefined; } function findSocketIdByUserId(userId: string): string | undefined { const user = users.get(userId); return user?.socketId; } io.on("connection", (socket) => { console.log(`[WebRTC] User connected: ${socket.id}`); // Присоединение к комнате socket.on( "join-room", async ({ roomId, userId, isAudioEnabled, isVideoEnabled }) => { console.log( `[WebRTC] User ${userId} (socket: ${socket.id}) joining room ${roomId}, audio: ${isAudioEnabled}, video: ${isVideoEnabled}` ); // Покинуть предыдущую комнату если была const existingUser = users.get(userId); if (existingUser?.roomId) { console.log( `[WebRTC] User ${userId} leaving previous room ${existingUser.roomId}` ); socket.leave(existingUser.roomId); const prevRoom = rooms.get(existingUser.roomId); if (prevRoom) { prevRoom.participants.delete(userId); socket.to(existingUser.roomId).emit("user-left", userId); } } // Присоединиться к новой комнате socket.join(roomId); // Создать комнату если не существует if (!rooms.has(roomId)) { console.log(`[WebRTC] Creating new room: ${roomId}`); rooms.set(roomId, { id: roomId, participants: new Set(), }); } const room = rooms.get(roomId)!; room.participants.add(userId); // Сохранить пользователя users.set(userId, { id: userId, roomId, socketId: socket.id, }); console.log( `[WebRTC] Room ${roomId} now has participants:`, Array.from(room.participants) ); // Уведомить других участников socket.to(roomId).emit("user-joined", userId); console.log( `[WebRTC] Notified room ${roomId} about user ${userId} joining` ); // Отправить состояние аудио/видео нового пользователя всем в комнате socket .to(roomId) .emit("audio-toggle", { userId, isEnabled: isAudioEnabled !== false }); socket .to(roomId) .emit("video-toggle", { userId, isEnabled: isVideoEnabled !== false }); // Отправить список участников новому пользователю const participants = Array.from(room.participants).filter( (id) => id !== userId ); console.log( `[WebRTC] Sending participant list to ${userId}:`, participants ); socket.emit("room-participants", participants); } ); // Покидание комнаты socket.on("leave-room", ({ roomId, userId }) => { console.log(`[WebRTC] User ${userId} leaving room ${roomId}`); socket.leave(roomId); const room = rooms.get(roomId); if (room) { room.participants.delete(userId); socket.to(roomId).emit("user-left", userId); // Удалить пустую комнату if (room.participants.size === 0) { rooms.delete(roomId); console.log(`[WebRTC] Deleted empty room ${roomId}`); } } const user = users.get(userId); if (user) { user.roomId = undefined; } }); // WebRTC сигнализация socket.on("offer", ({ target, offer }) => { console.log(`[WebRTC] Offer from ${socket.id} to ${target}`); const targetSocketId = findSocketIdByUserId(target); const senderUser = findUserBySocketId(socket.id); if (targetSocketId && senderUser) { socket.to(targetSocketId).emit("offer", { offer, sender: senderUser.id, }); } }); socket.on("answer", ({ target, answer }) => { console.log(`[WebRTC] Answer from ${socket.id} to ${target}`); const targetSocketId = findSocketIdByUserId(target); const senderUser = findUserBySocketId(socket.id); if (targetSocketId && senderUser) { socket.to(targetSocketId).emit("answer", { answer, sender: senderUser.id, }); } }); socket.on("ice-candidate", ({ target, candidate }) => { console.log(`[WebRTC] ICE candidate from ${socket.id} to ${target}`); const targetSocketId = findSocketIdByUserId(target); const senderUser = findUserBySocketId(socket.id); if (targetSocketId && senderUser) { socket.to(targetSocketId).emit("ice-candidate", { candidate, sender: senderUser.id, }); } }); // Обработка audio/video toggle socket.on("audio-toggle", ({ roomId, userId, isEnabled }) => { console.log( `[WebRTC] Audio toggle from ${userId} in room ${roomId}: ${isEnabled}` ); // Отправляем всем в комнате (кроме отправителя) socket.to(roomId).emit("audio-toggle", { userId, isEnabled }); }); socket.on("video-toggle", ({ roomId, userId, isEnabled }) => { console.log( `[WebRTC] Video toggle from ${userId} in room ${roomId}: ${isEnabled}` ); // Отправляем всем в комнате (кроме отправителя) socket.to(roomId).emit("video-toggle", { userId, isEnabled }); }); // Обработка speaking state socket.on("speaking-state", ({ roomId, userId, isSpeaking }) => { // Отправляем всем в комнате (кроме отправителя) socket.to(roomId).emit("speaking-state", { userId, isSpeaking }); }); // Обработка сообщений чата socket.on( "chat-message", async ({ roomId, userId, content, senderName, guestId }) => { console.log(`[Chat] Received message:`, { roomId, userId, guestId, senderName, content, }); const user = findUserBySocketId(socket.id); if (!user || user.roomId !== roomId) { console.warn(`[Chat] User ${socket.id} is not in room ${roomId}`); return; } try { // Определяем имя отправителя const finalSenderName = senderName || "Гость"; console.log( `[Chat] Preparing to save message with senderName: "${finalSenderName}"` ); // Валидация UUID const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!uuidRegex.test(roomId)) { console.error(`[Chat] Invalid roomId/sessionId format: ${roomId}`); throw new Error("Invalid sessionId format"); } if (userId && !uuidRegex.test(userId)) { console.error(`[Chat] Invalid userId format: ${userId}`); throw new Error("Invalid userId format"); } if (guestId && !uuidRegex.test(guestId)) { console.error(`[Chat] Invalid guestId format: ${guestId}`); throw new Error("Invalid guestId format"); } // Проверяем существование сессии console.log(`[Chat] Checking if session exists: ${roomId}`); try { const sessionExists = await serverSessionService.findById(roomId); if (!sessionExists) { console.error(`[Chat] Session not found in database: ${roomId}`); console.error( `[Chat] This might cause foreign key constraint error` ); // Не возвращаемся - попробуем сохранить и увидим точную ошибку БД } else { console.log(`[Chat] Session found:`, sessionExists.id); } } catch (sessionCheckError) { console.error(`[Chat] Error checking session:`, sessionCheckError); // Продолжаем - увидим точную ошибку при сохранении } // Сохраняем сообщение в БД // Приоритет: сначала userId (если авторизован), затем guestId (если гость) const messageData = { sessionId: roomId, userId: userId || null, // userId для авторизованных пользователей guestId: userId ? null : (guestId || null), // guestId только если нет userId senderName: finalSenderName, // Имя отправителя content, type: "text" as const, }; console.log( `[Chat] Saving message to DB:`, JSON.stringify(messageData, null, 2) ); const savedMessage = await saveChatMessage(messageData); console.log(`[Chat] Message saved successfully:`, savedMessage); // Формируем сообщение для отправки клиентам // senderId - это либо userId (приоритет), либо guestId const messageToSend = { id: savedMessage.id, sessionId: savedMessage.sessionId, // Добавляем sessionId для фильтрации на клиенте senderId: savedMessage.userId || savedMessage.guestId, senderName: savedMessage.senderName, content: savedMessage.content, timestamp: savedMessage.createdAt, type: savedMessage.type, }; // Отправляем всем в комнате (включая отправителя) io.to(roomId).emit("chat-message", messageToSend); console.log(`[Chat] Message broadcasted to room ${roomId}`); } catch (error) { console.error(`[Chat] Error saving message:`, error); console.error(`[Chat] Error details:`, JSON.stringify(error, null, 2)); // Отправляем детальную ошибку на клиент const errorMessage = error instanceof Error ? error.message : "Failed to save message"; const errorDetails = error && typeof error === "object" ? (error as any).detail || (error as any).message || errorMessage : errorMessage; console.error(`[Chat] Sending error to client:`, errorDetails); socket.emit("chat-error", { message: errorDetails, }); } } ); // Отключение socket.on("disconnect", () => { console.log(`[WebRTC] User disconnected: ${socket.id}`); // Найти пользователя по socket ID const disconnectedUser = findUserBySocketId(socket.id); if (disconnectedUser) { users.delete(disconnectedUser.id); if (disconnectedUser.roomId) { const room = rooms.get(disconnectedUser.roomId); if (room) { room.participants.delete(disconnectedUser.id); socket .to(disconnectedUser.roomId) .emit("user-left", disconnectedUser.id); // Удалить пустую комнату if (room.participants.size === 0) { rooms.delete(disconnectedUser.roomId); console.log( `[WebRTC] Deleted empty room ${disconnectedUser.roomId}` ); } } } } }); }); // Запуск фоновой задачи для автоматического назначения серверов const AUTO_ASSIGN_INTERVAL_MS = parseInt( process.env.AUTO_ASSIGN_INTERVAL_MS || "1000", 10 ); // 1 секунда по умолчанию async function autoAssignServersTask() { try { const results = await serverSessionService.autoAssignServers(); if (results.total > 0) { console.log( `[${new Date().toISOString()}] 🎯 Auto-assign: ${ results.assigned } assigned, ${results.failed} failed из ${results.total}` ); if (results.errors.length > 0) { console.error( `[${new Date().toISOString()}] ❌ Ошибки назначения:`, results.errors ); } } } catch (error) { console.error( `[${new Date().toISOString()}] ❌ Ошибка auto-assign:`, error instanceof Error ? error.message : error ); } // Планируем следующий запуск setTimeout(autoAssignServersTask, AUTO_ASSIGN_INTERVAL_MS); } // Запускаем через 1 секунду после старта сервера setTimeout(() => { console.log( `[${new Date().toISOString()}] 🤖 Запуск фоновой задачи auto-assign (интервал: ${AUTO_ASSIGN_INTERVAL_MS}ms)` ); autoAssignServersTask(); }, 1000);