Files
jackboxpartypack-gamepicker/scripts/get-player-count.go

395 lines
11 KiB
Go
Raw Normal View History

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()
}