Files
jackboxpartypack-gamepicker/scripts/get-jackbox-player-count.js

263 lines
9.1 KiB
JavaScript
Raw Normal View History

#!/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 <ROOM_CODE>
* 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 <ROOM_CODE>`);
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
};