Files
stream.graff.tech-new/server/src/index.ts
T

436 lines
14 KiB
TypeScript

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<string>;
}
interface User {
id: string;
roomId?: string;
socketId: string;
}
const rooms = new Map<string, Room>();
const users = new Map<string, User>();
// Вспомогательные функции
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);