package minecraft import ( "encoding/json" "io" "log" "net/http" "regexp" "strconv" "strings" "time" "penguins.lan/adapters/rcon" "penguins.lan/api/types" "penguins.lan/database" ) const ( queryTimeout = 3 * time.Second // staleAfter marks the cache offline if the scheduler stopped refreshing it. staleAfter = 30 * time.Second ) type Status struct { Online bool `json:"online"` Count int `json:"count"` Max int `json:"max"` Players []string `json:"players"` } var listPattern = regexp.MustCompile(`There are (\d+) of a max of (\d+) players online:?\s*(.*)`) // Query runs "list" over RCON. The scheduler calls this; requests read the cache. func Query(address, password string) (Status, error) { client, err := rcon.Dial(address, password, queryTimeout) if err != nil { return Status{}, err } defer client.Close() out, err := client.Execute("list") if err != nil { return Status{}, err } return parseList(out), nil } func parseList(out string) Status { m := listPattern.FindStringSubmatch(strings.TrimSpace(out)) if m == nil { return Status{Online: true} } count, _ := strconv.Atoi(m[1]) max, _ := strconv.Atoi(m[2]) players := []string{} for _, p := range strings.Split(m[3], ",") { if p = strings.TrimSpace(p); p != "" { players = append(players, p) } } return Status{Online: true, Count: count, Max: max, Players: players} } func StatusContinuation(context *types.RequestContext, req *http.Request, resp http.ResponseWriter) types.ContinuationChain { return func(success types.Continuation, _failure types.Continuation) types.ContinuationChain { resp.Header().Set("Content-Type", "application/json") cached, updatedAt, err := database.GetMinecraftStatus(context.DBConn) if err != nil { log.Println("minecraft: read cache failed:", err) } if cached == "" || time.Since(updatedAt) > staleAfter { json.NewEncoder(resp).Encode(Status{Online: false}) } else { io.WriteString(resp, cached) } return success(context, req, resp) } }