import http from "node:http"; import { readFile } from "node:fs/promises"; import { extname, join, normalize } from "node:path"; import { randomUUID } from "node:crypto"; const port = Number(process.env.PORT || 3000); const host = process.env.HOST || "127.0.0.1"; const captchaPassphrase = process.env.CAPTCHA || "kkkmeet"; const publicDir = join(process.cwd(), "public"); const rooms = new Map(); const watchStates = new Map(); const presentationStates = new Map(); const defaultRoomId = "main"; const captchaGlyphs = { a: ["01110", "10001", "10001", "11111", "10001", "10001", "10001"], b: ["11110", "10001", "10001", "11110", "10001", "10001", "11110"], c: ["01111", "10000", "10000", "10000", "10000", "10000", "01111"], d: ["11110", "10001", "10001", "10001", "10001", "10001", "11110"], e: ["11111", "10000", "10000", "11110", "10000", "10000", "11111"], f: ["11111", "10000", "10000", "11110", "10000", "10000", "10000"], g: ["01111", "10000", "10000", "10111", "10001", "10001", "01111"], h: ["10001", "10001", "10001", "11111", "10001", "10001", "10001"], i: ["11111", "00100", "00100", "00100", "00100", "00100", "11111"], j: ["00111", "00010", "00010", "00010", "10010", "10010", "01100"], k: ["10001", "10010", "10100", "11000", "10100", "10010", "10001"], l: ["10000", "10000", "10000", "10000", "10000", "10000", "11111"], m: ["10001", "11011", "10101", "10101", "10001", "10001", "10001"], n: ["10001", "11001", "10101", "10011", "10001", "10001", "10001"], o: ["01110", "10001", "10001", "10001", "10001", "10001", "01110"], p: ["11110", "10001", "10001", "11110", "10000", "10000", "10000"], q: ["01110", "10001", "10001", "10001", "10101", "10010", "01101"], r: ["11110", "10001", "10001", "11110", "10100", "10010", "10001"], s: ["01111", "10000", "10000", "01110", "00001", "00001", "11110"], t: ["11111", "00100", "00100", "00100", "00100", "00100", "00100"], u: ["10001", "10001", "10001", "10001", "10001", "10001", "01110"], v: ["10001", "10001", "10001", "10001", "10001", "01010", "00100"], w: ["10001", "10001", "10001", "10101", "10101", "10101", "01010"], x: ["10001", "10001", "01010", "00100", "01010", "10001", "10001"], y: ["10001", "10001", "01010", "00100", "00100", "00100", "00100"], z: ["11111", "00001", "00010", "00100", "01000", "10000", "11111"], 0: ["01110", "10001", "10011", "10101", "11001", "10001", "01110"], 1: ["00100", "01100", "00100", "00100", "00100", "00100", "01110"], 2: ["01110", "10001", "00001", "00010", "00100", "01000", "11111"], 3: ["11110", "00001", "00001", "01110", "00001", "00001", "11110"], 4: ["10010", "10010", "10010", "11111", "00010", "00010", "00010"], 5: ["11111", "10000", "10000", "11110", "00001", "00001", "11110"], 6: ["01111", "10000", "10000", "11110", "10001", "10001", "01110"], 7: ["11111", "00001", "00010", "00100", "01000", "01000", "01000"], 8: ["01110", "10001", "10001", "01110", "10001", "10001", "01110"], 9: ["01110", "10001", "10001", "01111", "00001", "00001", "11110"], "-": ["00000", "00000", "00000", "11111", "00000", "00000", "00000"], _: ["00000", "00000", "00000", "00000", "00000", "00000", "11111"] }; const mimeTypes = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".json": "application/json; charset=utf-8", ".svg": "image/svg+xml" }; function sendJson(res, status, body) { res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" }); res.end(JSON.stringify(body)); } function getRoom(roomId) { if (!rooms.has(roomId)) { rooms.set(roomId, new Map()); } return rooms.get(roomId); } function normalizeName(name) { const normalized = String(name || "").trim().replace(/\s+/g, " "); return normalized.slice(0, 48) || "Guest"; } function normalizeAnnouncement(text) { return String(text || "").trim().replace(/\s+/g, " ").slice(0, 240); } function clampNumber(value, fallback, min, max) { const parsed = Number(value); if (!Number.isFinite(parsed)) return fallback; return Math.max(min, Math.min(max, parsed)); } function normalizeVoiceSettings(settings = {}) { const source = settings && typeof settings === "object" ? settings : {}; return { amplitude: clampNumber(source.amplitude, 100, 0, 200), pitch: clampNumber(source.pitch, 32, 0, 99), speed: clampNumber(source.speed, 135, 80, 240), wordgap: clampNumber(source.wordgap, 4, 0, 20) }; } function normalizeClientId(value) { const normalized = String(value || "").trim(); return /^[a-z0-9_-]{16,80}$/i.test(normalized) ? normalized : ""; } function captchaAccepted(value) { return String(value || "").toLowerCase() === captchaPassphrase.toLowerCase(); } function captchaSvg() { const cell = 4; const gap = 4; const padding = 10; const chars = [...captchaPassphrase.toLowerCase()]; const width = Math.max(160, padding * 2 + chars.length * (5 * cell + gap) - gap); const height = 42; const rects = []; chars.forEach((char, index) => { const glyph = captchaGlyphs[char] || captchaGlyphs._; const xOffset = padding + index * (5 * cell + gap); glyph.forEach((row, y) => { [...row].forEach((pixel, x) => { if (pixel === "1") { rects.push(``); } }); }); }); return ` ${rects.join("")} `; } function normalizePlaylist(playlist) { if (!Array.isArray(playlist)) return []; const seen = new Set(); return playlist .map((item) => ({ videoId: String(item?.videoId || "").trim(), title: String(item?.title || "").trim(), position: Math.max(0, Number(item?.position) || 0), duration: Math.max(0, Number(item?.duration) || 0) })) .filter((item) => { if (!/^[a-zA-Z0-9_-]{11}$/.test(item.videoId) || seen.has(item.videoId)) { return false; } seen.add(item.videoId); item.title ||= `YouTube video ${item.videoId}`; return true; }) .slice(0, 50); } function broadcast(roomId, senderId, event) { const room = rooms.get(roomId); if (!room) return; for (const [peerId, peer] of room) { if (peerId === senderId) continue; peer.res.write(`data: ${JSON.stringify(event)}\n\n`); } } function removePeer(roomId, peerId) { const room = rooms.get(roomId); if (!room) return; const peer = room.get(peerId); const name = peer?.name || "A participant"; room.delete(peerId); broadcast(roomId, peerId, { type: "peer-left", peerId, name }); if (presentationStates.get(roomId)?.peerId === peerId) { presentationStates.delete(roomId); broadcast(roomId, peerId, { type: "presentation-state", from: peerId, name, sharing: false }); } if (room.size === 0) { rooms.delete(roomId); watchStates.delete(roomId); presentationStates.delete(roomId); } } async function parseJson(req) { const chunks = []; for await (const chunk of req) chunks.push(chunk); const raw = Buffer.concat(chunks).toString("utf8"); return raw ? JSON.parse(raw) : {}; } async function serveStatic(req, res) { const url = new URL(req.url, `http://${req.headers.host}`); const requestedPath = url.pathname === "/" ? "/index.html" : url.pathname; const safePath = normalize(decodeURIComponent(requestedPath)).replace(/^(\.\.[/\\])+/, ""); const filePath = join(publicDir, safePath); if (!filePath.startsWith(publicDir)) { res.writeHead(403); res.end("Forbidden"); return; } try { const data = await readFile(filePath); res.writeHead(200, { "content-type": mimeTypes[extname(filePath)] || "application/octet-stream", "cache-control": "no-store" }); res.end(req.method === "HEAD" ? undefined : data); } catch { res.writeHead(404); res.end("Not found"); } } const server = http.createServer(async (req, res) => { const url = new URL(req.url, `http://${req.headers.host}`); try { if (req.method === "GET" && url.pathname.startsWith("/events/")) { if (!captchaAccepted(url.searchParams.get("captcha"))) { sendJson(res, 403, { error: "Captcha passphrase is required." }); return; } const roomId = url.pathname.split("/").at(-1) || defaultRoomId; const displayName = normalizeName(url.searchParams.get("name")); const clientId = normalizeClientId(url.searchParams.get("clientId")); const peerId = randomUUID(); const room = getRoom(roomId); const existingPeers = [...room].map(([id, peer]) => ({ id, name: peer.name, clientId: peer.clientId })); res.writeHead(200, { "content-type": "text/event-stream; charset=utf-8", "cache-control": "no-store, no-transform", connection: "keep-alive", "x-accel-buffering": "no" }); room.set(peerId, { res, name: displayName, clientId }); res.write( `data: ${JSON.stringify({ type: "welcome", peerId, name: displayName, clientId, peers: existingPeers, watchState: watchStates.get(roomId) || null, presentationState: presentationStates.get(roomId) || null })}\n\n` ); broadcast(roomId, peerId, { type: "peer-joined", peerId, name: displayName, clientId }); const heartbeat = setInterval(() => { res.write(": heartbeat\n\n"); }, 15000); req.on("close", () => { clearInterval(heartbeat); removePeer(roomId, peerId); }); return; } if ((req.method === "GET" || req.method === "HEAD") && url.pathname === "/captcha.svg") { res.writeHead(200, { "content-type": "image/svg+xml; charset=utf-8", "cache-control": "no-store" }); res.end(req.method === "HEAD" ? undefined : captchaSvg()); return; } if (req.method === "GET" && url.pathname === "/captcha/verify") { sendJson(res, 200, { ok: captchaAccepted(url.searchParams.get("answer")) }); return; } if (req.method === "POST" && url.pathname.startsWith("/room-event/")) { const roomId = url.pathname.split("/").at(-1) || defaultRoomId; const payload = await parseJson(req); if (!payload.from || !["presentation-state", "voice-announcement"].includes(payload.type)) { sendJson(res, 400, { error: "Room events require from and a supported type." }); return; } if (payload.type === "voice-announcement") { const text = normalizeAnnouncement(payload.text); if (!text) { sendJson(res, 400, { error: "Voice announcements require text." }); return; } const event = { type: "voice-announcement", from: payload.from, name: normalizeName(payload.name), text, voice: normalizeVoiceSettings(payload.voice), updatedAt: Date.now() }; broadcast(roomId, payload.from, event); sendJson(res, 202, { ok: true }); return; } const event = { type: "presentation-state", from: payload.from, name: normalizeName(payload.name), sharing: Boolean(payload.sharing), updatedAt: Date.now() }; if (event.sharing) { const previous = presentationStates.get(roomId); if (previous?.peerId && previous.peerId !== payload.from) { broadcast(roomId, payload.from, { type: "presentation-state", from: previous.peerId, peerId: previous.peerId, name: previous.name, sharing: false, reason: "takeover", replacedBy: payload.from, replacedByName: event.name, updatedAt: Date.now() }); } presentationStates.set(roomId, { ...event, peerId: payload.from }); } else if (presentationStates.get(roomId)?.peerId === payload.from) { presentationStates.delete(roomId); } broadcast(roomId, payload.from, event); sendJson(res, 202, { ok: true }); return; } if (req.method === "POST" && url.pathname.startsWith("/signal/")) { const roomId = url.pathname.split("/").at(-1); const payload = await parseJson(req); if (!payload.from || !payload.to || !payload.type) { sendJson(res, 400, { error: "Signal messages require from, to, and type." }); return; } const room = rooms.get(roomId); const target = room?.get(payload.to); if (target) { target.res.write(`data: ${JSON.stringify(payload)}\n\n`); } sendJson(res, 202, { ok: true }); return; } if (req.method === "POST" && url.pathname.startsWith("/watch/")) { const roomId = url.pathname.split("/").at(-1) || defaultRoomId; const payload = await parseJson(req); if (!payload.from || payload.type !== "watch-state") { sendJson(res, 400, { error: "Watch messages require from and type=watch-state." }); return; } const playlist = normalizePlaylist(payload.playlist); const videoId = String(payload.videoId || "").trim(); const activeVideoId = playlist.some((item) => item.videoId === videoId) ? videoId : ""; const action = payload.action === "play" && activeVideoId ? "play" : "pause"; const watchState = { type: "watch-state", from: payload.from, name: normalizeName(payload.name), videoId: activeVideoId, playlist, position: Number(payload.position) || 0, playing: action === "play", action, updatedAt: Number(payload.updatedAt) || Date.now() }; watchStates.set(roomId, watchState); broadcast(roomId, payload.from, watchState); sendJson(res, 202, { ok: true }); return; } if (req.method === "GET" || req.method === "HEAD") { await serveStatic(req, res); return; } res.writeHead(405); res.end("Method not allowed"); } catch (error) { sendJson(res, 500, { error: error.message }); } }); server.listen(port, host, () => { console.log(`kkkmeet is running at http://${host}:${port}`); });