WhisperNet / server.ts
bonesmasher's picture
Upload 56 files
abc1805 verified
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<string, Map<string, number>>();
// Track room limits: RoomId -> Limit
const roomLimits = new Map<string, number>();
// Track nicknames: SocketId -> Nickname
const socketNicknames = new Map<string, string>();
// Track call participants: RoomId -> Set<SocketId>
const callParticipants = new Map<string, Set<string>>();
interface RoomConfig {
passwordHash?: string;
creatorId: string;
limit: number;
}
const roomConfigs = new Map<string, RoomConfig>();
const mutedUsers = new Map<string, Set<string>>(); // RoomId -> Set<UserId>
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}`);
});
});