const WebSocket = require('ws'); const http = require('http'); const crypto = require('crypto'); const PORT = 3000; const INACTIVITY_CHECK = 30 * 1000; // check every 30s const DEFAULT_INACTIVITY = 5 * 60 * 1000; // 5 min default const MAX_INACTIVITY = 24 * 60 * 60 * 1000; // 24 hour max const MAX_PAYLOAD = 25 * 1024 * 1024; // 25MB max message size // roomId -> { // clients: Map, // creatorId, // inactivityLimit, // ms // nukeOnLeave // bool // } const rooms = new Map(); const ADJECTIVES = [ 'null','void','dark','silent','ghost','lost','blind','cold', 'dead','zero','blank','grey','static','empty','hollow','faded', 'neon','rogue','stray','anon','hidden','masked','veiled','mute' ]; const NOUNS = [ 'node','signal','proxy','cipher','relay','packet','vector','daemon', 'shell','byte','socket','kernel','loop','thread','stack','hash', 'gate','root','shard','flux','drift','echo','pulse','trace' ]; function randomName() { const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]; const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]; const num = Math.floor(Math.random() * 99) + 1; return `${adj}_${noun}_${num}`; } function generateClientId() { return crypto.randomBytes(8).toString('hex'); } function broadcast(room, message, excludeWs = null) { const data = JSON.stringify(message); for (const [ws] of room.clients) { if (ws !== excludeWs && ws.readyState === WebSocket.OPEN) { ws.send(data); } } } function sendTo(ws, message) { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(message)); } } function nukeRoom(roomId) { const room = rooms.get(roomId); if (!room) return; for (const [ws] of room.clients) { sendTo(ws, { type: 'nuked' }); ws.close(); } rooms.delete(roomId); console.log(`[nuke] room ${roomId} destroyed`); } function leaveRoom(roomId, ws) { const room = rooms.get(roomId); if (!room) return; const client = room.clients.get(ws); if (!client) return; const wasCreator = client.id === room.creatorId; room.clients.delete(ws); console.log(`[leave] ${client.name} left room ${roomId} (creator: ${wasCreator})`); // Nuke on host leave if (wasCreator && room.nukeOnLeave && room.clients.size > 0) { console.log(`[nuke-on-leave] creator left, nuking room ${roomId}`); nukeRoom(roomId); return; } broadcast(room, { type: 'system', text: `${client.name} disconnected.` }); broadcast(room, { type: 'userlist', users: [...room.clients.values()].map(c => c.name) }); if (room.clients.size === 0) { rooms.delete(roomId); console.log(`[cleanup] room ${roomId} empty, deleted`); } } // Inactivity checker setInterval(() => { const now = Date.now(); for (const [roomId, room] of rooms) { for (const [ws, client] of room.clients) { if (now - client.lastActive > room.inactivityLimit) { console.log(`[inactivity] kicking ${client.name} from ${roomId}`); sendTo(ws, { type: 'kicked', reason: 'inactivity' }); leaveRoom(roomId, ws); ws.close(); } } } }, INACTIVITY_CHECK); const server = http.createServer((req, res) => { res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end('NulledRoom'); }); const wss = new WebSocket.Server({ server, maxPayload: MAX_PAYLOAD }); wss.on('connection', (ws) => { let currentRoomId = null; ws.on('message', (raw) => { let msg; try { msg = JSON.parse(raw); } catch { return; } // Update activity if (currentRoomId) { const room = rooms.get(currentRoomId); if (room && room.clients.has(ws)) { room.clients.get(ws).lastActive = Date.now(); } } switch (msg.type) { case 'create': { const roomId = msg.roomId; if (!roomId || rooms.has(roomId)) { sendTo(ws, { type: 'error', text: 'Room already exists or invalid ID.' }); return; } // Inactivity limit — clamp between 1 min and 24 hours let inactivityLimit = DEFAULT_INACTIVITY; if (msg.inactivityMinutes) { const mins = parseInt(msg.inactivityMinutes); if (!isNaN(mins)) { inactivityLimit = Math.min(Math.max(mins * 60 * 1000, 60000), MAX_INACTIVITY); } } const nukeOnLeave = msg.nukeOnLeave === true; const clientId = generateClientId(); const name = randomName(); rooms.set(roomId, { clients: new Map([[ws, { id: clientId, name, lastActive: Date.now() }]]), creatorId: clientId, inactivityLimit, nukeOnLeave }); currentRoomId = roomId; sendTo(ws, { type: 'joined', roomId, name, clientId, isCreator: true, inactivityLimit, nukeOnLeave }); console.log(`[create] room ${roomId} by ${name} | inactivity: ${inactivityLimit/60000}min | nukeOnLeave: ${nukeOnLeave}`); break; } case 'join': { const roomId = msg.roomId; const room = rooms.get(roomId); if (!room) { sendTo(ws, { type: 'error', text: 'Room not found.' }); return; } const clientId = generateClientId(); const name = randomName(); room.clients.set(ws, { id: clientId, name, lastActive: Date.now() }); currentRoomId = roomId; sendTo(ws, { type: 'joined', roomId, name, clientId, isCreator: false }); broadcast(room, { type: 'system', text: `${name} connected.` }, ws); const userlist = [...room.clients.values()].map(c => c.name); broadcast(room, { type: 'userlist', users: userlist }); sendTo(ws, { type: 'userlist', users: userlist }); console.log(`[join] ${name} joined room ${roomId}`); break; } case 'message': { if (!currentRoomId) return; const room = rooms.get(currentRoomId); if (!room) return; const client = room.clients.get(ws); if (!client) return; // payload = encrypted blob (text or image), server never sees plaintext broadcast(room, { type: 'message', from: client.name, payload: msg.payload, isImage: msg.isImage === true // flag so receiver knows to render as image }, ws); break; } case 'nuke': { if (!currentRoomId) return; const room = rooms.get(currentRoomId); if (!room) return; const client = room.clients.get(ws); if (!client || client.id !== room.creatorId) { sendTo(ws, { type: 'error', text: 'Only the room creator can nuke.' }); return; } console.log(`[nuke] ${client.name} nuked room ${currentRoomId}`); nukeRoom(currentRoomId); break; } case 'rejoin': { // Guest silently reconnecting — slide back in with same name, no announcement const roomId = msg.roomId; const room = rooms.get(roomId); if (!room) { sendTo(ws, { type: 'error', text: 'Room not found.' }); return; } const clientId = generateClientId(); const name = msg.name || randomName(); // keep same name if provided room.clients.set(ws, { id: clientId, name, lastActive: Date.now() }); currentRoomId = roomId; sendTo(ws, { type: 'rejoined', roomId, name, clientId }); // Update user list for everyone (including rejoiner) const userlist = [...room.clients.values()].map(c => c.name); broadcast(room, { type: 'userlist', users: userlist }); sendTo(ws, { type: 'userlist', users: userlist }); console.log(`[rejoin] ${name} silently rejoined room ${roomId}`); break; } case 'ping': { // keepalive, lastActive already updated above break; } } }); ws.on('close', () => { if (currentRoomId) leaveRoom(currentRoomId, ws); }); ws.on('error', (err) => { console.error('[ws error]', err.message); }); }); server.listen(PORT, () => { console.log(`NulledRoom server running on port ${PORT}`); });