#!/usr/bin/env node /** * Jackbox Player Count Fetcher * * This script connects to a Jackbox game room and retrieves the actual player count * by establishing a WebSocket connection and listening for game state updates. * * Usage: * node get-jackbox-player-count.js * node get-jackbox-player-count.js JYET */ const https = require('https'); // Try to load ws from multiple locations let WebSocket; try { WebSocket = require('ws'); } catch (e) { try { WebSocket = require('../backend/node_modules/ws'); } catch (e2) { console.error('Error: WebSocket library (ws) not found.'); console.error('Please run: npm install ws'); console.error('Or run this script from the backend directory where ws is already installed.'); process.exit(1); } } // ANSI color codes for pretty output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', red: '\x1b[31m', cyan: '\x1b[36m' }; /** * Fetches room information from the Jackbox REST API */ async function getRoomInfo(roomCode) { return new Promise((resolve, reject) => { const url = `https://ecast.jackboxgames.com/api/v2/rooms/${roomCode}`; https.get(url, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const json = JSON.parse(data); if (json.ok) { resolve(json.body); } else { reject(new Error('Room not found or invalid')); } } catch (e) { reject(e); } }); }).on('error', reject); }); } /** * Connects to the Jackbox WebSocket and retrieves player count * Note: Direct WebSocket connection requires proper authentication flow. * This uses the ecast endpoint which is designed for external connections. */ async function getPlayerCount(roomCode, roomInfo) { return new Promise((resolve, reject) => { // Use the audienceHost (ecast) instead of direct game host const wsUrl = `wss://${roomInfo.audienceHost}/api/v2/audience/${roomCode}/play`; console.log(`${colors.blue}Connecting to WebSocket...${colors.reset}`); console.log(`${colors.cyan}URL: ${wsUrl}${colors.reset}\n`); const ws = new WebSocket(wsUrl, { headers: { 'Origin': 'https://jackbox.tv', 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36' } }); let timeout = setTimeout(() => { ws.close(); reject(new Error('Connection timeout - room may be closed or unreachable')); }, 15000); // 15 second timeout let receivedAnyData = false; ws.on('open', () => { console.log(`${colors.green}✓ WebSocket connected${colors.reset}\n`); // For audience endpoint, we might not need to send join message // Just listen for messages }); ws.on('message', (data) => { receivedAnyData = true; try { const message = JSON.parse(data.toString()); console.log(`${colors.yellow}Received message:${colors.reset}`, message.opcode || 'unknown'); // Look for various message types that might contain player info if (message.opcode === 'client/welcome' && message.result) { clearTimeout(timeout); const here = message.result.here || {}; const playerCount = Object.keys(here).length; const audienceCount = message.result.entities?.audience?.[1]?.count || 0; const lobbyState = message.result.entities?.['bc:room']?.[1]?.val?.lobbyState || 'Unknown'; const gameState = message.result.entities?.['bc:room']?.[1]?.val?.state || 'Unknown'; // Extract player details const players = []; for (const [id, playerData] of Object.entries(here)) { const roles = playerData.roles || {}; if (roles.host) { players.push({ id, role: 'host', name: 'Host' }); } else if (roles.player) { players.push({ id, role: 'player', name: roles.player.name || 'Unknown' }); } } const result = { roomCode, appTag: roomInfo.appTag, playerCount, audienceCount, maxPlayers: roomInfo.maxPlayers, gameState, lobbyState, locked: roomInfo.locked, full: roomInfo.full, players }; ws.close(); resolve(result); } else if (message.opcode === 'room/count' || message.opcode === 'audience/count-group') { // Audience count updates console.log(`${colors.cyan}Audience count message received${colors.reset}`); } } catch (e) { // Ignore parse errors, might be non-JSON messages console.log(`${colors.yellow}Parse error:${colors.reset}`, e.message); } }); ws.on('error', (error) => { clearTimeout(timeout); reject(new Error(`WebSocket error: ${error.message}\n\n` + `${colors.yellow}Note:${colors.reset} Direct WebSocket access requires joining through jackbox.tv.\n` + `This limitation means we cannot directly query player count without joining the game.`)); }); ws.on('close', () => { clearTimeout(timeout); if (!receivedAnyData) { reject(new Error('WebSocket closed without receiving data.\n\n' + `${colors.yellow}Note:${colors.reset} The Jackbox WebSocket API requires authentication that's only\n` + `available when joining through the official jackbox.tv interface.\n\n` + `${colors.cyan}Alternative:${colors.reset} Use the REST API to check if room is full, or join\n` + `through jackbox.tv in a browser to get real-time player counts.`)); } }); }); } /** * Pretty prints the results */ function printResults(result) { console.log(`${colors.bright}═══════════════════════════════════════════${colors.reset}`); console.log(`${colors.bright} Jackbox Room Status${colors.reset}`); console.log(`${colors.bright}═══════════════════════════════════════════${colors.reset}\n`); console.log(`${colors.cyan}Room Code:${colors.reset} ${result.roomCode}`); console.log(`${colors.cyan}Game:${colors.reset} ${result.appTag}`); console.log(`${colors.cyan}Game State:${colors.reset} ${result.gameState}`); console.log(`${colors.cyan}Lobby State:${colors.reset} ${result.lobbyState}`); console.log(`${colors.cyan}Locked:${colors.reset} ${result.locked ? 'Yes' : 'No'}`); console.log(`${colors.cyan}Full:${colors.reset} ${result.full ? 'Yes' : 'No'}`); console.log(); console.log(`${colors.bright}${colors.green}Players:${colors.reset} ${colors.bright}${result.playerCount}${colors.reset} / ${result.maxPlayers}`); console.log(`${colors.cyan}Audience:${colors.reset} ${result.audienceCount}`); console.log(); if (result.players.length > 0) { console.log(`${colors.bright}Current Players:${colors.reset}`); result.players.forEach((player, idx) => { const roleColor = player.role === 'host' ? colors.yellow : colors.green; console.log(` ${idx + 1}. ${roleColor}${player.name}${colors.reset} (${player.role})`); }); } console.log(`\n${colors.bright}═══════════════════════════════════════════${colors.reset}\n`); } /** * Main function */ async function main() { const args = process.argv.slice(2); if (args.length === 0) { console.error(`${colors.red}Error: Room code required${colors.reset}`); console.log(`\nUsage: node get-jackbox-player-count.js `); console.log(`Example: node get-jackbox-player-count.js JYET\n`); process.exit(1); } const roomCode = args[0].toUpperCase(); console.log(`${colors.bright}Jackbox Player Count Fetcher${colors.reset}`); console.log(`${colors.cyan}Room Code: ${roomCode}${colors.reset}\n`); try { // Step 1: Get room info from REST API console.log(`${colors.blue}Fetching room information...${colors.reset}`); const roomInfo = await getRoomInfo(roomCode); console.log(`${colors.green}✓ Room found: ${roomInfo.appTag}${colors.reset}`); console.log(`${colors.cyan} Max Players: ${roomInfo.maxPlayers}${colors.reset}\n`); // Step 2: Connect to WebSocket and get player count const result = await getPlayerCount(roomCode, roomInfo); // Step 3: Print results printResults(result); // Return just the player count for scripting purposes if (process.env.JSON_OUTPUT === 'true') { console.log(JSON.stringify(result, null, 2)); } process.exit(0); } catch (error) { console.error(`${colors.red}Error: ${error.message}${colors.reset}\n`); process.exit(1); } } // Run if executed directly if (require.main === module) { main(); } // Export for use as a module module.exports = { getRoomInfo, getPlayerCount };