import { createServer } from "node:http"; import next from "next"; import { Server } from "socket.io"; import mongoose from "mongoose"; import bcrypt from "bcryptjs"; import dbConnect from "./lib/db"; const dev = process.env.NODE_ENV !== "production"; const hostname = "localhost"; const port = parseInt(process.env.PORT || "8000", 10); const app = next({ dev, hostname, port }); const handler = app.getRequestHandler(); app.prepare().then(async () => { try { await dbConnect(); } catch (e) { console.warn("MongoDB connection failed, running in memory-only mode:", e); } const httpServer = createServer(handler); const io = new Server(httpServer, { cors: { origin: "*", methods: ["GET", "POST"], }, }); console.log("✓ Socket.IO server initialized"); // Track room users: RoomId -> UserId -> SocketCount const roomUsers = new Map>(); // Track room limits: RoomId -> Limit const roomLimits = new Map(); // Track nicknames: SocketId -> Nickname const socketNicknames = new Map(); // Track call participants: RoomId -> Set const callParticipants = new Map>(); interface RoomConfig { passwordHash?: string; creatorId: string; limit: number; } const roomConfigs = new Map(); const mutedUsers = new Map>(); // RoomId -> Set io.on("connection", (socket) => { console.log("\n========================================"); console.log("NEW CLIENT CONNECTED:", socket.id); console.log("========================================\n"); const userId = socket.handshake.auth.userId; if (!userId) { console.log("No userId provided, ignoring"); return; } socket.on("join-room", async ({ roomId, nickname, userLimit, password }: { roomId: string; nickname?: string; userLimit?: number; password?: string }) => { let config = roomConfigs.get(roomId); // Room Creation / Initialization if (!config) { const hashedPassword = password ? await bcrypt.hash(password, 10) : undefined; config = { creatorId: userId, limit: userLimit || 10, passwordHash: hashedPassword }; roomConfigs.set(roomId, config); roomLimits.set(roomId, config.limit); } else { if (config.passwordHash) { if (!password) { socket.emit("error", "Password required"); return; } const isMatch = await bcrypt.compare(password, config.passwordHash); if (!isMatch) { socket.emit("error", "Invalid password"); return; } } } const currentLimit = config.limit; const users = roomUsers.get(roomId); const currentUniqueUsers = users ? users.size : 0; const isNewUser = !users || !users.has(userId); if (isNewUser && currentUniqueUsers >= currentLimit) { socket.emit("error", "Room is full"); return; } socket.join(roomId); if (nickname) { socketNicknames.set(socket.id, nickname); } console.log(`Socket ${socket.id} (User ${userId}) joined room ${roomId}`); if (!roomUsers.has(roomId)) { roomUsers.set(roomId, new Map()); } const roomUserMap = roomUsers.get(roomId)!; const currentCount = roomUserMap.get(userId) || 0; roomUserMap.set(userId, currentCount + 1); socket.to(roomId).emit("user-joined", { socketId: socket.id, userId, nickname }); io.to(roomId).emit("room-info", { count: roomUserMap.size, limit: currentLimit }); const roomSockets = io.sockets.adapter.rooms.get(roomId); const participants: { socketId: string; userId: string; nickname?: string; isMuted: boolean }[] = []; if (roomSockets) { for (const sid of roomSockets) { const s = io.sockets.sockets.get(sid); if (s) { const pUserId = s.handshake.auth.userId; const isMuted = mutedUsers.get(roomId)?.has(pUserId) || false; participants.push({ socketId: sid, userId: pUserId, nickname: socketNicknames.get(sid), isMuted }); } } } socket.emit("room-participants", participants); if (config.creatorId === userId) { socket.emit("room-role", { role: "creator" }); } // Send current call participants if any if (callParticipants.has(roomId)) { const callUsers = Array.from(callParticipants.get(roomId)!); socket.emit("call-participants", callUsers); } }); // Call Events socket.on("join-call", ({ roomId }) => { if (!callParticipants.has(roomId)) { callParticipants.set(roomId, new Set()); } callParticipants.get(roomId)!.add(socket.id); socket.to(roomId).emit("user-connected-to-call", { socketId: socket.id }); }); socket.on("leave-call", ({ roomId }) => { if (callParticipants.has(roomId)) { callParticipants.get(roomId)!.delete(socket.id); if (callParticipants.get(roomId)!.size === 0) { callParticipants.delete(roomId); } } socket.to(roomId).emit("user-disconnected-from-call", { socketId: socket.id }); }); socket.on("kick-user", ({ roomId, targetUserId }) => { const config = roomConfigs.get(roomId); if (!config || config.creatorId !== userId) { socket.emit("error", "Unauthorized"); return; } const roomSockets = io.sockets.adapter.rooms.get(roomId); if (roomSockets) { for (const socketId of roomSockets) { const s = io.sockets.sockets.get(socketId); if (s && s.handshake.auth.userId === targetUserId) { s.emit("kicked", "You have been kicked from the room"); s.disconnect(true); } } } }); socket.on("mute-user", ({ roomId, targetUserId }) => { const config = roomConfigs.get(roomId); if (!config || config.creatorId !== userId) { socket.emit("error", "Unauthorized"); return; } if (!mutedUsers.has(roomId)) mutedUsers.set(roomId, new Set()); mutedUsers.get(roomId)!.add(targetUserId); const roomSockets = io.sockets.adapter.rooms.get(roomId); if (roomSockets) { for (const socketId of roomSockets) { const s = io.sockets.sockets.get(socketId); if (s && s.handshake.auth.userId === targetUserId) { s.emit("muted", "You have been muted"); } } } io.to(roomId).emit("user-muted", { userId: targetUserId }); }); socket.on("unmute-user", ({ roomId, targetUserId }) => { const config = roomConfigs.get(roomId); if (!config || config.creatorId !== userId) { socket.emit("error", "Unauthorized"); return; } if (mutedUsers.has(roomId)) { mutedUsers.get(roomId)!.delete(targetUserId); const roomSockets = io.sockets.adapter.rooms.get(roomId); if (roomSockets) { for (const socketId of roomSockets) { const s = io.sockets.sockets.get(socketId); if (s && s.handshake.auth.userId === targetUserId) { s.emit("unmuted", "You have been unmuted"); } } } io.to(roomId).emit("user-unmuted", { userId: targetUserId }); } }); socket.on("update-room-settings", async ({ roomId, limit, password }) => { const config = roomConfigs.get(roomId); if (!config || config.creatorId !== userId) { socket.emit("error", "Unauthorized"); return; } if (limit) { config.limit = limit; roomLimits.set(roomId, limit); } if (password !== undefined) { if (password === "") { config.passwordHash = undefined; } else { config.passwordHash = await bcrypt.hash(password, 10); } } const users = roomUsers.get(roomId); io.to(roomId).emit("room-info", { count: users ? users.size : 0, limit: config.limit }); }); socket.on("send-message", (data) => { const userId = socket.handshake.auth.userId; const muted = mutedUsers.get(data.roomId); if (muted && muted.has(userId)) { socket.emit("error", "You are muted"); return; } socket.to(data.roomId).emit("receive-message", data); }); socket.on("message-delivered", (data) => { io.to(data.roomId).emit("message-status", { messageId: data.messageId, status: "delivered", recipientId: data.recipientId, originalSenderId: data.senderId }); }); socket.on("message-read", (data) => { io.to(data.roomId).emit("message-status", { messageId: data.messageId, status: "read", recipientId: data.recipientId, originalSenderId: data.senderId }); }); socket.on("message-reaction", (data: { roomId: string; messageId: string; reaction: string; senderId: string }) => { socket.to(data.roomId).emit("message-reaction-update", data); }); socket.on("signal", (data) => { io.to(data.target).emit("signal", { sender: socket.id, signal: data.signal, }); }); socket.on("typing-start", ({ roomId }) => { const nickname = socketNicknames.get(socket.id) || `User ${socket.id.slice(0, 4)}`; socket.to(roomId).emit("user-typing", { socketId: socket.id, nickname }); }); socket.on("typing-stop", ({ roomId }) => { socket.to(roomId).emit("user-stopped-typing", { socketId: socket.id }); }); socket.on("disconnecting", () => { for (const roomId of socket.rooms) { if (roomId !== socket.id) { // Handle Call Disconnect if (callParticipants.has(roomId)) { callParticipants.get(roomId)!.delete(socket.id); if (callParticipants.get(roomId)!.size === 0) { callParticipants.delete(roomId); } socket.to(roomId).emit("user-disconnected-from-call", { socketId: socket.id }); } const users = roomUsers.get(roomId); if (users) { const currentCount = users.get(userId) || 0; if (currentCount > 1) { users.set(userId, currentCount - 1); } else { users.delete(userId); if (users.size === 0) { roomUsers.delete(roomId); roomLimits.delete(roomId); roomConfigs.delete(roomId); mutedUsers.delete(roomId); } } socket.to(roomId).emit("user-left", { socketId: socket.id, userId }); socketNicknames.delete(socket.id); io.to(roomId).emit("room-info", { count: users.size }); } } } }); socket.on("disconnect", () => { console.log("Client disconnected:", socket.id); }); }); httpServer .once("error", (err) => { console.error(err); process.exit(1); }) .listen(port, () => { console.log(`> Ready on http://${hostname}:${port}`); }); });