263 lines
9.1 KiB
JavaScript
263 lines
9.1 KiB
JavaScript
#!/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
|
|
};
|
|
|