191 lines
6.3 KiB
JavaScript
Executable File
191 lines
6.3 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
const puppeteer = require('puppeteer');
|
|
|
|
async function checkRoomStatus(roomCode) {
|
|
try {
|
|
const response = await fetch(`https://ecast.jackboxgames.com/api/v2/rooms/${roomCode}`);
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
const roomData = data.body || data;
|
|
|
|
if (process.env.DEBUG) {
|
|
console.error('[API] Room data:', JSON.stringify(roomData, null, 2));
|
|
}
|
|
return {
|
|
exists: true,
|
|
locked: roomData.locked || false,
|
|
full: roomData.full || false,
|
|
maxPlayers: roomData.maxPlayers || 8,
|
|
minPlayers: roomData.minPlayers || 0
|
|
};
|
|
}
|
|
return { exists: false };
|
|
} catch (e) {
|
|
if (process.env.DEBUG) {
|
|
console.error('[API] Error checking room:', e.message);
|
|
}
|
|
return { exists: false };
|
|
}
|
|
}
|
|
|
|
async function getPlayerCountFromAudience(roomCode) {
|
|
const browser = await puppeteer.launch({
|
|
headless: 'new',
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
});
|
|
|
|
const page = await browser.newPage();
|
|
await page.setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
|
|
|
|
let playerCount = null;
|
|
|
|
// Enable CDP and listen for WebSocket frames BEFORE navigating
|
|
const client = await page.target().createCDPSession();
|
|
await client.send('Network.enable');
|
|
|
|
client.on('Network.webSocketFrameReceived', ({ response }) => {
|
|
if (response.payloadData && playerCount === null) {
|
|
try {
|
|
const data = JSON.parse(response.payloadData);
|
|
|
|
// Check for bc:room with player count data
|
|
let roomVal = null;
|
|
|
|
if (data.opcode === 'client/welcome' && data.result?.entities?.['bc:room']) {
|
|
roomVal = data.result.entities['bc:room'][1]?.val;
|
|
}
|
|
|
|
if (data.opcode === 'object' && data.result?.key === 'bc:room') {
|
|
roomVal = data.result.val;
|
|
}
|
|
|
|
if (roomVal) {
|
|
// Strategy 1: Game ended - use gameResults
|
|
if (roomVal.gameResults?.players) {
|
|
playerCount = roomVal.gameResults.players.length;
|
|
if (process.env.DEBUG) {
|
|
console.error('[SUCCESS] Found', playerCount, 'players from gameResults');
|
|
}
|
|
}
|
|
|
|
// Strategy 2: Game in progress - use analytics
|
|
if (playerCount === null && roomVal.analytics) {
|
|
const startAnalytic = roomVal.analytics.find(a => a.action === 'start');
|
|
if (startAnalytic?.value) {
|
|
playerCount = startAnalytic.value;
|
|
if (process.env.DEBUG) {
|
|
console.error('[SUCCESS] Found', playerCount, 'players from analytics');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Ignore parse errors
|
|
}
|
|
}
|
|
});
|
|
|
|
try {
|
|
if (process.env.DEBUG) console.error('[2] Navigating to jackbox.tv...');
|
|
await page.goto('https://jackbox.tv/', { waitUntil: 'networkidle2', timeout: 30000 });
|
|
|
|
if (process.env.DEBUG) console.error('[3] Waiting for form...');
|
|
await page.waitForSelector('input#roomcode', { timeout: 10000 });
|
|
|
|
await page.evaluate(() => {
|
|
localStorage.clear();
|
|
sessionStorage.clear();
|
|
});
|
|
|
|
if (process.env.DEBUG) console.error('[4] Typing room code:', roomCode);
|
|
const roomInput = await page.$('input#roomcode');
|
|
await roomInput.type(roomCode.toUpperCase(), { delay: 50 });
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
if (process.env.DEBUG) console.error('[5] Typing name...');
|
|
const nameInput = await page.$('input#username');
|
|
await nameInput.type('CountBot', { delay: 30 });
|
|
|
|
if (process.env.DEBUG) console.error('[6] Waiting for JOIN AUDIENCE button...');
|
|
await page.waitForFunction(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button'));
|
|
return buttons.some(b => b.textContent.toUpperCase().includes('JOIN AUDIENCE') && !b.disabled);
|
|
}, { timeout: 10000 });
|
|
|
|
if (process.env.DEBUG) console.error('[7] Clicking JOIN AUDIENCE...');
|
|
await page.evaluate(() => {
|
|
const buttons = Array.from(document.querySelectorAll('button'));
|
|
const btn = buttons.find(b => b.textContent.toUpperCase().includes('JOIN AUDIENCE') && !b.disabled);
|
|
if (btn) btn.click();
|
|
});
|
|
|
|
// Wait for WebSocket messages
|
|
if (process.env.DEBUG) console.error('[8] Waiting for player count...');
|
|
for (let i = 0; i < 20 && playerCount === null; i++) {
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
}
|
|
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
return playerCount;
|
|
}
|
|
|
|
async function getPlayerCount(roomCode) {
|
|
if (process.env.DEBUG) console.error('[1] Checking room status via API...');
|
|
const roomStatus = await checkRoomStatus(roomCode);
|
|
|
|
if (!roomStatus.exists) {
|
|
if (process.env.DEBUG) console.error('[ERROR] Room does not exist');
|
|
return 0;
|
|
}
|
|
|
|
// If full, return maxPlayers
|
|
if (roomStatus.full) {
|
|
if (process.env.DEBUG) console.error('[1] Room is FULL - returning maxPlayers:', roomStatus.maxPlayers);
|
|
return roomStatus.maxPlayers;
|
|
}
|
|
|
|
// If locked (game in progress), join as audience to get count
|
|
if (roomStatus.locked) {
|
|
if (process.env.DEBUG) console.error('[1] Room is LOCKED - joining as audience...');
|
|
|
|
try {
|
|
const count = await getPlayerCountFromAudience(roomCode);
|
|
if (count !== null) {
|
|
return count;
|
|
}
|
|
} catch (e) {
|
|
if (process.env.DEBUG) console.error('[ERROR] Failed to get count:', e.message);
|
|
}
|
|
|
|
// Fallback to maxPlayers if we couldn't get exact count
|
|
if (process.env.DEBUG) console.error('[1] Could not get exact count, returning maxPlayers:', roomStatus.maxPlayers);
|
|
return roomStatus.maxPlayers;
|
|
}
|
|
|
|
// Not locked (lobby open) - don't join, return minPlayers
|
|
if (process.env.DEBUG) console.error('[1] Room is NOT locked (lobby) - returning minPlayers:', roomStatus.minPlayers);
|
|
return roomStatus.minPlayers;
|
|
}
|
|
|
|
// Main
|
|
const roomCode = process.argv[2];
|
|
if (!roomCode) {
|
|
console.error('Usage: node jackbox-count.js <ROOM_CODE>');
|
|
process.exit(1);
|
|
}
|
|
|
|
getPlayerCount(roomCode.toUpperCase())
|
|
.then(count => {
|
|
console.log(count);
|
|
process.exit(0);
|
|
})
|
|
.catch(err => {
|
|
if (process.env.DEBUG) console.error('Error:', err.message);
|
|
console.log(0);
|
|
process.exit(0);
|
|
});
|