chore: remove Puppeteer and old room-monitor/player-count-checker modules
Made-with: Cursor
This commit is contained in:
974
backend/package-lock.json
generated
974
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,6 @@
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"puppeteer": "^24.0.0",
|
||||
"ws": "^8.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,396 +0,0 @@
|
||||
const puppeteer = require('puppeteer');
|
||||
const db = require('../database');
|
||||
const { getWebSocketManager } = require('./websocket-manager');
|
||||
const { checkRoomStatus } = require('./jackbox-api');
|
||||
|
||||
// Store active check jobs
|
||||
const activeChecks = new Map();
|
||||
|
||||
/**
|
||||
* Watch a game from start to finish as audience member
|
||||
* Collects analytics throughout the entire game lifecycle
|
||||
*/
|
||||
async function watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers) {
|
||||
let browser;
|
||||
const checkKey = `${sessionId}-${gameId}`;
|
||||
|
||||
try {
|
||||
console.log(`[Player Count] Opening audience connection for ${checkKey} (max: ${maxPlayers})`);
|
||||
|
||||
browser = await puppeteer.launch({
|
||||
headless: 'new',
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-accelerated-2d-canvas',
|
||||
'--no-first-run',
|
||||
'--no-zygote',
|
||||
'--disable-gpu'
|
||||
]
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
// Track all player counts we've seen
|
||||
const seenPlayerCounts = new Set();
|
||||
let bestPlayerCount = null;
|
||||
let startPlayerCount = null; // Authoritative count from 'start' action
|
||||
let gameEnded = false;
|
||||
let audienceJoined = false;
|
||||
let frameCount = 0;
|
||||
|
||||
// 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 && !gameEnded) {
|
||||
frameCount++;
|
||||
try {
|
||||
const data = JSON.parse(response.payloadData);
|
||||
|
||||
if (process.env.DEBUG && frameCount % 10 === 0) {
|
||||
console.log(`[Frame ${frameCount}] opcode: ${data.opcode}`);
|
||||
}
|
||||
|
||||
// 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 (process.env.DEBUG) {
|
||||
console.log(`[Frame ${frameCount}] Found bc:room in client/welcome`);
|
||||
}
|
||||
|
||||
// First client/welcome means Jackbox accepted our audience join
|
||||
if (!audienceJoined) {
|
||||
audienceJoined = true;
|
||||
console.log(`[Audience] Successfully joined room ${roomCode} as audience`);
|
||||
|
||||
const wsManager = getWebSocketManager();
|
||||
if (wsManager) {
|
||||
wsManager.broadcastEvent('audience.joined', {
|
||||
sessionId,
|
||||
gameId,
|
||||
roomCode
|
||||
}, parseInt(sessionId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.opcode === 'object' && data.result?.key === 'bc:room') {
|
||||
roomVal = data.result.val;
|
||||
}
|
||||
|
||||
if (roomVal) {
|
||||
// Check if game has ended
|
||||
if (roomVal.gameResults?.players) {
|
||||
const finalCount = roomVal.gameResults.players.length;
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[Frame ${frameCount}] 🎉 GAME ENDED - Final count: ${finalCount} players`);
|
||||
|
||||
if (startPlayerCount !== null && startPlayerCount !== finalCount) {
|
||||
console.log(`[Frame ${frameCount}] ⚠️ WARNING: Start count (${startPlayerCount}) != Final count (${finalCount})`);
|
||||
} else if (startPlayerCount !== null) {
|
||||
console.log(`[Frame ${frameCount}] ✓ Verified: Start count matches final count (${finalCount})`);
|
||||
}
|
||||
}
|
||||
bestPlayerCount = finalCount;
|
||||
gameEnded = true;
|
||||
updatePlayerCount(sessionId, gameId, finalCount, 'completed');
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract player counts from analytics (game in progress)
|
||||
if (roomVal.analytics && Array.isArray(roomVal.analytics)) {
|
||||
for (const analytic of roomVal.analytics) {
|
||||
if (analytic.action === 'start' && analytic.value && typeof analytic.value === 'number') {
|
||||
if (startPlayerCount === null) {
|
||||
startPlayerCount = analytic.value;
|
||||
bestPlayerCount = analytic.value;
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[Frame ${frameCount}] 🎯 Found 'start' action: ${analytic.value} players (authoritative)`);
|
||||
}
|
||||
updatePlayerCount(sessionId, gameId, startPlayerCount, 'checking');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (startPlayerCount !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (analytic.value && typeof analytic.value === 'number' && analytic.value > 0 && analytic.value <= 100) {
|
||||
seenPlayerCounts.add(analytic.value);
|
||||
|
||||
const clampedValue = Math.min(analytic.value, maxPlayers);
|
||||
|
||||
if (bestPlayerCount === null || clampedValue > bestPlayerCount) {
|
||||
bestPlayerCount = clampedValue;
|
||||
if (process.env.DEBUG) {
|
||||
if (analytic.value > maxPlayers) {
|
||||
console.log(`[Frame ${frameCount}] 📊 Found player count ${analytic.value} in action '${analytic.action}' (clamped to ${clampedValue})`);
|
||||
} else {
|
||||
console.log(`[Frame ${frameCount}] 📊 Found player count ${analytic.value} in action '${analytic.action}' (best so far)`);
|
||||
}
|
||||
}
|
||||
updatePlayerCount(sessionId, gameId, bestPlayerCount, 'checking');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if room is no longer locked (game ended another way)
|
||||
if (roomVal.locked === false && bestPlayerCount !== null) {
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[Frame ${frameCount}] Room unlocked, game likely ended. Final count: ${bestPlayerCount}`);
|
||||
}
|
||||
gameEnded = true;
|
||||
updatePlayerCount(sessionId, gameId, bestPlayerCount, 'completed');
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (process.env.DEBUG && frameCount % 50 === 0) {
|
||||
console.log(`[Frame ${frameCount}] Parse error:`, e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Navigate and join audience
|
||||
if (process.env.DEBUG) console.log('[Audience] Navigating to jackbox.tv...');
|
||||
await page.goto('https://jackbox.tv/', { waitUntil: 'networkidle2', timeout: 30000 });
|
||||
|
||||
if (process.env.DEBUG) console.log('[Audience] Waiting for form...');
|
||||
await page.waitForSelector('input#roomcode', { timeout: 10000 });
|
||||
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) console.log('[Audience] 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.log('[Audience] Typing name...');
|
||||
const nameInput = await page.$('input#username');
|
||||
await nameInput.type('CountBot', { delay: 30 });
|
||||
|
||||
if (process.env.DEBUG) console.log('[Audience] 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.log('[Audience] 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();
|
||||
});
|
||||
|
||||
if (process.env.DEBUG) console.log('[Audience] 👀 Watching game... (will monitor until game ends)');
|
||||
|
||||
// Keep watching until game ends or we're told to stop
|
||||
const checkInterval = setInterval(async () => {
|
||||
const game = db.prepare(`
|
||||
SELECT status, player_count_check_status
|
||||
FROM session_games
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).get(sessionId, gameId);
|
||||
|
||||
if (!game || game.status === 'skipped' || game.status === 'played' || game.player_count_check_status === 'stopped') {
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[Audience] Stopping watch - game status changed`);
|
||||
}
|
||||
clearInterval(checkInterval);
|
||||
gameEnded = true;
|
||||
if (browser) await browser.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameEnded) {
|
||||
clearInterval(checkInterval);
|
||||
if (browser) await browser.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const roomStatus = await checkRoomStatus(roomCode);
|
||||
if (!roomStatus.exists) {
|
||||
if (process.env.DEBUG) {
|
||||
console.log(`[Audience] Room no longer exists - game ended`);
|
||||
}
|
||||
gameEnded = true;
|
||||
clearInterval(checkInterval);
|
||||
if (bestPlayerCount !== null) {
|
||||
updatePlayerCount(sessionId, gameId, bestPlayerCount, 'completed');
|
||||
} else {
|
||||
updatePlayerCount(sessionId, gameId, null, 'failed');
|
||||
}
|
||||
if (browser) await browser.close();
|
||||
return;
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
const check = activeChecks.get(checkKey);
|
||||
if (check) {
|
||||
check.watchInterval = checkInterval;
|
||||
check.browser = browser;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Audience] Error watching game:', error.message);
|
||||
if (browser) {
|
||||
await browser.close();
|
||||
}
|
||||
if (bestPlayerCount !== null) {
|
||||
updatePlayerCount(sessionId, gameId, bestPlayerCount, 'completed');
|
||||
} else {
|
||||
updatePlayerCount(sessionId, gameId, null, 'failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update player count in database and broadcast via WebSocket
|
||||
*/
|
||||
function updatePlayerCount(sessionId, gameId, playerCount, status) {
|
||||
try {
|
||||
db.prepare(`
|
||||
UPDATE session_games
|
||||
SET player_count = ?, player_count_check_status = ?
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).run(playerCount, status, sessionId, gameId);
|
||||
|
||||
const wsManager = getWebSocketManager();
|
||||
if (wsManager) {
|
||||
wsManager.broadcastEvent('player-count.updated', {
|
||||
sessionId,
|
||||
gameId,
|
||||
playerCount,
|
||||
status
|
||||
}, parseInt(sessionId));
|
||||
}
|
||||
|
||||
console.log(`[Player Count] Updated game ${gameId}: ${playerCount} players (${status})`);
|
||||
} catch (error) {
|
||||
console.error('[Player Count] Failed to update database:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start player count checking for a game.
|
||||
* Called by room-monitor once the game is confirmed started (room locked).
|
||||
* Goes straight to joining the audience — no polling needed.
|
||||
*/
|
||||
async function startPlayerCountCheck(sessionId, gameId, roomCode, maxPlayers = 8) {
|
||||
const checkKey = `${sessionId}-${gameId}`;
|
||||
|
||||
if (activeChecks.has(checkKey)) {
|
||||
console.log(`[Player Count] Already checking ${checkKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const game = db.prepare(`
|
||||
SELECT player_count_check_status
|
||||
FROM session_games
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).get(sessionId, gameId);
|
||||
|
||||
if (game && game.player_count_check_status === 'completed') {
|
||||
console.log(`[Player Count] Check already completed for ${checkKey}, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (game && game.player_count_check_status === 'failed') {
|
||||
console.log(`[Player Count] Retrying failed check for ${checkKey}`);
|
||||
}
|
||||
|
||||
console.log(`[Player Count] Starting audience watch for game ${gameId} (room ${roomCode}, max ${maxPlayers})`);
|
||||
|
||||
db.prepare(`
|
||||
UPDATE session_games
|
||||
SET player_count_check_status = 'checking'
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).run(sessionId, gameId);
|
||||
|
||||
activeChecks.set(checkKey, {
|
||||
sessionId,
|
||||
gameId,
|
||||
roomCode,
|
||||
watchInterval: null,
|
||||
browser: null
|
||||
});
|
||||
|
||||
await watchGameAsAudience(sessionId, gameId, roomCode, maxPlayers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop checking player count for a game
|
||||
*/
|
||||
async function stopPlayerCountCheck(sessionId, gameId) {
|
||||
const checkKey = `${sessionId}-${gameId}`;
|
||||
const check = activeChecks.get(checkKey);
|
||||
|
||||
if (check) {
|
||||
if (check.watchInterval) {
|
||||
clearInterval(check.watchInterval);
|
||||
}
|
||||
if (check.browser) {
|
||||
try {
|
||||
await check.browser.close();
|
||||
} catch (e) {
|
||||
// Ignore errors closing browser
|
||||
}
|
||||
}
|
||||
activeChecks.delete(checkKey);
|
||||
|
||||
const game = db.prepare(`
|
||||
SELECT player_count_check_status
|
||||
FROM session_games
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).get(sessionId, gameId);
|
||||
|
||||
if (game && game.player_count_check_status !== 'completed' && game.player_count_check_status !== 'failed') {
|
||||
db.prepare(`
|
||||
UPDATE session_games
|
||||
SET player_count_check_status = 'stopped'
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).run(sessionId, gameId);
|
||||
}
|
||||
|
||||
console.log(`[Player Count] Stopped check for ${checkKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all active checks (for graceful shutdown)
|
||||
*/
|
||||
async function cleanupAllChecks() {
|
||||
for (const [, check] of activeChecks.entries()) {
|
||||
if (check.watchInterval) {
|
||||
clearInterval(check.watchInterval);
|
||||
}
|
||||
if (check.browser) {
|
||||
try {
|
||||
await check.browser.close();
|
||||
} catch (e) {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
activeChecks.clear();
|
||||
console.log('[Player Count] Cleaned up all active checks');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startPlayerCountCheck,
|
||||
stopPlayerCountCheck,
|
||||
cleanupAllChecks
|
||||
};
|
||||
@@ -1,135 +0,0 @@
|
||||
const db = require('../database');
|
||||
const { getWebSocketManager } = require('./websocket-manager');
|
||||
const { checkRoomStatus } = require('./jackbox-api');
|
||||
|
||||
const POLL_INTERVAL_MS = 10000;
|
||||
|
||||
// Active room monitors, keyed by "{sessionId}-{gameId}"
|
||||
const activeMonitors = new Map();
|
||||
|
||||
/**
|
||||
* Broadcast game.started event when room becomes locked
|
||||
*/
|
||||
function broadcastGameStarted(sessionId, gameId, roomCode, maxPlayers) {
|
||||
try {
|
||||
const wsManager = getWebSocketManager();
|
||||
if (wsManager) {
|
||||
wsManager.broadcastEvent('game.started', {
|
||||
sessionId,
|
||||
gameId,
|
||||
roomCode,
|
||||
maxPlayers
|
||||
}, parseInt(sessionId));
|
||||
}
|
||||
console.log(`[Room Monitor] Broadcasted game.started for room ${roomCode} (max: ${maxPlayers})`);
|
||||
} catch (error) {
|
||||
console.error('[Room Monitor] Failed to broadcast game.started:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start monitoring a Jackbox room for game start (locked state).
|
||||
*
|
||||
* Polls the Jackbox REST API every 10 seconds. When the room becomes
|
||||
* locked, broadcasts a game.started WebSocket event and then hands off
|
||||
* to the player-count-checker to join as audience.
|
||||
*/
|
||||
async function startRoomMonitor(sessionId, gameId, roomCode, maxPlayers = 8) {
|
||||
const monitorKey = `${sessionId}-${gameId}`;
|
||||
|
||||
if (activeMonitors.has(monitorKey)) {
|
||||
console.log(`[Room Monitor] Already monitoring ${monitorKey}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[Room Monitor] Starting monitor for room ${roomCode} (${monitorKey})`);
|
||||
|
||||
const onGameStarted = (realMaxPlayers) => {
|
||||
broadcastGameStarted(sessionId, gameId, roomCode, realMaxPlayers);
|
||||
// Lazy require breaks the circular dependency with player-count-checker
|
||||
const { startPlayerCountCheck } = require('./player-count-checker');
|
||||
console.log(`[Room Monitor] Room ${roomCode} locked — handing off to player count checker`);
|
||||
startPlayerCountCheck(sessionId, gameId, roomCode, realMaxPlayers);
|
||||
};
|
||||
|
||||
const pollRoom = async () => {
|
||||
const game = db.prepare(`
|
||||
SELECT status FROM session_games
|
||||
WHERE session_id = ? AND id = ?
|
||||
`).get(sessionId, gameId);
|
||||
|
||||
if (!game || game.status === 'skipped' || game.status === 'played') {
|
||||
console.log(`[Room Monitor] Stopping — game status changed for ${monitorKey}`);
|
||||
stopRoomMonitor(sessionId, gameId);
|
||||
return;
|
||||
}
|
||||
|
||||
const roomStatus = await checkRoomStatus(roomCode);
|
||||
|
||||
if (!roomStatus.exists) {
|
||||
console.log(`[Room Monitor] Room ${roomCode} does not exist — stopping`);
|
||||
stopRoomMonitor(sessionId, gameId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (roomStatus.locked) {
|
||||
stopRoomMonitor(sessionId, gameId);
|
||||
onGameStarted(roomStatus.maxPlayers);
|
||||
return;
|
||||
}
|
||||
|
||||
if (roomStatus.full) {
|
||||
console.log(`[Room Monitor] Room ${roomCode} is full but not locked yet — waiting`);
|
||||
} else {
|
||||
console.log(`[Room Monitor] Room ${roomCode} lobby still open — waiting`);
|
||||
}
|
||||
};
|
||||
|
||||
// Poll immediately, then every POLL_INTERVAL_MS
|
||||
activeMonitors.set(monitorKey, {
|
||||
sessionId,
|
||||
gameId,
|
||||
roomCode,
|
||||
interval: null
|
||||
});
|
||||
|
||||
await pollRoom();
|
||||
|
||||
// If the monitor was already stopped (room locked or gone on first check), bail
|
||||
if (!activeMonitors.has(monitorKey)) return;
|
||||
|
||||
const interval = setInterval(() => pollRoom(), POLL_INTERVAL_MS);
|
||||
const monitor = activeMonitors.get(monitorKey);
|
||||
if (monitor) monitor.interval = interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop monitoring a room
|
||||
*/
|
||||
function stopRoomMonitor(sessionId, gameId) {
|
||||
const monitorKey = `${sessionId}-${gameId}`;
|
||||
const monitor = activeMonitors.get(monitorKey);
|
||||
|
||||
if (monitor) {
|
||||
if (monitor.interval) clearInterval(monitor.interval);
|
||||
activeMonitors.delete(monitorKey);
|
||||
console.log(`[Room Monitor] Stopped monitor for ${monitorKey}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all active monitors (for graceful shutdown)
|
||||
*/
|
||||
function cleanupAllMonitors() {
|
||||
for (const [, monitor] of activeMonitors.entries()) {
|
||||
if (monitor.interval) clearInterval(monitor.interval);
|
||||
}
|
||||
activeMonitors.clear();
|
||||
console.log('[Room Monitor] Cleaned up all active monitors');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startRoomMonitor,
|
||||
stopRoomMonitor,
|
||||
cleanupAllMonitors
|
||||
};
|
||||
Reference in New Issue
Block a user