Files
jackboxpartypack-gamepicker/scripts/get-player-count.go
2025-11-03 13:57:26 -05:00

395 lines
11 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/chromedp"
)
// PlayerInfo represents a player in the lobby
type PlayerInfo struct {
ID string `json:"id"`
Role string `json:"role"`
Name string `json:"name"`
}
// LobbyStatus contains all information about the current lobby
type LobbyStatus struct {
RoomCode string `json:"roomCode"`
AppTag string `json:"appTag"`
PlayerCount int `json:"playerCount"`
AudienceCount int `json:"audienceCount"`
MaxPlayers int `json:"maxPlayers"`
GameState string `json:"gameState"`
LobbyState string `json:"lobbyState"`
Locked bool `json:"locked"`
Full bool `json:"full"`
Players []PlayerInfo `json:"players"`
}
// WebSocketMessage represents a parsed WebSocket message
type WebSocketMessage struct {
PC int `json:"pc"`
Opcode string `json:"opcode"`
Result map[string]interface{} `json:"result"`
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: go run get-player-count.go <ROOM_CODE>")
fmt.Println("Example: go run get-player-count.go JYET")
fmt.Println("\nSet DEBUG=true for verbose output:")
fmt.Println("DEBUG=true go run get-player-count.go JYET")
os.Exit(1)
}
roomCode := strings.ToUpper(strings.TrimSpace(os.Args[1]))
if len(roomCode) != 4 {
fmt.Println("Error: Room code must be exactly 4 characters")
os.Exit(1)
}
fmt.Printf("🎮 Jackbox Player Count Fetcher\n")
fmt.Printf("Room Code: %s\n\n", roomCode)
status, err := getPlayerCount(roomCode)
if err != nil {
log.Fatalf("Error: %v\n", err)
}
printStatus(status)
}
func getPlayerCount(roomCode string) (*LobbyStatus, error) {
// Create chrome context with less verbose logging
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", true),
chromedp.Flag("disable-gpu", true),
chromedp.Flag("no-sandbox", true),
chromedp.Flag("disable-web-security", true),
)
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
// Create context without default logging to reduce cookie errors
ctx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(func(s string, i ...interface{}) {
// Only log non-cookie errors
msg := fmt.Sprintf(s, i...)
if !strings.Contains(msg, "cookiePart") && !strings.Contains(msg, "could not unmarshal") {
log.Printf(msg)
}
}))
defer cancel()
// Set timeout
ctx, cancel = context.WithTimeout(ctx, 30*time.Second)
defer cancel()
var lobbyStatus *LobbyStatus
welcomeMessageFound := false
wsMessages := make([]string, 0)
// Listen for WebSocket frames - this is the most reliable method
debugMode := os.Getenv("DEBUG") == "true"
chromedp.ListenTarget(ctx, func(ev interface{}) {
if debugMode {
fmt.Printf("[DEBUG] Event type: %T\n", ev)
}
switch ev := ev.(type) {
case *network.EventWebSocketCreated:
if debugMode {
fmt.Printf("[DEBUG] WebSocket Created: %s\n", ev.URL)
}
case *network.EventWebSocketFrameReceived:
// Capture all WebSocket frames
wsMessages = append(wsMessages, ev.Response.PayloadData)
if debugMode {
fmt.Printf("[DEBUG] WS Frame received (%d bytes)\n", len(ev.Response.PayloadData))
if len(ev.Response.PayloadData) < 200 {
fmt.Printf("[DEBUG] Data: %s\n", ev.Response.PayloadData)
} else {
fmt.Printf("[DEBUG] Data (truncated): %s...\n", ev.Response.PayloadData[:200])
}
}
// Try to parse immediately
var wsMsg WebSocketMessage
if err := json.Unmarshal([]byte(ev.Response.PayloadData), &wsMsg); err == nil {
if debugMode {
fmt.Printf("[DEBUG] Parsed opcode: %s\n", wsMsg.Opcode)
}
if wsMsg.Opcode == "client/welcome" && wsMsg.Result != nil {
lobbyStatus = parseWelcomeMessage(&wsMsg)
welcomeMessageFound = true
fmt.Println("✓ Captured lobby data from WebSocket")
}
} else if debugMode {
fmt.Printf("[DEBUG] Failed to parse JSON: %v\n", err)
}
case *network.EventWebSocketFrameSent:
if debugMode {
fmt.Printf("[DEBUG] WS Frame sent: %s\n", ev.Response.PayloadData)
}
}
})
fmt.Println("⏳ Navigating to jackbox.tv...")
// Enable network tracking BEFORE navigation
if err := chromedp.Run(ctx, network.Enable()); err != nil {
return nil, fmt.Errorf("failed to enable network tracking: %w", err)
}
err := chromedp.Run(ctx,
chromedp.Navigate("https://jackbox.tv/"),
chromedp.WaitVisible(`input[placeholder*="ENTER 4-LETTER CODE"]`, chromedp.ByQuery),
)
if err != nil {
return nil, fmt.Errorf("failed to load jackbox.tv: %w", err)
}
fmt.Printf("✓ Loaded jackbox.tv\n")
fmt.Printf("⏳ Joining room %s...\n", roomCode)
// Type room code and press Enter to join
err = chromedp.Run(ctx,
chromedp.Focus(`input[placeholder*="ENTER 4-LETTER CODE"]`, chromedp.ByQuery),
chromedp.SendKeys(`input[placeholder*="ENTER 4-LETTER CODE"]`, roomCode+"\n", chromedp.ByQuery),
)
if err != nil {
return nil, fmt.Errorf("failed to enter room code: %w", err)
}
if debugMode {
fmt.Println("[DEBUG] Entered room code and pressed Enter")
}
// Wait for room code validation and page transition
time.Sleep(2 * time.Second)
fmt.Println("✓ Clicked Play button, waiting for WebSocket data...")
// Check if we successfully joined (look for typical lobby UI elements)
time.Sleep(2 * time.Second)
var pageText string
err = chromedp.Run(ctx,
chromedp.Text("body", &pageText, chromedp.ByQuery),
)
if err == nil && debugMode {
if strings.Contains(pageText, "Sit back") || strings.Contains(pageText, "waiting") {
fmt.Println("[DEBUG] Successfully joined lobby (found lobby text)")
} else {
fmt.Printf("[DEBUG] Page text: %s\n", pageText[:min(300, len(pageText))])
}
}
// Wait longer for WebSocket to connect and receive welcome message
for i := 0; i < 15 && !welcomeMessageFound; i++ {
time.Sleep(500 * time.Millisecond)
if i%4 == 0 {
fmt.Printf("⏳ Waiting for lobby data... (%ds)\n", i/2)
}
}
// If we still didn't get it from WebSocket frames, try parsing all captured messages
if !welcomeMessageFound && len(wsMessages) > 0 {
fmt.Printf("⏳ Parsing %d captured WebSocket messages...\n", len(wsMessages))
for _, msg := range wsMessages {
var wsMsg WebSocketMessage
if err := json.Unmarshal([]byte(msg), &wsMsg); err == nil {
if wsMsg.Opcode == "client/welcome" && wsMsg.Result != nil {
lobbyStatus = parseWelcomeMessage(&wsMsg)
welcomeMessageFound = true
fmt.Println("✓ Found lobby data in captured messages")
break
}
}
}
}
if lobbyStatus == nil {
if len(wsMessages) == 0 {
return nil, fmt.Errorf("no WebSocket messages captured - connection may have failed\nTry running with DEBUG=true for more details")
}
// Show what we captured
fmt.Printf("\n⚠ Captured %d WebSocket messages but couldn't find 'client/welcome'\n", len(wsMessages))
fmt.Println("\nMessage types found:")
opcodes := make(map[string]int)
for _, msg := range wsMessages {
var wsMsg WebSocketMessage
if err := json.Unmarshal([]byte(msg), &wsMsg); err == nil {
opcodes[wsMsg.Opcode]++
}
}
for opcode, count := range opcodes {
fmt.Printf(" - %s: %d\n", opcode, count)
}
return nil, fmt.Errorf("could not find player count data in WebSocket messages\nRoom may be invalid, closed, or not in lobby state")
}
lobbyStatus.RoomCode = roomCode
// Fetch additional room info from REST API
if err := enrichWithRestAPI(lobbyStatus); err != nil {
fmt.Printf("Warning: Could not fetch additional room info: %v\n", err)
}
return lobbyStatus, nil
}
func parseWelcomeMessage(msg *WebSocketMessage) *LobbyStatus {
status := &LobbyStatus{
Players: []PlayerInfo{},
}
// Parse "here" object for players
if here, ok := msg.Result["here"].(map[string]interface{}); ok {
status.PlayerCount = len(here)
for id, playerData := range here {
if pd, ok := playerData.(map[string]interface{}); ok {
player := PlayerInfo{ID: id}
if roles, ok := pd["roles"].(map[string]interface{}); ok {
if _, hasHost := roles["host"]; hasHost {
player.Role = "host"
player.Name = "Host"
} else if playerRole, ok := roles["player"].(map[string]interface{}); ok {
player.Role = "player"
if name, ok := playerRole["name"].(string); ok {
player.Name = name
} else {
player.Name = "Unknown"
}
}
}
status.Players = append(status.Players, player)
}
}
}
// Parse entities for additional info
if entities, ok := msg.Result["entities"].(map[string]interface{}); ok {
// Audience count
if audience, ok := entities["audience"].([]interface{}); ok && len(audience) > 1 {
if audienceData, ok := audience[1].(map[string]interface{}); ok {
if count, ok := audienceData["count"].(float64); ok {
status.AudienceCount = int(count)
}
}
}
// Room state
if bcRoom, ok := entities["bc:room"].([]interface{}); ok && len(bcRoom) > 1 {
if roomData, ok := bcRoom[1].(map[string]interface{}); ok {
if val, ok := roomData["val"].(map[string]interface{}); ok {
if gameState, ok := val["state"].(string); ok {
status.GameState = gameState
}
if lobbyState, ok := val["lobbyState"].(string); ok {
status.LobbyState = lobbyState
}
}
}
}
}
return status
}
func enrichWithRestAPI(status *LobbyStatus) error {
// Fetch additional room info from REST API
url := fmt.Sprintf("https://ecast.jackboxgames.com/api/v2/rooms/%s", status.RoomCode)
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var result struct {
OK bool `json:"ok"`
Body struct {
AppTag string `json:"appTag"`
MaxPlayers int `json:"maxPlayers"`
Locked bool `json:"locked"`
Full bool `json:"full"`
} `json:"body"`
}
if err := json.Unmarshal(body, &result); err != nil {
return err
}
if result.OK {
status.AppTag = result.Body.AppTag
status.MaxPlayers = result.Body.MaxPlayers
status.Locked = result.Body.Locked
status.Full = result.Body.Full
}
return nil
}
func printStatus(status *LobbyStatus) {
fmt.Println()
fmt.Println("═══════════════════════════════════════════")
fmt.Println(" Jackbox Room Status")
fmt.Println("═══════════════════════════════════════════")
fmt.Println()
fmt.Printf("Room Code: %s\n", status.RoomCode)
fmt.Printf("Game: %s\n", status.AppTag)
fmt.Printf("Game State: %s\n", status.GameState)
fmt.Printf("Lobby State: %s\n", status.LobbyState)
fmt.Printf("Locked: %t\n", status.Locked)
fmt.Printf("Full: %t\n", status.Full)
fmt.Println()
fmt.Printf("Players: %d / %d\n", status.PlayerCount, status.MaxPlayers)
fmt.Printf("Audience: %d\n", status.AudienceCount)
fmt.Println()
if len(status.Players) > 0 {
fmt.Println("Current Players:")
for i, player := range status.Players {
fmt.Printf(" %d. %s (%s)\n", i+1, player.Name, player.Role)
}
fmt.Println()
}
fmt.Println("═══════════════════════════════════════════")
fmt.Println()
}