UNPKG

peermsg

Version:

Lightweight peer-to-peer LAN messenger CLI with UDP broadcast/multicast, AES encryption, and full-screen TUI mode.

145 lines (131 loc) 3.96 kB
import { createSocket, sendJSON } from "./udp.js"; import { HELLO_INTERVAL_MS, PEER_TTL_MS } from "./constants.js"; import { uuid, now, logLine } from "./utils.js"; import { decryptStr } from "./crypto.js"; /** * startSession({ room, name, key, transport }) * transport: { mode: "bc"|"mc", mcAddr?, mcIface?, ttl? } */ export function startSession({ room, name, key, transport = {}, interactiveStdin = true, handlers = {}, }) { const id = uuid(); const peers = new Map(); // id -> { name, last, ip } const sock = createSocket({ bind: true, ...transport }); const announce = () => sendJSON(sock, { type: "hello", room, id, name, ts: now() }, null); const who = () => sendJSON(sock, { type: "who", room, id, name, ts: now() }, null); const emitPeers = () => { if (handlers.onPeers) { const arr = Array.from(peers.values()).map((p) => ({ name: p.name, ip: p.ip, })); handlers.onPeers(arr); } }; sock.on("message", (msg, rinfo) => { let data; try { data = JSON.parse(msg.toString("utf8")); } catch { return; } if (!data) return; if (data.type === "enc" && key) { const pt = decryptStr(data, key); if (!pt) return; try { data = JSON.parse(pt); } catch { return; } } if (!data.room || data.room !== room) return; if (data.id === id) return; switch (data.type) { case "hello": peers.set(data.id, { name: data.name || "anon", last: now(), ip: rinfo.address, }); emitPeers(); break; case "who": announce(); break; case "chat": { const from = data.name || "anon"; const text = String(data.text ?? ""); peers.set(data.id, { name: from, last: now(), ip: rinfo.address }); if (handlers.onChat) handlers.onChat({ from, text, ts: data.ts || now() }); else logLine(`[${from}@${room}] ${text}`); emitPeers(); break; } default: break; } }); const helloTimer = setInterval(announce, HELLO_INTERVAL_MS); const gc = setInterval(() => { const t = now(); let changed = false; for (const [pid, p] of peers) if (t - p.last > PEER_TTL_MS) { peers.delete(pid); changed = true; } if (changed) emitPeers(); }, 5_000); announce(); who(); // Optional stdin handler (non-TUI mode) if (interactiveStdin) { process.stdin.setEncoding("utf8"); process.stdin.resume(); process.stdin.on("data", (chunk) => { const lines = chunk.split(/\r?\n/).filter(Boolean); for (const text of lines) { send(text); logLine(`[me@${room}] ${text}`); if (process.stdin.isTTY) process.stdout.write("> "); } }); if (process.stdin.isTTY) process.stdout.write("> "); } function send(text) { sendJSON( sock, { type: "chat", room, id, name, ts: now(), text }, key || null, ); } const stop = () => { clearInterval(helloTimer); clearInterval(gc); try { sock.close(); } catch { } }; // expose helpers + assignable handlers (TUI can set these after creation) return { stop, send, peers, id, name, room, sock, onChat: null, onPeers: null, }; }