From 140988d01d57bbc7c3470586077ee304fba45ace Mon Sep 17 00:00:00 2001 From: cottongin Date: Mon, 3 Nov 2025 13:57:26 -0500 Subject: [PATCH] initial actual player count implementation --- README.md | 36 + scripts/README.md | 230 ++++++ scripts/TESTING.md | 184 +++++ scripts/get-jackbox-player-count.js | 262 +++++++ scripts/get-player-count.go | 394 ++++++++++ scripts/get-player-count.html | 468 +++++++++++ scripts/go.mod | 18 + scripts/go.sum | 23 + scripts/jackbox-count-v2.js | 184 +++++ scripts/jackbox-count-v3.js | 184 +++++ scripts/jackbox-count.js | 190 +++++ scripts/jackbox-player-count.go | 112 +++ scripts/package-lock.json | 1127 +++++++++++++++++++++++++++ scripts/package.json | 16 + 14 files changed, 3428 insertions(+) create mode 100644 scripts/README.md create mode 100644 scripts/TESTING.md create mode 100644 scripts/get-jackbox-player-count.js create mode 100644 scripts/get-player-count.go create mode 100644 scripts/get-player-count.html create mode 100644 scripts/go.mod create mode 100644 scripts/go.sum create mode 100644 scripts/jackbox-count-v2.js create mode 100755 scripts/jackbox-count-v3.js create mode 100755 scripts/jackbox-count.js create mode 100644 scripts/jackbox-player-count.go create mode 100644 scripts/package-lock.json create mode 100644 scripts/package.json diff --git a/README.md b/README.md index c1ce688..d1240bf 100644 --- a/README.md +++ b/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 - 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 ### games diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..f93c9f5 --- /dev/null +++ b/scripts/README.md @@ -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 + diff --git a/scripts/TESTING.md b/scripts/TESTING.md new file mode 100644 index 0000000..ba62535 --- /dev/null +++ b/scripts/TESTING.md @@ -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 + diff --git a/scripts/get-jackbox-player-count.js b/scripts/get-jackbox-player-count.js new file mode 100644 index 0000000..dd9e41f --- /dev/null +++ b/scripts/get-jackbox-player-count.js @@ -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 + * 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 `); + 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 +}; + diff --git a/scripts/get-player-count.go b/scripts/get-player-count.go new file mode 100644 index 0000000..708e184 --- /dev/null +++ b/scripts/get-player-count.go @@ -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 ") + 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() +} + diff --git a/scripts/get-player-count.html b/scripts/get-player-count.html new file mode 100644 index 0000000..8bcd93a --- /dev/null +++ b/scripts/get-player-count.html @@ -0,0 +1,468 @@ + + + + + + Jackbox Player Count Fetcher + + + +
+

šŸŽ® Jackbox Player Count Fetcher

+

Enter a room code to get real-time player information

+ +
+ + +
+ +
+ +
+
+
+
+
Players
+
-
+
+
+
Max Players
+
-
+
+
+
Audience
+
-
+
+
+ +
+ Room Code: + - +
+
+ Game: + - +
+
+ Game State: + - +
+
+ Lobby State: + - +
+
+ Locked: + - +
+
+ Full: + - +
+
+ + +
+
+ + + + + diff --git a/scripts/go.mod b/scripts/go.mod new file mode 100644 index 0000000..1fe1f4e --- /dev/null +++ b/scripts/go.mod @@ -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 +) diff --git a/scripts/go.sum b/scripts/go.sum new file mode 100644 index 0000000..035b62e --- /dev/null +++ b/scripts/go.sum @@ -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= diff --git a/scripts/jackbox-count-v2.js b/scripts/jackbox-count-v2.js new file mode 100644 index 0000000..8a4af7a --- /dev/null +++ b/scripts/jackbox-count-v2.js @@ -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 '); + process.exit(1); +} + +getPlayerCount(roomCode.toUpperCase()) + .then(count => { + console.log(count); + process.exit(0); + }) + .catch(err => { + console.error('Error:', err.message); + process.exit(1); + }); + diff --git a/scripts/jackbox-count-v3.js b/scripts/jackbox-count-v3.js new file mode 100755 index 0000000..73d501a --- /dev/null +++ b/scripts/jackbox-count-v3.js @@ -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 '); + process.exit(1); +} + +getPlayerCount(roomCode.toUpperCase()) + .then(count => { + console.log(count); + process.exit(0); + }) + .catch(err => { + console.error('Error:', err.message); + process.exit(1); + }); + diff --git a/scripts/jackbox-count.js b/scripts/jackbox-count.js new file mode 100755 index 0000000..3857c33 --- /dev/null +++ b/scripts/jackbox-count.js @@ -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 '); + 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); + }); diff --git a/scripts/jackbox-player-count.go b/scripts/jackbox-player-count.go new file mode 100644 index 0000000..0ed2541 --- /dev/null +++ b/scripts/jackbox-player-count.go @@ -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 ") + 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 +} + diff --git a/scripts/package-lock.json b/scripts/package-lock.json new file mode 100644 index 0000000..f95a519 --- /dev/null +++ b/scripts/package-lock.json @@ -0,0 +1,1127 @@ +{ + "name": "jackbox-scripts", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "jackbox-scripts", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "puppeteer": "^24.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.10.13", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.13.tgz", + "integrity": "sha512-a9Ruw3j3qlnB5a/zHRTkruppynxqaeE4H9WNj5eYGRWqw0ZauZ23f4W2ARf3hghF5doozyD+CRtt7XSYuYRI/Q==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.3", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", + "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.1.tgz", + "integrity": "sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.0.tgz", + "integrity": "sha512-GljgCjeupKZJNetTqxKaQArLK10vpmK28or0+RwWjEl5Rk+/xG3wkpmkv+WrcBm3q1BwHKlnhXzR8O37kcvkXQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", + "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "streamx": "^2.21.0" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz", + "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chromium-bidi": { + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-10.5.1.tgz", + "integrity": "sha512-rlj6OyhKhVTnk4aENcUme3Jl9h+cq4oXu4AzBcvr8RMmT6BR4a3zSNT9dbIfXr9/BS6ibzRyDhowuw4n2GgzsQ==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1521046", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz", + "integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==", + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "24.28.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.28.0.tgz", + "integrity": "sha512-KLRGFNCGmXJpocEBbEIoHJB0vNRZLQNBjl5ExXEv0z7MIU+qqVEQcfWTyat+qxPDk/wZvSf+b30cQqAfWxX0zg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.13", + "chromium-bidi": "10.5.1", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1521046", + "puppeteer-core": "24.28.0", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "24.28.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.28.0.tgz", + "integrity": "sha512-QpAqaYgeZHF5/xAZ4jAOzsU+l0Ed4EJoWkRdfw8rNqmSN7itcdYeCJaSPQ0s5Pyn/eGNC4xNevxbgY+5bzNllw==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.10.13", + "chromium-bidi": "10.5.1", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1521046", + "typed-query-selector": "^2.12.0", + "webdriver-bidi-protocol": "0.3.8", + "ws": "^8.18.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/text-decoder": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", + "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "license": "MIT", + "optional": true + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.8.tgz", + "integrity": "sha512-21Yi2GhGntMc671vNBCjiAeEVknXjVRoyu+k+9xOMShu+ZQfpGQwnBqbNz/Sv4GXZ6JmutlPAi2nIJcrymAWuQ==", + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..17604bf --- /dev/null +++ b/scripts/package.json @@ -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" + } +} +