#!/usr/bin/env node const WebSocket = require('ws'); const https = require('https'); const ROOM_CODE = process.argv[2] || 'LSBN'; const PLAYER_NAME = process.argv[3] || 'PROBE_WS'; const ROLE = process.argv[4] || 'player'; // 'player' or 'audience' const USER_ID = `probe-${Date.now()}`; function getRoomInfo(code) { return new Promise((resolve, reject) => { https.get(`https://ecast.jackboxgames.com/api/v2/rooms/${code}`, res => { let data = ''; res.on('data', c => data += c); res.on('end', () => { try { const json = JSON.parse(data); if (json.ok) resolve(json.body); else reject(new Error(json.error || 'Room not found')); } catch (e) { reject(e); } }); }).on('error', reject); }); } function ts() { return new Date().toISOString().slice(11, 23); } async function main() { console.log(`[${ts()}] Fetching room info for ${ROOM_CODE}...`); const room = await getRoomInfo(ROOM_CODE); console.log(`[${ts()}] Room: ${room.appTag}, host: ${room.host}, locked: ${room.locked}`); let wsUrl; if (ROLE === 'audience') { wsUrl = `wss://${room.audienceHost}/api/v2/audience/${ROOM_CODE}/play`; } else { wsUrl = `wss://${room.host}/api/v2/rooms/${ROOM_CODE}/play?role=${ROLE}&name=${encodeURIComponent(PLAYER_NAME)}&userId=${USER_ID}&format=json`; } console.log(`[${ts()}] Connecting: ${wsUrl}`); const ws = new WebSocket(wsUrl, ['ecast-v0'], { headers: { 'Origin': 'https://jackbox.tv', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' } }); let msgCount = 0; ws.on('open', () => { console.log(`[${ts()}] CONNECTED`); }); ws.on('message', (raw) => { msgCount++; try { const msg = JSON.parse(raw.toString()); const summary = summarize(msg); console.log(`[${ts()}] RECV #${msgCount} | pc:${msg.pc} | opcode:${msg.opcode} | ${summary}`); if (process.env.VERBOSE === 'true') { console.log(JSON.stringify(msg, null, 2)); } } catch (e) { console.log(`[${ts()}] RECV #${msgCount} | raw: ${raw.toString().slice(0, 200)}`); } }); ws.on('close', (code, reason) => { console.log(`[${ts()}] CLOSED code=${code} reason=${reason}`); process.exit(0); }); ws.on('error', (err) => { console.error(`[${ts()}] ERROR: ${err.message}`); }); process.on('SIGINT', () => { console.log(`\n[${ts()}] Closing (${msgCount} messages received)`); ws.close(); }); } function summarize(msg) { if (msg.opcode === 'client/welcome') { const r = msg.result || {}; const hereIds = r.here ? Object.keys(r.here) : []; const entityKeys = r.entities ? Object.keys(r.entities) : []; return `id=${r.id} name=${r.name} reconnect=${r.reconnect} here=[${hereIds}] entities=[${entityKeys}]`; } if (msg.opcode === 'object') { const r = msg.result || {}; const valKeys = r.val ? Object.keys(r.val).slice(0, 5).join(',') : 'null'; return `key=${r.key} v${r.version} from=${r.from} val=[${valKeys}...]`; } if (msg.opcode === 'client/connected') { const r = msg.result || {}; return `id=${r.id} userId=${r.userId} name=${r.name} role=${r.role}`; } if (msg.opcode === 'client/disconnected') { const r = msg.result || {}; return `id=${r.id} role=${r.role}`; } return JSON.stringify(msg.result || msg).slice(0, 150); } main().catch(e => { console.error(e); process.exit(1); });