const decoder = new TextDecoder(); const PORT = 8080; /** * @typedef Room * @property {string} name * @property {Client} host * @property {Client[]} clients */ /** * @typedef Client * @property {string} name * @property {WebSocket} connection * @property {Room?} room */ /** * @type {Map} */ const rooms = new Map(); /** * @type {Map} */ const clients = new Map(); function deleteRoom(room) { rooms.delete(room); for(let [, client] of clients) { if(!client.room) { client.connection.send( JSON.stringify({ t: "room_deleted", name: room }) ) } } } require("uWebSockets.js") .App({}) .ws("/", { idleTimeout: 32, maxPayloadLength: 16 * 1024 * 1024, upgrade: (res, req, context) => { console.log( `An Http connection wants to become WebSocket, URL: ${req.getUrl()}?${req.getQuery()}!` ); let name = req.getQuery("name").trim().toLocaleLowerCase(); if ( !name || typeof name !== "string" || name.length < 2 || name.length > 64 || !name.trim() ) { console.log("Invalid name"); return res.end("invalid_name"); } name = name.trim(); if ([...clients.values()].find((client) => client.name === name)) { console.log("Duplicate name"); return res.end("name_used"); } /* This immediately calls open handler, you must not use res after this call */ res.upgrade( { name, }, /* Spell these correctly */ req.getHeader("sec-websocket-key"), req.getHeader("sec-websocket-protocol"), req.getHeader("sec-websocket-extensions"), context ); }, open: (ws) => { console.log( "CON", ws.name, decoder.decode(ws.getRemoteAddressAsText()) ); clients.set(ws, { connection: ws, name: ws.name, ipa: decoder.decode(ws.getRemoteAddressAsText()), room: null, }); }, message: (ws, message, isBinary) => { if (isBinary) return ws.end(0, "invalid_message"); try { const data = JSON.parse(decoder.decode(message)); const client = clients.get(ws); console.log(client.name, data); if (data.t === "ping") return ws.ping(); switch (data.t) { case "ping": { return ws.ping(); } case "create": { if (client.room) return ws.send( JSON.stringify({ t: "error", e: "already_in_room", }) ); const name = data.name.trim().toLocaleLowerCase(); if ( !name || typeof name !== "string" || name.length < 2 || name.length > 64 || !name.trim() ) return res.send( JSON.stringify({ t: "error", e: "invalid_room_name", }) ); if ( [...rooms.values()].find( (room) => room.name === name ) ) return ws.send( JSON.stringify({ t: "error", e: "room_name_used", }) ); const room = { name: name, host: ws, clients: [client], }; rooms.set(room.name, room); client.room = room; for(let [, cclient] of clients) { if(!cclient.room) { cclient.connection.send( JSON.stringify({ t: "room_created", name }) ); } } return ws.send( JSON.stringify({ t: "create", name }) ); } case "leave": { const room = client.room; if (!room) return ws.send( JSON.stringify({ t: "error", e: "room_not_found", }) ); if (!room.clients.includes(client)) return ws.send( JSON.stringify({ t: "error", e: "not_in_room" }) ); room.clients.splice(room.clients.indexOf(client), 1); if (room.clients.length === 0) { deleteRoom(room.name); } else if (room.host == ws) { room.host = room.clients[0]; room.clients.forEach((client) => client.connection.send( JSON.stringify({ t: "host", host: client.name, }) ) ); } client.room = null; room.clients.forEach((client) => client.connection.send( JSON.stringify({ t: "leave", client: client.name, }) ) ); ws.send( JSON.stringify({ t: "left", name: client.name }) ); break; } case "join": { if (!client) return ws.end(0, "missing_client"); const room = rooms.get(data.name); if (!room) return ws.send( JSON.stringify({ t: "error", e: "room_not_found", }) ); if (client.room) return ws.send( JSON.stringify({ t: "error", e: "already_in_other_room", }) ); if (room.clients.includes(client)) return ws.send( JSON.stringify({ t: "error", e: "already_in_room", }) ); if (room.clients.length > 5) return ws.send( JSON.stringify({ t: "error", e: "room_full" }) ); room.clients.push(client); client.room = room; const srcclient = client; room.clients .filter((t) => t !== client) .forEach((client) => client.connection.send( JSON.stringify({ t: "join", client: srcclient.name, }) ) ); ws.send( JSON.stringify({ t: "joined", name: room.name, client: client.name, host: room.host.name, clients: room.clients.map((t) => t.name), }) ); break; } case "broadcast": { if (!client) return ws.end(0, "missing_client"); const room = client.room; if (!room) return ws.send( JSON.stringify({ t: "error", e: "room_not_found", }) ); if (!room.clients.includes(client)) return ws.send( JSON.stringify({ t: "error", e: "not_in_room" }) ); let clientName = client.name room.clients.forEach((client) => { client.connection.send( JSON.stringify({ t: "broadcast", client: clientName, d: data.d, }) ); }); } case "list": { ws.send( JSON.stringify({ t: "list", rooms: [...rooms.values()] .filter((t) => t.clients.length < 2) .map((t) => ({ name: t.name, host: t.host.name, count: t.clients.length, })), }) ); break; } } } catch (e) { console.warn(e); return ws.end(0, "internal"); } }, close: (ws, code, message) => { console.log("DIS1", ws, code, message); try { if (clients.get(ws)) { const client = clients.get(ws); console.log("DIS", client.name, client.ipa); let room = client.room; if (room) { room.clients.splice(room.clients.indexOf(client), 1); if (room.clients.length === 0) { deleteRoom(room.name); } else if (room.host == ws) { room.host = room.clients[0]; room.clients.forEach((client) => client.connection.send( JSON.stringify({ t: "host", host: client.name, }) ) ); } const srcclient = client; room.clients.forEach((client) => client.connection.send(JSON.stringify({ t: "leave", client: srcclient.name })) ); } } } catch (e) { console.warn("Error during closing", e); } clients.delete(ws); }, }) .get("/*", (res, req) => { res.writeStatus("200 OK").writeHeader("Content-Type", "text/plain").end("OK"); }) .listen(PORT, () => { console.log(`Listening on port ${PORT}`); });