docs: comprehensive Jackbox ecast API reverse engineering
Adds complete documentation of the ecast platform covering: - REST API (8 endpoints including newly discovered /connections, /info, /status) - WebSocket protocol (connection, message format, 40+ opcodes) - Entity model (room, player, audience, textDescriptions) - Game lifecycle (lobby → start → gameplay → end) - Player/room management answers (counting, join/leave detection, etc.) Also adds scripts/ws-probe.js utility for direct WebSocket probing. Made-with: Cursor
This commit is contained in:
109
scripts/ws-probe.js
Normal file
109
scripts/ws-probe.js
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/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); });
|
||||
Reference in New Issue
Block a user