initial actual player count implementation
This commit is contained in:
36
README.md
36
README.md
@@ -341,6 +341,42 @@ For integrating external bots (e.g., for live voting and game notifications), se
|
|||||||
- Example implementations in Node.js and Go
|
- Example implementations in Node.js and Go
|
||||||
- Security best practices
|
- Security best practices
|
||||||
|
|
||||||
|
## Jackbox Player Count Fetcher
|
||||||
|
|
||||||
|
The `scripts/` directory contains utilities for inspecting Jackbox game lobbies:
|
||||||
|
|
||||||
|
- **[get-player-count.go](scripts/get-player-count.go)** - Go + chromedp script (recommended, most reliable)
|
||||||
|
- **[get-player-count.html](scripts/get-player-count.html)** - Browser-based tool (no installation required!)
|
||||||
|
- **[get-jackbox-player-count.js](scripts/get-jackbox-player-count.js)** - Node.js script (limited, may not work)
|
||||||
|
|
||||||
|
See **[scripts/README.md](scripts/README.md)** for detailed usage instructions.
|
||||||
|
|
||||||
|
### Quick Start
|
||||||
|
|
||||||
|
**Go version (recommended for automation):**
|
||||||
|
```bash
|
||||||
|
cd scripts
|
||||||
|
go run get-player-count.go JYET
|
||||||
|
```
|
||||||
|
|
||||||
|
**Browser version (easiest for manual testing):**
|
||||||
|
1. Open `scripts/get-player-count.html` in any browser
|
||||||
|
2. Enter a 4-letter room code
|
||||||
|
3. View real-time player count and lobby status
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
- Automates joining jackbox.tv through Chrome/Chromium
|
||||||
|
- Captures WebSocket messages containing player data
|
||||||
|
- Extracts actual player count from lobby state
|
||||||
|
|
||||||
|
These tools retrieve:
|
||||||
|
- ✅ Actual player count (not just max capacity)
|
||||||
|
- ✅ List of current players and their roles (host/player)
|
||||||
|
- ✅ Game state and lobby status
|
||||||
|
- ✅ Audience count
|
||||||
|
|
||||||
|
**Note:** Direct WebSocket connection is not possible without authentication, so the tools join through jackbox.tv to capture the data.
|
||||||
|
|
||||||
## Database Schema
|
## Database Schema
|
||||||
|
|
||||||
### games
|
### games
|
||||||
|
|||||||
230
scripts/README.md
Normal file
230
scripts/README.md
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
# Jackbox Player Count Fetcher
|
||||||
|
|
||||||
|
Tools to retrieve the actual player count from a Jackbox game room in real-time.
|
||||||
|
|
||||||
|
## Available Implementations
|
||||||
|
|
||||||
|
### 1. Go + chromedp (Recommended) 🚀
|
||||||
|
The most reliable method - automates joining through jackbox.tv to capture WebSocket data.
|
||||||
|
|
||||||
|
### 2. Browser HTML Interface 🌐
|
||||||
|
Quick visual tool for manual testing - no installation required.
|
||||||
|
|
||||||
|
### 3. Node.js Script (Limited)
|
||||||
|
Attempts direct WebSocket connection - may not work due to authentication requirements.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- 🔍 Automatically joins jackbox.tv to capture WebSocket data
|
||||||
|
- 📊 Returns actual player count (not just max capacity)
|
||||||
|
- 👥 Lists all current players and their roles (host/player)
|
||||||
|
- 🎮 Shows game state, lobby state, and audience count
|
||||||
|
- 🎨 Pretty-printed output with colors
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Go Version (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scripts
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
**Prerequisites:** Go 1.21+ and Chrome/Chromium browser installed
|
||||||
|
|
||||||
|
### Browser Version (No Installation Required!)
|
||||||
|
|
||||||
|
Just open `get-player-count.html` in any web browser - no installation needed!
|
||||||
|
|
||||||
|
### Node.js Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scripts
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** The Node.js version may not work reliably due to Jackbox WebSocket authentication requirements.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Go Version (Best) 🚀
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Navigate to scripts directory
|
||||||
|
cd scripts
|
||||||
|
|
||||||
|
# Run the script
|
||||||
|
go run get-player-count.go JYET
|
||||||
|
|
||||||
|
# Or build and run
|
||||||
|
go build -o get-player-count get-player-count.go
|
||||||
|
./get-player-count JYET
|
||||||
|
```
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
1. Opens jackbox.tv in headless Chrome
|
||||||
|
2. Automatically enters room code and joins as "Observer"
|
||||||
|
3. Captures WebSocket messages from the browser
|
||||||
|
4. Extracts player count from `client/welcome` message
|
||||||
|
5. Enriches with data from REST API
|
||||||
|
|
||||||
|
### Browser Version 🌐
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Just open in browser
|
||||||
|
open get-player-count.html
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Open `get-player-count.html` in your web browser
|
||||||
|
2. Enter a room code (e.g., "JYET")
|
||||||
|
3. Click "Get Player Count"
|
||||||
|
4. View results instantly
|
||||||
|
|
||||||
|
This version runs entirely in the browser and doesn't require any backend!
|
||||||
|
|
||||||
|
### Node.js Version (Limited)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node get-jackbox-player-count.js JYET
|
||||||
|
```
|
||||||
|
|
||||||
|
**Warning:** May fail due to WebSocket authentication requirements. Use the Go version for reliable results.
|
||||||
|
|
||||||
|
### JSON Output (for scripting)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
JSON_OUTPUT=true node get-jackbox-player-count.js JYET
|
||||||
|
```
|
||||||
|
|
||||||
|
### As a Module
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { getRoomInfo, getPlayerCount } = require('./get-jackbox-player-count');
|
||||||
|
|
||||||
|
async function example() {
|
||||||
|
const roomCode = 'JYET';
|
||||||
|
|
||||||
|
// Get room info
|
||||||
|
const roomInfo = await getRoomInfo(roomCode);
|
||||||
|
console.log('Game:', roomInfo.appTag);
|
||||||
|
|
||||||
|
// Get player count
|
||||||
|
const result = await getPlayerCount(roomCode, roomInfo);
|
||||||
|
console.log('Players:', result.playerCount);
|
||||||
|
console.log('Player list:', result.players);
|
||||||
|
}
|
||||||
|
|
||||||
|
example();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Output Example
|
||||||
|
|
||||||
|
```
|
||||||
|
Jackbox Player Count Fetcher
|
||||||
|
Room Code: JYET
|
||||||
|
|
||||||
|
Fetching room information...
|
||||||
|
✓ Room found: triviadeath
|
||||||
|
Max Players: 8
|
||||||
|
|
||||||
|
Connecting to WebSocket...
|
||||||
|
URL: wss://i-007fc4f534bce7898.play.jackboxgames.com/api/v2/rooms/JYET
|
||||||
|
|
||||||
|
✓ WebSocket connected
|
||||||
|
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
Jackbox Room Status
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
|
||||||
|
Room Code: JYET
|
||||||
|
Game: triviadeath
|
||||||
|
Game State: Lobby
|
||||||
|
Lobby State: CanStart
|
||||||
|
Locked: No
|
||||||
|
Full: No
|
||||||
|
|
||||||
|
Players: 3 / 8
|
||||||
|
Audience: 0
|
||||||
|
|
||||||
|
Current Players:
|
||||||
|
1. Host (host)
|
||||||
|
2. E (player)
|
||||||
|
3. F (player)
|
||||||
|
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
```
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
1. **REST API Call**: First queries `https://ecast.jackboxgames.com/api/v2/rooms/{ROOM_CODE}` to get room metadata
|
||||||
|
2. **WebSocket Connection**: Establishes a WebSocket connection to the game server
|
||||||
|
3. **Join as Observer**: Sends a `client/connect` message to join as an audience member
|
||||||
|
4. **Parse Response**: Listens for the `client/welcome` message containing the full lobby state
|
||||||
|
5. **Extract Player Count**: Counts the players in the `here` object
|
||||||
|
|
||||||
|
## API Response Structure
|
||||||
|
|
||||||
|
The script returns an object with the following structure:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
roomCode: "JYET",
|
||||||
|
appTag: "triviadeath",
|
||||||
|
playerCount: 3, // Actual player count
|
||||||
|
audienceCount: 0, // Number of audience members
|
||||||
|
maxPlayers: 8, // Maximum capacity
|
||||||
|
gameState: "Lobby", // Current game state
|
||||||
|
lobbyState: "CanStart", // Whether game can start
|
||||||
|
locked: false, // Whether lobby is locked
|
||||||
|
full: false, // Whether lobby is full
|
||||||
|
players: [ // List of all players
|
||||||
|
{ id: "1", role: "host", name: "Host" },
|
||||||
|
{ id: "2", role: "player", name: "E" },
|
||||||
|
{ id: "3", role: "player", name: "F" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Integration with Your App
|
||||||
|
|
||||||
|
You can integrate this into your Jackbox game picker application:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// In your backend API
|
||||||
|
const { getRoomInfo, getPlayerCount } = require('./scripts/get-jackbox-player-count');
|
||||||
|
|
||||||
|
app.get('/api/room-status/:roomCode', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const roomCode = req.params.roomCode.toUpperCase();
|
||||||
|
const roomInfo = await getRoomInfo(roomCode);
|
||||||
|
const playerData = await getPlayerCount(roomCode, roomInfo);
|
||||||
|
res.json(playerData);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(404).json({ error: 'Room not found' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Room not found or invalid"
|
||||||
|
- Double-check the room code is correct
|
||||||
|
- Make sure the game is currently running (room codes expire after games end)
|
||||||
|
|
||||||
|
### "Connection timeout"
|
||||||
|
- The game server might be unavailable
|
||||||
|
- Check your internet connection
|
||||||
|
- The room might have closed
|
||||||
|
|
||||||
|
### WebSocket connection fails
|
||||||
|
- Ensure you have the `ws` package installed: `npm install`
|
||||||
|
- Some networks/firewalls might block WebSocket connections
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `ws` (^8.14.0) - WebSocket client for Node.js
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
184
scripts/TESTING.md
Normal file
184
scripts/TESTING.md
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
# Testing the Jackbox Player Count Script
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Go 1.21+ installed
|
||||||
|
2. Chrome or Chromium browser installed
|
||||||
|
3. Active Jackbox lobby with a valid room code
|
||||||
|
|
||||||
|
## Running the Script
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd scripts
|
||||||
|
go run get-player-count.go JYET
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace `JYET` with your actual room code.
|
||||||
|
|
||||||
|
### Debug Mode
|
||||||
|
|
||||||
|
If the script isn't capturing data, run with debug output:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DEBUG=true go run get-player-count.go JYET
|
||||||
|
```
|
||||||
|
|
||||||
|
This will show:
|
||||||
|
- Each WebSocket frame received
|
||||||
|
- Parsed opcodes
|
||||||
|
- Detailed connection info
|
||||||
|
|
||||||
|
## Expected Output
|
||||||
|
|
||||||
|
### Success
|
||||||
|
|
||||||
|
```
|
||||||
|
🎮 Jackbox Player Count Fetcher
|
||||||
|
Room Code: JYET
|
||||||
|
|
||||||
|
⏳ Navigating to jackbox.tv...
|
||||||
|
✓ Loaded jackbox.tv
|
||||||
|
⏳ Joining room JYET...
|
||||||
|
✓ Clicked Play button, waiting for WebSocket data...
|
||||||
|
✓ Captured lobby data from WebSocket
|
||||||
|
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
Jackbox Room Status
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
|
||||||
|
Room Code: JYET
|
||||||
|
Game: triviadeath
|
||||||
|
Game State: Lobby
|
||||||
|
Lobby State: CanStart
|
||||||
|
Locked: false
|
||||||
|
Full: false
|
||||||
|
|
||||||
|
Players: 3 / 8
|
||||||
|
Audience: 0
|
||||||
|
|
||||||
|
Current Players:
|
||||||
|
1. Host (host)
|
||||||
|
2. E (player)
|
||||||
|
3. F (player)
|
||||||
|
|
||||||
|
═══════════════════════════════════════════
|
||||||
|
```
|
||||||
|
|
||||||
|
### If No WebSocket Messages Captured
|
||||||
|
|
||||||
|
```
|
||||||
|
Error: no WebSocket messages captured - connection may have failed
|
||||||
|
Try running with DEBUG=true for more details
|
||||||
|
```
|
||||||
|
|
||||||
|
**Possible causes:**
|
||||||
|
- Room code is invalid
|
||||||
|
- Game lobby is closed
|
||||||
|
- Network connectivity issues
|
||||||
|
|
||||||
|
### If Messages Captured but No Player Data
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ Captured 15 WebSocket messages but couldn't find 'client/welcome'
|
||||||
|
|
||||||
|
Message types found:
|
||||||
|
- room/update: 5
|
||||||
|
- audience/count: 3
|
||||||
|
- ping: 7
|
||||||
|
|
||||||
|
Error: could not find player count data in WebSocket messages
|
||||||
|
Room may be invalid, closed, or not in lobby state
|
||||||
|
```
|
||||||
|
|
||||||
|
**Possible causes:**
|
||||||
|
- Game has already started (not in lobby)
|
||||||
|
- Room code exists but game is in different state
|
||||||
|
- WebSocket connected but welcome message not sent
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Cookie Errors (Harmless)
|
||||||
|
|
||||||
|
You may see errors like:
|
||||||
|
```
|
||||||
|
ERROR: could not unmarshal event: parse error: expected string near offset 1247 of 'cookiePart...'
|
||||||
|
```
|
||||||
|
|
||||||
|
**These are harmless** and are suppressed in the output. They're chromedp trying to parse cookie data that doesn't follow expected format.
|
||||||
|
|
||||||
|
### "Failed to load jackbox.tv"
|
||||||
|
|
||||||
|
- Check your internet connection
|
||||||
|
- Verify https://jackbox.tv loads in a regular browser
|
||||||
|
- Try running without `headless` mode (edit the Go file)
|
||||||
|
|
||||||
|
### "Failed to enter room code"
|
||||||
|
|
||||||
|
- Verify the room code is valid
|
||||||
|
- Check that the lobby is actually open
|
||||||
|
- Try running with DEBUG=true to see what's happening
|
||||||
|
|
||||||
|
### "Failed to click Play button"
|
||||||
|
|
||||||
|
- The button may still be disabled
|
||||||
|
- Room code validation may have failed
|
||||||
|
- Name field may not be filled
|
||||||
|
|
||||||
|
### No WebSocket Messages at All
|
||||||
|
|
||||||
|
This means the browser never connected to the game's WebSocket server:
|
||||||
|
- Verify the room code is correct
|
||||||
|
- Check that the game lobby is actually open and accepting players
|
||||||
|
- The game may have a full lobby
|
||||||
|
- The room may have expired
|
||||||
|
|
||||||
|
## Testing with Different Game States
|
||||||
|
|
||||||
|
### Lobby (Should Work)
|
||||||
|
When the game is in the lobby waiting for players to join.
|
||||||
|
|
||||||
|
### During Game (May Not Work)
|
||||||
|
Once the game starts, the WebSocket messages change. The `client/welcome` message may not be sent.
|
||||||
|
|
||||||
|
### After Game (Won't Work)
|
||||||
|
Room codes expire after the game session ends.
|
||||||
|
|
||||||
|
## Manual Verification
|
||||||
|
|
||||||
|
You can verify the data by:
|
||||||
|
|
||||||
|
1. Open https://jackbox.tv in a regular browser
|
||||||
|
2. Open Developer Tools (F12)
|
||||||
|
3. Go to Network tab
|
||||||
|
4. Filter by "WS" (WebSocket)
|
||||||
|
5. Join the room with the same code
|
||||||
|
6. Look for `client/welcome` message in WebSocket frames
|
||||||
|
7. Compare the data with what the script outputs
|
||||||
|
|
||||||
|
## Common Room States
|
||||||
|
|
||||||
|
| State | `client/welcome` | Player Count Available |
|
||||||
|
|-------|------------------|------------------------|
|
||||||
|
| Lobby - Waiting | ✅ Yes | ✅ Yes |
|
||||||
|
| Lobby - Full | ✅ Yes | ✅ Yes |
|
||||||
|
| Game Starting | ⚠️ Maybe | ⚠️ Maybe |
|
||||||
|
| Game In Progress | ❌ No | ❌ No |
|
||||||
|
| Game Ended | ❌ No | ❌ No |
|
||||||
|
|
||||||
|
## Performance Notes
|
||||||
|
|
||||||
|
- Takes ~5-10 seconds to complete
|
||||||
|
- Most time is waiting for WebSocket connection
|
||||||
|
- Headless Chrome startup adds ~1-2 seconds
|
||||||
|
- Network latency affects timing
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
If the script works:
|
||||||
|
1. Extract the function into a library package
|
||||||
|
2. Integrate with your bot
|
||||||
|
3. Set up cron jobs or periodic polling
|
||||||
|
4. Add result caching to reduce load
|
||||||
|
|
||||||
262
scripts/get-jackbox-player-count.js
Normal file
262
scripts/get-jackbox-player-count.js
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
#!/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
|
||||||
|
};
|
||||||
|
|
||||||
394
scripts/get-player-count.go
Normal file
394
scripts/get-player-count.go
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/chromedp/cdproto/network"
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PlayerInfo represents a player in the lobby
|
||||||
|
type PlayerInfo struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LobbyStatus contains all information about the current lobby
|
||||||
|
type LobbyStatus struct {
|
||||||
|
RoomCode string `json:"roomCode"`
|
||||||
|
AppTag string `json:"appTag"`
|
||||||
|
PlayerCount int `json:"playerCount"`
|
||||||
|
AudienceCount int `json:"audienceCount"`
|
||||||
|
MaxPlayers int `json:"maxPlayers"`
|
||||||
|
GameState string `json:"gameState"`
|
||||||
|
LobbyState string `json:"lobbyState"`
|
||||||
|
Locked bool `json:"locked"`
|
||||||
|
Full bool `json:"full"`
|
||||||
|
Players []PlayerInfo `json:"players"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocketMessage represents a parsed WebSocket message
|
||||||
|
type WebSocketMessage struct {
|
||||||
|
PC int `json:"pc"`
|
||||||
|
Opcode string `json:"opcode"`
|
||||||
|
Result map[string]interface{} `json:"result"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func min(a, b int) int {
|
||||||
|
if a < b {
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Println("Usage: go run get-player-count.go <ROOM_CODE>")
|
||||||
|
fmt.Println("Example: go run get-player-count.go JYET")
|
||||||
|
fmt.Println("\nSet DEBUG=true for verbose output:")
|
||||||
|
fmt.Println("DEBUG=true go run get-player-count.go JYET")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
roomCode := strings.ToUpper(strings.TrimSpace(os.Args[1]))
|
||||||
|
|
||||||
|
if len(roomCode) != 4 {
|
||||||
|
fmt.Println("Error: Room code must be exactly 4 characters")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("🎮 Jackbox Player Count Fetcher\n")
|
||||||
|
fmt.Printf("Room Code: %s\n\n", roomCode)
|
||||||
|
|
||||||
|
status, err := getPlayerCount(roomCode)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
printStatus(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPlayerCount(roomCode string) (*LobbyStatus, error) {
|
||||||
|
// Create chrome context with less verbose logging
|
||||||
|
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||||
|
chromedp.Flag("headless", true),
|
||||||
|
chromedp.Flag("disable-gpu", true),
|
||||||
|
chromedp.Flag("no-sandbox", true),
|
||||||
|
chromedp.Flag("disable-web-security", true),
|
||||||
|
)
|
||||||
|
|
||||||
|
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Create context without default logging to reduce cookie errors
|
||||||
|
ctx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(func(s string, i ...interface{}) {
|
||||||
|
// Only log non-cookie errors
|
||||||
|
msg := fmt.Sprintf(s, i...)
|
||||||
|
if !strings.Contains(msg, "cookiePart") && !strings.Contains(msg, "could not unmarshal") {
|
||||||
|
log.Printf(msg)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Set timeout
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var lobbyStatus *LobbyStatus
|
||||||
|
welcomeMessageFound := false
|
||||||
|
wsMessages := make([]string, 0)
|
||||||
|
|
||||||
|
// Listen for WebSocket frames - this is the most reliable method
|
||||||
|
debugMode := os.Getenv("DEBUG") == "true"
|
||||||
|
|
||||||
|
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||||
|
if debugMode {
|
||||||
|
fmt.Printf("[DEBUG] Event type: %T\n", ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ev := ev.(type) {
|
||||||
|
case *network.EventWebSocketCreated:
|
||||||
|
if debugMode {
|
||||||
|
fmt.Printf("[DEBUG] WebSocket Created: %s\n", ev.URL)
|
||||||
|
}
|
||||||
|
case *network.EventWebSocketFrameReceived:
|
||||||
|
// Capture all WebSocket frames
|
||||||
|
wsMessages = append(wsMessages, ev.Response.PayloadData)
|
||||||
|
|
||||||
|
if debugMode {
|
||||||
|
fmt.Printf("[DEBUG] WS Frame received (%d bytes)\n", len(ev.Response.PayloadData))
|
||||||
|
if len(ev.Response.PayloadData) < 200 {
|
||||||
|
fmt.Printf("[DEBUG] Data: %s\n", ev.Response.PayloadData)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[DEBUG] Data (truncated): %s...\n", ev.Response.PayloadData[:200])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse immediately
|
||||||
|
var wsMsg WebSocketMessage
|
||||||
|
if err := json.Unmarshal([]byte(ev.Response.PayloadData), &wsMsg); err == nil {
|
||||||
|
if debugMode {
|
||||||
|
fmt.Printf("[DEBUG] Parsed opcode: %s\n", wsMsg.Opcode)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wsMsg.Opcode == "client/welcome" && wsMsg.Result != nil {
|
||||||
|
lobbyStatus = parseWelcomeMessage(&wsMsg)
|
||||||
|
welcomeMessageFound = true
|
||||||
|
fmt.Println("✓ Captured lobby data from WebSocket")
|
||||||
|
}
|
||||||
|
} else if debugMode {
|
||||||
|
fmt.Printf("[DEBUG] Failed to parse JSON: %v\n", err)
|
||||||
|
}
|
||||||
|
case *network.EventWebSocketFrameSent:
|
||||||
|
if debugMode {
|
||||||
|
fmt.Printf("[DEBUG] WS Frame sent: %s\n", ev.Response.PayloadData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Println("⏳ Navigating to jackbox.tv...")
|
||||||
|
|
||||||
|
// Enable network tracking BEFORE navigation
|
||||||
|
if err := chromedp.Run(ctx, network.Enable()); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to enable network tracking: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := chromedp.Run(ctx,
|
||||||
|
chromedp.Navigate("https://jackbox.tv/"),
|
||||||
|
chromedp.WaitVisible(`input[placeholder*="ENTER 4-LETTER CODE"]`, chromedp.ByQuery),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load jackbox.tv: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✓ Loaded jackbox.tv\n")
|
||||||
|
fmt.Printf("⏳ Joining room %s...\n", roomCode)
|
||||||
|
|
||||||
|
// Type room code and press Enter to join
|
||||||
|
err = chromedp.Run(ctx,
|
||||||
|
chromedp.Focus(`input[placeholder*="ENTER 4-LETTER CODE"]`, chromedp.ByQuery),
|
||||||
|
chromedp.SendKeys(`input[placeholder*="ENTER 4-LETTER CODE"]`, roomCode+"\n", chromedp.ByQuery),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to enter room code: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if debugMode {
|
||||||
|
fmt.Println("[DEBUG] Entered room code and pressed Enter")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for room code validation and page transition
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
fmt.Println("✓ Clicked Play button, waiting for WebSocket data...")
|
||||||
|
|
||||||
|
// Check if we successfully joined (look for typical lobby UI elements)
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
var pageText string
|
||||||
|
err = chromedp.Run(ctx,
|
||||||
|
chromedp.Text("body", &pageText, chromedp.ByQuery),
|
||||||
|
)
|
||||||
|
if err == nil && debugMode {
|
||||||
|
if strings.Contains(pageText, "Sit back") || strings.Contains(pageText, "waiting") {
|
||||||
|
fmt.Println("[DEBUG] Successfully joined lobby (found lobby text)")
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[DEBUG] Page text: %s\n", pageText[:min(300, len(pageText))])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait longer for WebSocket to connect and receive welcome message
|
||||||
|
for i := 0; i < 15 && !welcomeMessageFound; i++ {
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
if i%4 == 0 {
|
||||||
|
fmt.Printf("⏳ Waiting for lobby data... (%ds)\n", i/2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we still didn't get it from WebSocket frames, try parsing all captured messages
|
||||||
|
if !welcomeMessageFound && len(wsMessages) > 0 {
|
||||||
|
fmt.Printf("⏳ Parsing %d captured WebSocket messages...\n", len(wsMessages))
|
||||||
|
|
||||||
|
for _, msg := range wsMessages {
|
||||||
|
var wsMsg WebSocketMessage
|
||||||
|
if err := json.Unmarshal([]byte(msg), &wsMsg); err == nil {
|
||||||
|
if wsMsg.Opcode == "client/welcome" && wsMsg.Result != nil {
|
||||||
|
lobbyStatus = parseWelcomeMessage(&wsMsg)
|
||||||
|
welcomeMessageFound = true
|
||||||
|
fmt.Println("✓ Found lobby data in captured messages")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lobbyStatus == nil {
|
||||||
|
if len(wsMessages) == 0 {
|
||||||
|
return nil, fmt.Errorf("no WebSocket messages captured - connection may have failed\nTry running with DEBUG=true for more details")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show what we captured
|
||||||
|
fmt.Printf("\n⚠️ Captured %d WebSocket messages but couldn't find 'client/welcome'\n", len(wsMessages))
|
||||||
|
fmt.Println("\nMessage types found:")
|
||||||
|
opcodes := make(map[string]int)
|
||||||
|
for _, msg := range wsMessages {
|
||||||
|
var wsMsg WebSocketMessage
|
||||||
|
if err := json.Unmarshal([]byte(msg), &wsMsg); err == nil {
|
||||||
|
opcodes[wsMsg.Opcode]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for opcode, count := range opcodes {
|
||||||
|
fmt.Printf(" - %s: %d\n", opcode, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("could not find player count data in WebSocket messages\nRoom may be invalid, closed, or not in lobby state")
|
||||||
|
}
|
||||||
|
|
||||||
|
lobbyStatus.RoomCode = roomCode
|
||||||
|
|
||||||
|
// Fetch additional room info from REST API
|
||||||
|
if err := enrichWithRestAPI(lobbyStatus); err != nil {
|
||||||
|
fmt.Printf("Warning: Could not fetch additional room info: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lobbyStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWelcomeMessage(msg *WebSocketMessage) *LobbyStatus {
|
||||||
|
status := &LobbyStatus{
|
||||||
|
Players: []PlayerInfo{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse "here" object for players
|
||||||
|
if here, ok := msg.Result["here"].(map[string]interface{}); ok {
|
||||||
|
status.PlayerCount = len(here)
|
||||||
|
|
||||||
|
for id, playerData := range here {
|
||||||
|
if pd, ok := playerData.(map[string]interface{}); ok {
|
||||||
|
player := PlayerInfo{ID: id}
|
||||||
|
|
||||||
|
if roles, ok := pd["roles"].(map[string]interface{}); ok {
|
||||||
|
if _, hasHost := roles["host"]; hasHost {
|
||||||
|
player.Role = "host"
|
||||||
|
player.Name = "Host"
|
||||||
|
} else if playerRole, ok := roles["player"].(map[string]interface{}); ok {
|
||||||
|
player.Role = "player"
|
||||||
|
if name, ok := playerRole["name"].(string); ok {
|
||||||
|
player.Name = name
|
||||||
|
} else {
|
||||||
|
player.Name = "Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status.Players = append(status.Players, player)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse entities for additional info
|
||||||
|
if entities, ok := msg.Result["entities"].(map[string]interface{}); ok {
|
||||||
|
// Audience count
|
||||||
|
if audience, ok := entities["audience"].([]interface{}); ok && len(audience) > 1 {
|
||||||
|
if audienceData, ok := audience[1].(map[string]interface{}); ok {
|
||||||
|
if count, ok := audienceData["count"].(float64); ok {
|
||||||
|
status.AudienceCount = int(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Room state
|
||||||
|
if bcRoom, ok := entities["bc:room"].([]interface{}); ok && len(bcRoom) > 1 {
|
||||||
|
if roomData, ok := bcRoom[1].(map[string]interface{}); ok {
|
||||||
|
if val, ok := roomData["val"].(map[string]interface{}); ok {
|
||||||
|
if gameState, ok := val["state"].(string); ok {
|
||||||
|
status.GameState = gameState
|
||||||
|
}
|
||||||
|
if lobbyState, ok := val["lobbyState"].(string); ok {
|
||||||
|
status.LobbyState = lobbyState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
|
||||||
|
func enrichWithRestAPI(status *LobbyStatus) error {
|
||||||
|
// Fetch additional room info from REST API
|
||||||
|
url := fmt.Sprintf("https://ecast.jackboxgames.com/api/v2/rooms/%s", status.RoomCode)
|
||||||
|
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var result struct {
|
||||||
|
OK bool `json:"ok"`
|
||||||
|
Body struct {
|
||||||
|
AppTag string `json:"appTag"`
|
||||||
|
MaxPlayers int `json:"maxPlayers"`
|
||||||
|
Locked bool `json:"locked"`
|
||||||
|
Full bool `json:"full"`
|
||||||
|
} `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.OK {
|
||||||
|
status.AppTag = result.Body.AppTag
|
||||||
|
status.MaxPlayers = result.Body.MaxPlayers
|
||||||
|
status.Locked = result.Body.Locked
|
||||||
|
status.Full = result.Body.Full
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func printStatus(status *LobbyStatus) {
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("═══════════════════════════════════════════")
|
||||||
|
fmt.Println(" Jackbox Room Status")
|
||||||
|
fmt.Println("═══════════════════════════════════════════")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("Room Code: %s\n", status.RoomCode)
|
||||||
|
fmt.Printf("Game: %s\n", status.AppTag)
|
||||||
|
fmt.Printf("Game State: %s\n", status.GameState)
|
||||||
|
fmt.Printf("Lobby State: %s\n", status.LobbyState)
|
||||||
|
fmt.Printf("Locked: %t\n", status.Locked)
|
||||||
|
fmt.Printf("Full: %t\n", status.Full)
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Printf("Players: %d / %d\n", status.PlayerCount, status.MaxPlayers)
|
||||||
|
fmt.Printf("Audience: %d\n", status.AudienceCount)
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
if len(status.Players) > 0 {
|
||||||
|
fmt.Println("Current Players:")
|
||||||
|
for i, player := range status.Players {
|
||||||
|
fmt.Printf(" %d. %s (%s)\n", i+1, player.Name, player.Role)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("═══════════════════════════════════════════")
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
468
scripts/get-player-count.html
Normal file
468
scripts/get-player-count.html
Normal file
@@ -0,0 +1,468 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Jackbox Player Count Fetcher</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 50px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 30px;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 12px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.loading {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.success {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-card {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value.highlight {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.players-list {
|
||||||
|
background: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.players-list h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-item {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 5px 0;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-name {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-role {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-role.host {
|
||||||
|
background: #fff3e0;
|
||||||
|
color: #e65100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-role.player {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.success {
|
||||||
|
background: #e8f5e9;
|
||||||
|
color: #2e7d32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.error {
|
||||||
|
background: #ffebee;
|
||||||
|
color: #c62828;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>🎮 Jackbox Player Count Fetcher</h1>
|
||||||
|
<p class="subtitle">Enter a room code to get real-time player information</p>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="roomCode" placeholder="Enter room code (e.g., JYET)" maxlength="4">
|
||||||
|
<button id="fetchBtn" onclick="fetchPlayerCount()">Get Player Count</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status" class="status"></div>
|
||||||
|
|
||||||
|
<div id="results" class="results">
|
||||||
|
<div class="result-card">
|
||||||
|
<div class="stat-grid">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">Players</div>
|
||||||
|
<div class="stat-value highlight" id="playerCount">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">Max Players</div>
|
||||||
|
<div class="stat-value" id="maxPlayers">-</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<div class="stat-label">Audience</div>
|
||||||
|
<div class="stat-value" id="audienceCount">-</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">Room Code:</span>
|
||||||
|
<span class="info-value" id="displayRoomCode">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">Game:</span>
|
||||||
|
<span class="info-value" id="gameTag">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">Game State:</span>
|
||||||
|
<span class="info-value" id="gameState">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">Lobby State:</span>
|
||||||
|
<span class="info-value" id="lobbyState">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">Locked:</span>
|
||||||
|
<span class="info-value" id="locked">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-row">
|
||||||
|
<span class="info-label">Full:</span>
|
||||||
|
<span class="info-value" id="full">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="playersContainer" class="players-list" style="display: none;">
|
||||||
|
<h3>Current Players</h3>
|
||||||
|
<div id="playersList"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Allow Enter key to trigger fetch
|
||||||
|
document.getElementById('roomCode').addEventListener('keypress', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
fetchPlayerCount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function fetchPlayerCount() {
|
||||||
|
const roomCode = document.getElementById('roomCode').value.toUpperCase().trim();
|
||||||
|
|
||||||
|
if (!roomCode || roomCode.length !== 4) {
|
||||||
|
showStatus('Please enter a valid 4-letter room code', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showStatus('Fetching room information...', 'loading');
|
||||||
|
document.getElementById('fetchBtn').disabled = true;
|
||||||
|
document.getElementById('results').classList.remove('visible');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Get room info
|
||||||
|
const roomInfo = await getRoomInfo(roomCode);
|
||||||
|
|
||||||
|
showStatus('Connecting to WebSocket...', 'loading');
|
||||||
|
|
||||||
|
// Step 2: Connect to WebSocket and get player count
|
||||||
|
const result = await getPlayerCount(roomCode, roomInfo);
|
||||||
|
|
||||||
|
showStatus('✓ Successfully retrieved player information', 'success');
|
||||||
|
displayResults(result);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('status').style.display = 'none';
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
showStatus(`Error: ${error.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
document.getElementById('fetchBtn').disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRoomInfo(roomCode) {
|
||||||
|
const response = await fetch(`https://ecast.jackboxgames.com/api/v2/rooms/${roomCode}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!data.ok) {
|
||||||
|
throw new Error('Room not found or invalid');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getPlayerCount(roomCode, roomInfo) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const wsUrl = `wss://${roomInfo.host}/api/v2/rooms/${roomCode}`;
|
||||||
|
const ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
ws.close();
|
||||||
|
reject(new Error('Connection timeout'));
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
// Join as audience to get lobby state without affecting the game
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
opcode: 'client/connect',
|
||||||
|
params: {
|
||||||
|
name: 'WebObserver',
|
||||||
|
role: 'audience',
|
||||||
|
format: 'json'
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
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' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.close();
|
||||||
|
resolve({
|
||||||
|
roomCode,
|
||||||
|
appTag: roomInfo.appTag,
|
||||||
|
playerCount,
|
||||||
|
audienceCount,
|
||||||
|
maxPlayers: roomInfo.maxPlayers,
|
||||||
|
gameState,
|
||||||
|
lobbyState,
|
||||||
|
locked: roomInfo.locked,
|
||||||
|
full: roomInfo.full,
|
||||||
|
players
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore parse errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
reject(new Error('WebSocket connection failed'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showStatus(message, type) {
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
status.textContent = message;
|
||||||
|
status.className = `status ${type}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayResults(result) {
|
||||||
|
document.getElementById('results').classList.add('visible');
|
||||||
|
|
||||||
|
document.getElementById('playerCount').textContent = result.playerCount;
|
||||||
|
document.getElementById('maxPlayers').textContent = result.maxPlayers;
|
||||||
|
document.getElementById('audienceCount').textContent = result.audienceCount;
|
||||||
|
document.getElementById('displayRoomCode').textContent = result.roomCode;
|
||||||
|
document.getElementById('gameTag').textContent = result.appTag;
|
||||||
|
document.getElementById('gameState').textContent = result.gameState;
|
||||||
|
document.getElementById('lobbyState').textContent = result.lobbyState;
|
||||||
|
|
||||||
|
document.getElementById('locked').innerHTML = result.locked
|
||||||
|
? '<span class="badge error">Yes</span>'
|
||||||
|
: '<span class="badge success">No</span>';
|
||||||
|
|
||||||
|
document.getElementById('full').innerHTML = result.full
|
||||||
|
? '<span class="badge error">Yes</span>'
|
||||||
|
: '<span class="badge success">No</span>';
|
||||||
|
|
||||||
|
if (result.players && result.players.length > 0) {
|
||||||
|
const playersList = document.getElementById('playersList');
|
||||||
|
playersList.innerHTML = result.players.map(player => `
|
||||||
|
<div class="player-item">
|
||||||
|
<span class="player-name">${player.name}</span>
|
||||||
|
<span class="player-role ${player.role}">${player.role}</span>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
document.getElementById('playersContainer').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
document.getElementById('playersContainer').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
18
scripts/go.mod
Normal file
18
scripts/go.mod
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
module jackbox-player-count
|
||||||
|
|
||||||
|
go 1.21
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998
|
||||||
|
github.com/chromedp/chromedp v0.9.3
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/chromedp/sysutil v1.0.0 // indirect
|
||||||
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
|
github.com/gobwas/ws v1.3.0 // indirect
|
||||||
|
github.com/josharian/intern v1.0.0 // indirect
|
||||||
|
github.com/mailru/easyjson v0.7.7 // indirect
|
||||||
|
golang.org/x/sys v0.13.0 // indirect
|
||||||
|
)
|
||||||
23
scripts/go.sum
Normal file
23
scripts/go.sum
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998 h1:2zipcnjfFdqAjOQa8otCCh0Lk1M7RBzciy3s80YAKHk=
|
||||||
|
github.com/chromedp/cdproto v0.0.0-20231011050154-1d073bb38998/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=
|
||||||
|
github.com/chromedp/chromedp v0.9.3 h1:Wq58e0dZOdHsxaj9Owmfcf+ibtpYN1N0FWVbaxa/esg=
|
||||||
|
github.com/chromedp/chromedp v0.9.3/go.mod h1:NipeUkUcuzIdFbBP8eNNvl9upcceOfWzoJn6cRe4ksA=
|
||||||
|
github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic=
|
||||||
|
github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=
|
||||||
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0=
|
||||||
|
github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||||
|
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||||
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
|
||||||
|
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
|
||||||
|
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
|
||||||
|
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||||
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||||
|
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||||
|
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
184
scripts/jackbox-count-v2.js
Normal file
184
scripts/jackbox-count-v2.js
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const puppeteer = require('puppeteer');
|
||||||
|
|
||||||
|
async function getPlayerCount(roomCode) {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: 'new',
|
||||||
|
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security']
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
let playerCount = null;
|
||||||
|
|
||||||
|
// Listen for console API calls (this catches console.log with formatting)
|
||||||
|
page.on('console', async msg => {
|
||||||
|
try {
|
||||||
|
// Get all args
|
||||||
|
for (const arg of msg.args()) {
|
||||||
|
const val = await arg.jsonValue();
|
||||||
|
const str = JSON.stringify(val);
|
||||||
|
|
||||||
|
if (process.env.DEBUG) console.error('[CONSOLE]', str.substring(0, 200));
|
||||||
|
|
||||||
|
// Check if this is the welcome message
|
||||||
|
if (str && str.includes('"opcode":"client/welcome"') && str.includes('"here"')) {
|
||||||
|
const data = JSON.parse(str);
|
||||||
|
if (data.result && data.result.here) {
|
||||||
|
playerCount = Object.keys(data.result.here).length;
|
||||||
|
if (process.env.DEBUG) console.error('[FOUND] Player count:', playerCount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (process.env.DEBUG) console.error('[1] Loading page...');
|
||||||
|
await page.goto('https://jackbox.tv/', { waitUntil: 'networkidle2', timeout: 30000 });
|
||||||
|
|
||||||
|
// Clear storage and reload to avoid reconnect
|
||||||
|
if (process.env.DEBUG) console.error('[2] Clearing storage...');
|
||||||
|
await page.evaluate(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
});
|
||||||
|
await page.reload({ waitUntil: 'networkidle2' });
|
||||||
|
|
||||||
|
await page.waitForSelector('input[placeholder*="ENTER 4-LETTER CODE"]', { timeout: 10000 });
|
||||||
|
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error('[2.5] Checking all inputs on page...');
|
||||||
|
const allInputs = await page.evaluate(() => {
|
||||||
|
const inputs = Array.from(document.querySelectorAll('input'));
|
||||||
|
return inputs.map(inp => ({
|
||||||
|
type: inp.type,
|
||||||
|
placeholder: inp.placeholder,
|
||||||
|
value: inp.value,
|
||||||
|
name: inp.name,
|
||||||
|
id: inp.id,
|
||||||
|
visible: inp.offsetParent !== null
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
console.error('[2.5] All inputs:', JSON.stringify(allInputs, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.DEBUG) console.error('[3] Typing room code...');
|
||||||
|
|
||||||
|
// Type room code character by character (with delay to trigger validation)
|
||||||
|
await page.click('input[placeholder*="ENTER 4-LETTER CODE"]');
|
||||||
|
await page.type('input[placeholder*="ENTER 4-LETTER CODE"]', roomCode, { delay: 100 });
|
||||||
|
|
||||||
|
// Wait for room validation to complete (look for loader success message)
|
||||||
|
if (process.env.DEBUG) console.error('[3.5] Waiting for room validation...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
// Type name character by character - this will enable the Play button
|
||||||
|
if (process.env.DEBUG) console.error('[3.6] Typing name...');
|
||||||
|
await page.click('input[placeholder*="ENTER YOUR NAME"]');
|
||||||
|
await page.type('input[placeholder*="ENTER YOUR NAME"]', 'Observer', { delay: 100 });
|
||||||
|
|
||||||
|
// Wait a moment for button to fully enable
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
const fieldValues = await page.evaluate(() => {
|
||||||
|
const roomInput = document.querySelector('input[placeholder*="ENTER 4-LETTER CODE"]');
|
||||||
|
const nameInput = document.querySelector('input[placeholder*="ENTER YOUR NAME"]');
|
||||||
|
return {
|
||||||
|
roomCode: roomInput ? roomInput.value : 'NOT FOUND',
|
||||||
|
name: nameInput ? nameInput.value : 'NOT FOUND'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.error('[3.5] Field values:', fieldValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the Play or RECONNECT button (case-insensitive, not disabled)
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
const buttonInfo = await page.evaluate(() => {
|
||||||
|
const buttons = Array.from(document.querySelectorAll('button'));
|
||||||
|
const allButtons = buttons.map(b => ({
|
||||||
|
text: b.textContent.trim(),
|
||||||
|
disabled: b.disabled,
|
||||||
|
visible: b.offsetParent !== null
|
||||||
|
}));
|
||||||
|
const actionBtn = buttons.find(b => {
|
||||||
|
const text = b.textContent.toUpperCase();
|
||||||
|
return (text.includes('PLAY') || text.includes('RECONNECT')) && !b.disabled;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
allButtons,
|
||||||
|
found: actionBtn ? actionBtn.textContent.trim() : 'NOT FOUND'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
console.error('[4] All buttons:', JSON.stringify(buttonInfo.allButtons, null, 2));
|
||||||
|
console.error('[4] Target button:', buttonInfo.found);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.DEBUG) console.error('[5] Clicking Play/Reconnect (even if disabled)...');
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const buttons = Array.from(document.querySelectorAll('button'));
|
||||||
|
const actionBtn = buttons.find(b => {
|
||||||
|
const text = b.textContent.toUpperCase();
|
||||||
|
return text.includes('PLAY') || text.includes('RECONNECT');
|
||||||
|
});
|
||||||
|
if (actionBtn) {
|
||||||
|
// Remove disabled attribute and click
|
||||||
|
actionBtn.disabled = false;
|
||||||
|
actionBtn.click();
|
||||||
|
} else {
|
||||||
|
throw new Error('Could not find PLAY or RECONNECT button');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for navigation/lobby to load
|
||||||
|
if (process.env.DEBUG) console.error('[6] Waiting for lobby (5 seconds)...');
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||||
|
|
||||||
|
// Check if we're in the lobby
|
||||||
|
const pageText = await page.evaluate(() => document.body.innerText);
|
||||||
|
const inLobby = pageText.includes('Sit back') || pageText.includes('relax');
|
||||||
|
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error('[7] In lobby:', inLobby);
|
||||||
|
console.error('[7] Page text sample:', pageText.substring(0, 100));
|
||||||
|
console.error('[7] Page URL:', page.url());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for WebSocket message
|
||||||
|
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, 500));
|
||||||
|
if (process.env.DEBUG && i % 4 === 0) {
|
||||||
|
console.error(`[8.${i}] Still waiting...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerCount === null) {
|
||||||
|
throw new Error('Could not get player count');
|
||||||
|
}
|
||||||
|
|
||||||
|
return playerCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomCode = process.argv[2];
|
||||||
|
if (!roomCode) {
|
||||||
|
console.error('Usage: node jackbox-count-v2.js <ROOM_CODE>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerCount(roomCode.toUpperCase())
|
||||||
|
.then(count => {
|
||||||
|
console.log(count);
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
184
scripts/jackbox-count-v3.js
Executable file
184
scripts/jackbox-count-v3.js
Executable file
@@ -0,0 +1,184 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const puppeteer = require('puppeteer');
|
||||||
|
|
||||||
|
async function getPlayerCount(roomCode) {
|
||||||
|
const browser = await puppeteer.launch({
|
||||||
|
headless: 'new',
|
||||||
|
args: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
// Set a realistic user agent to avoid bot detection
|
||||||
|
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;
|
||||||
|
let roomValidated = false;
|
||||||
|
|
||||||
|
// Monitor network requests to see if API call happens
|
||||||
|
page.on('response', async (response) => {
|
||||||
|
const url = response.url();
|
||||||
|
if (url.includes('ecast.jackboxgames.com/api/v2/rooms')) {
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error('[NETWORK] Room API called:', url, 'Status:', response.status());
|
||||||
|
}
|
||||||
|
if (response.status() === 200) {
|
||||||
|
roomValidated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for console messages that contain the client/welcome WebSocket message
|
||||||
|
page.on('console', async msg => {
|
||||||
|
try {
|
||||||
|
const args = msg.args();
|
||||||
|
for (const arg of args) {
|
||||||
|
const val = await arg.jsonValue();
|
||||||
|
const str = typeof val === 'object' ? JSON.stringify(val) : String(val);
|
||||||
|
|
||||||
|
// Debug: log all console messages that might be relevant
|
||||||
|
if (process.env.DEBUG && (str.includes('welcome') || str.includes('here') || str.includes('opcode'))) {
|
||||||
|
console.error('[CONSOLE]', str.substring(0, 200));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for the client/welcome message with player data
|
||||||
|
if (str.includes('client/welcome')) {
|
||||||
|
try {
|
||||||
|
let data;
|
||||||
|
if (typeof val === 'object') {
|
||||||
|
data = val;
|
||||||
|
} else {
|
||||||
|
// The string might be "recv <- {...}" so extract the JSON part
|
||||||
|
const jsonStart = str.indexOf('{');
|
||||||
|
if (jsonStart !== -1) {
|
||||||
|
const jsonStr = str.substring(jsonStart);
|
||||||
|
data = JSON.parse(jsonStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.opcode === 'client/welcome' && data.result) {
|
||||||
|
// Look for the "here" object which contains all connected players
|
||||||
|
if (data.result.here) {
|
||||||
|
playerCount = Object.keys(data.result.here).length;
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error('[SUCCESS] Found player count:', playerCount);
|
||||||
|
}
|
||||||
|
} else if (process.env.DEBUG) {
|
||||||
|
console.error('[DEBUG] client/welcome found but no "here" object. Keys:', Object.keys(data.result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error('[PARSE ERROR]', e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (process.env.DEBUG) console.error('[1] Navigating to jackbox.tv...');
|
||||||
|
await page.goto('https://jackbox.tv/', { waitUntil: 'networkidle2', timeout: 30000 });
|
||||||
|
|
||||||
|
// Wait for the room code input to be ready
|
||||||
|
if (process.env.DEBUG) console.error('[2] Waiting for form...');
|
||||||
|
await page.waitForSelector('input#roomcode', { timeout: 10000 });
|
||||||
|
|
||||||
|
// Type the room code using the input ID (more reliable)
|
||||||
|
// Use the element.type() method which properly triggers React events
|
||||||
|
if (process.env.DEBUG) console.error('[3] Typing room code:', roomCode);
|
||||||
|
const roomInput = await page.$('input#roomcode');
|
||||||
|
await roomInput.type(roomCode.toUpperCase(), { delay: 50 }); // Reduced delay from 100ms to 50ms
|
||||||
|
|
||||||
|
// Wait for room validation (the app info appears after validation)
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error('[4] Waiting for room validation...');
|
||||||
|
const roomValue = await page.evaluate(() => document.querySelector('input#roomcode').value);
|
||||||
|
console.error('[4] Room code value:', roomValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actually wait for the validation to complete - the game name label appears
|
||||||
|
try {
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const labels = Array.from(document.querySelectorAll('div, span, label'));
|
||||||
|
return labels.some(el => {
|
||||||
|
const text = el.textContent;
|
||||||
|
return text.includes('Trivia') || text.includes('Party') || text.includes('Quiplash') ||
|
||||||
|
text.includes('Fibbage') || text.includes('Drawful') || text.includes('Murder');
|
||||||
|
});
|
||||||
|
}, { timeout: 5000 });
|
||||||
|
if (process.env.DEBUG) console.error('[4.5] Room validated successfully!');
|
||||||
|
} catch (e) {
|
||||||
|
if (process.env.DEBUG) console.error('[4.5] Room validation timeout - continuing anyway...');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type the name using the input ID
|
||||||
|
// This will trigger the input event that enables the Play button
|
||||||
|
if (process.env.DEBUG) console.error('[5] Typing name...');
|
||||||
|
const nameInput = await page.$('input#username');
|
||||||
|
await nameInput.type('Observer', { delay: 30 }); // Reduced delay from 100ms to 30ms
|
||||||
|
|
||||||
|
// Wait a moment for the button to enable and click immediately
|
||||||
|
if (process.env.DEBUG) console.error('[6] Waiting for Play button...');
|
||||||
|
await page.waitForFunction(() => {
|
||||||
|
const buttons = Array.from(document.querySelectorAll('button'));
|
||||||
|
const playBtn = buttons.find(b => {
|
||||||
|
const text = b.textContent.toUpperCase();
|
||||||
|
return (text.includes('PLAY') || text.includes('RECONNECT')) && !b.disabled;
|
||||||
|
});
|
||||||
|
return playBtn !== undefined;
|
||||||
|
}, { timeout: 5000 }); // Reduced timeout from 10s to 5s
|
||||||
|
|
||||||
|
if (process.env.DEBUG) console.error('[7] Clicking Play...');
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const buttons = Array.from(document.querySelectorAll('button'));
|
||||||
|
const playBtn = buttons.find(b => {
|
||||||
|
const text = b.textContent.toUpperCase();
|
||||||
|
return (text.includes('PLAY') || text.includes('RECONNECT')) && !b.disabled;
|
||||||
|
});
|
||||||
|
if (playBtn) {
|
||||||
|
playBtn.click();
|
||||||
|
} else {
|
||||||
|
throw new Error('Play button not found or still disabled');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for the WebSocket player count message (up to 5 seconds)
|
||||||
|
if (process.env.DEBUG) console.error('[8] Waiting for player count message...');
|
||||||
|
for (let i = 0; i < 10 && playerCount === null; i++) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playerCount === null) {
|
||||||
|
throw new Error('Could not get player count from WebSocket');
|
||||||
|
}
|
||||||
|
|
||||||
|
return playerCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main
|
||||||
|
const roomCode = process.argv[2];
|
||||||
|
if (!roomCode) {
|
||||||
|
console.error('Usage: node jackbox-count-v3.js <ROOM_CODE>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlayerCount(roomCode.toUpperCase())
|
||||||
|
.then(count => {
|
||||||
|
console.log(count);
|
||||||
|
process.exit(0);
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Error:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
190
scripts/jackbox-count.js
Executable file
190
scripts/jackbox-count.js
Executable file
@@ -0,0 +1,190 @@
|
|||||||
|
#!/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);
|
||||||
|
});
|
||||||
112
scripts/jackbox-player-count.go
Normal file
112
scripts/jackbox-player-count.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Println("Usage: go run jackbox-player-count.go <ROOM_CODE>")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
roomCode := strings.ToUpper(os.Args[1])
|
||||||
|
count, err := getPlayerCount(roomCode)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("%d\n", count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPlayerCount(roomCode string) (int, error) {
|
||||||
|
// Suppress cookie errors
|
||||||
|
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||||
|
chromedp.Flag("headless", true),
|
||||||
|
)
|
||||||
|
|
||||||
|
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ctx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(func(s string, i ...interface{}) {
|
||||||
|
msg := fmt.Sprintf(s, i...)
|
||||||
|
if !strings.Contains(msg, "cookie") {
|
||||||
|
log.Printf(msg)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var playerCount int
|
||||||
|
|
||||||
|
err := chromedp.Run(ctx,
|
||||||
|
chromedp.Navigate("https://jackbox.tv/"),
|
||||||
|
chromedp.WaitVisible(`input[placeholder*="ENTER 4-LETTER CODE"]`),
|
||||||
|
|
||||||
|
// Inject WebSocket hook BEFORE joining
|
||||||
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
return chromedp.Evaluate(`
|
||||||
|
window.__jackboxPlayerCount = null;
|
||||||
|
const OriginalWebSocket = window.WebSocket;
|
||||||
|
window.WebSocket = function(...args) {
|
||||||
|
const ws = new OriginalWebSocket(...args);
|
||||||
|
const originalAddEventListener = ws.addEventListener;
|
||||||
|
ws.addEventListener = function(type, listener, ...rest) {
|
||||||
|
if (type === 'message') {
|
||||||
|
const wrappedListener = function(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.opcode === 'client/welcome' && data.result && data.result.here) {
|
||||||
|
window.__jackboxPlayerCount = Object.keys(data.result.here).length;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
return listener.call(this, event);
|
||||||
|
};
|
||||||
|
return originalAddEventListener.call(this, type, wrappedListener, ...rest);
|
||||||
|
}
|
||||||
|
return originalAddEventListener.call(this, type, listener, ...rest);
|
||||||
|
};
|
||||||
|
return ws;
|
||||||
|
};
|
||||||
|
`, nil).Do(ctx)
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Now join
|
||||||
|
chromedp.SendKeys(`input[placeholder*="ENTER 4-LETTER CODE"]`, roomCode+"\n\n"),
|
||||||
|
|
||||||
|
// Poll for the value
|
||||||
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
for i := 0; i < 60; i++ { // Try for 30 seconds
|
||||||
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
var count int
|
||||||
|
err := chromedp.Evaluate(`window.__jackboxPlayerCount || -1`, &count).Do(ctx)
|
||||||
|
if err == nil && count > 0 {
|
||||||
|
playerCount = count
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("timeout waiting for player count")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if playerCount < 0 {
|
||||||
|
return 0, fmt.Errorf("could not find player count")
|
||||||
|
}
|
||||||
|
|
||||||
|
return playerCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
1127
scripts/package-lock.json
generated
Normal file
1127
scripts/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
scripts/package.json
Normal file
16
scripts/package.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"name": "jackbox-scripts",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Utility scripts for Jackbox game integration",
|
||||||
|
"main": "jackbox-count.js",
|
||||||
|
"scripts": {
|
||||||
|
"count": "node jackbox-count.js"
|
||||||
|
},
|
||||||
|
"keywords": ["jackbox", "websocket", "player-count"],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"puppeteer": "^24.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user