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, userName }) => { console.log(`[Chat] Message from ${userId} in room ${roomId}: ${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 savedMessage = await saveChatMessage({ sessionId: roomId, userId: userId || null, // null для анонимных пользователей content, type: "text", }); // Формируем сообщение для отправки клиентам const messageToSend = { id: savedMessage.id, senderId: userId, senderName: userName, 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); socket.emit("chat-error", { message: "Failed to save message", }); } }); // Отключение 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);