package auth import ( "encoding/json" "fmt" "io" "net/http" "strconv" "strings" "time" ) // GateStatus is what SE's server-status endpoint says about the login gate. type GateStatus struct { Up bool Message string // localized notice published while the gate is closed, lines joined with \n News string // optional news lines published alongside the notice } // CheckGate asks frontier.ffxiv.com whether the game servers are accepting // logins. FFXIVQuickLauncher does this before login and again right before it // launches the game; we use its richer endpoint (gate_status.json, which // carries a localized maintenance message) for both moments. lang is one of // the client language codes: "ja", "en", "de" or "fr". func CheckGate(lang string) (*GateStatus, error) { u := "https://frontier.ffxiv.com/worldStatus/gate_status.json?lang=" + lang + "&_=" + strconv.FormatInt(time.Now().UnixMilli(), 10) req, err := http.NewRequest(http.MethodGet, u, nil) if err != nil { return nil, err } setCommonHeaders(req, frontierReferer()) resp, err := httpClient.Do(req) if err != nil { return nil, fmt.Errorf("gate status: %w", err) } defer resp.Body.Close() body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("gate status: HTTP %d", resp.StatusCode) } return parseGateStatus(body) } // SE answers {"status":1} while the gate is open — a number, not a boolean. // Json.NET coerces that for QL; we accept both shapes here. type rawGate struct { Status any `json:"status"` Message []string `json:"message"` News []string `json:"news"` } func parseGateStatus(body []byte) (*GateStatus, error) { var r rawGate if err := json.Unmarshal(body, &r); err != nil { return nil, fmt.Errorf("gate status: %w", err) } gs := &GateStatus{ Message: strings.Join(r.Message, "\n"), News: strings.Join(r.News, "\n"), } switch s := r.Status.(type) { case bool: gs.Up = s case float64: gs.Up = s != 0 default: return nil, fmt.Errorf("gate status: unrecognized status value %v", r.Status) } return gs, nil }