395 lines
11 KiB
Go
395 lines
11 KiB
Go
|
|
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()
|
|||
|
|
}
|
|||
|
|
|