package web import ( "errors" "net/http" "slices" "time" "git.zio.sh/cs2/simpleadmin-web/internal/live" "git.zio.sh/cs2/simpleadmin-web/internal/steam" "git.zio.sh/cs2/simpleadmin-web/internal/store" ) // publicPenalty is a penalty with everything about staff and IPs removed. type publicPenalty struct { ID int64 `json:"id"` Kind string `json:"kind"` Type string `json:"type,omitempty"` Name string `json:"name"` SteamID string `json:"steamid"` Reason string `json:"reason"` Duration int `json:"duration"` Passed *int `json:"passed,omitempty"` Ends *time.Time `json:"ends,omitempty"` Created time.Time `json:"created"` Status string `json:"status"` } func toPublic(ps []store.Penalty) []publicPenalty { out := make([]publicPenalty, 0, len(ps)) for _, p := range ps { out = append(out, publicPenalty{ID: p.ID, Kind: p.Kind, Type: p.Type, Name: p.Name, SteamID: p.SteamID, Reason: p.Reason, Duration: p.Duration, Passed: p.Passed, Ends: p.Ends, Created: p.Created, Status: p.Status}) } return out } type livePlayer struct { live.Player Rank *rankView `json:"rank,omitempty"` // Staff-only fields, left empty on the public page. Tags []string `json:"tags,omitempty"` CanTarget bool `json:"canTarget,omitempty"` } type serverView struct { Online bool `json:"online"` Hostname string `json:"hostname"` Map string `json:"map"` Count int `json:"count"` MaxPlayers int `json:"maxPlayers"` ScoreT int `json:"scoreT"` ScoreCT int `json:"scoreCt"` Warmup bool `json:"warmup"` StaffOnline int `json:"staffOnline"` Address string `json:"address"` Players []livePlayer `json:"players"` Updated time.Time `json:"updated"` } func (s *Server) serverView(x *staffIndex, st live.Status) serverView { v := serverView{Online: st.Online, Hostname: st.Hostname, Map: st.Map, MaxPlayers: st.MaxPlayers, ScoreT: st.ScoreT, ScoreCT: st.ScoreCT, Warmup: st.Warmup, Address: s.cfg.ServerAddress, Players: []livePlayer{}, Updated: st.Updated} for _, p := range st.Players { lp := livePlayer{Player: p} if !p.Bot { v.Count++ lp.Rank = s.rankOf(x, p.SteamID) if s.isStaff(x, p.SteamID) { v.StaffOnline++ } } v.Players = append(v.Players, lp) } return v } func (s *Server) publicServer(w http.ResponseWriter, r *http.Request) { x, err := s.staffIdx(r.Context()) if err != nil { internal(w, r, err) return } writeJSON(w, http.StatusOK, s.serverView(x, s.poll.Current())) } // penaltyQuery reads ?status=&q=&page= (and for comms, a type in place of the status). func penaltyQuery(r *http.Request, comms bool) (store.Query, int) { lim, off, num := page(r) q := store.Query{Search: r.URL.Query().Get("q"), Limit: lim, Offset: off} switch st := r.URL.Query().Get("status"); st { case store.StatusActive, store.StatusExpired, store.StatusLifted: q.Status = st case "GAG", "MUTE", "SILENCE": if comms { q.Status, q.Type = store.StatusActive, st } case "all": default: q.Status = store.StatusActive } return q, num } type penaltyPage struct { list[publicPenalty] Active int `json:"active"` } func (s *Server) publicBans(w http.ResponseWriter, r *http.Request) { q, num := penaltyQuery(r, false) items, total, err := s.st.Bans(r.Context(), q) if err != nil { internal(w, r, err) return } _, active, err := s.st.Bans(r.Context(), store.Query{Status: store.StatusActive, Limit: 1}) if err != nil { internal(w, r, err) return } writeJSON(w, http.StatusOK, penaltyPage{newList(toPublic(items), total, num), active}) } func (s *Server) publicComms(w http.ResponseWriter, r *http.Request) { q, num := penaltyQuery(r, true) items, total, err := s.st.Comms(r.Context(), q) if err != nil { internal(w, r, err) return } _, active, err := s.st.Comms(r.Context(), store.Query{Status: store.StatusActive, Limit: 1}) if err != nil { internal(w, r, err) return } writeJSON(w, http.StatusOK, penaltyPage{newList(toPublic(items), total, num), active}) } type memberView struct { SteamID string `json:"steamid"` Name string `json:"name"` Online bool `json:"online"` Ends *time.Time `json:"ends,omitempty"` LastSeen *time.Time `json:"lastSeen,omitempty"` } type groupView struct { ID int64 `json:"id"` Name string `json:"name"` Label string `json:"label"` Color string `json:"color"` About string `json:"about,omitempty"` Supporter bool `json:"supporter"` Immunity int `json:"immunity,omitempty"` Flags []string `json:"flags,omitempty"` Members []memberView `json:"members"` } // groupViews lists groups by immunity with their members. Staff get flags and expiry dates. func (s *Server) groupViews(x *staffIndex, st live.Status, forStaff bool) []groupView { out := make([]groupView, 0, len(x.groups)) for i, g := range x.groups { r := s.site.rank(g.Name, i) gv := groupView{ID: g.ID, Name: g.Name, Label: r.Label, Color: r.Color, About: r.About, Supporter: r.Supporter, Immunity: g.Immunity, Members: []memberView{}} if forStaff { gv.Flags = g.Flags } else { gv.Immunity = 0 } for _, a := range x.admins { if x.primaryGroup(&a) != g.Name { continue } _, online := st.Find(a.SteamID) m := memberView{SteamID: a.SteamID, Name: a.Name, Online: online} if forStaff { m.Ends = a.Ends } gv.Members = append(gv.Members, m) } slices.SortStableFunc(gv.Members, func(a, b memberView) int { if a.Online != b.Online { if a.Online { return -1 } return 1 } return 0 }) out = append(out, gv) } return out } func (s *Server) publicStaff(w http.ResponseWriter, r *http.Request) { x, err := s.staffIdx(r.Context()) if err != nil { internal(w, r, err) return } st := s.poll.Current() groups := s.groupViews(x, st, false) // Hide empty ranks from the public. groups = slices.DeleteFunc(groups, func(g groupView) bool { return len(g.Members) == 0 }) writeJSON(w, http.StatusOK, map[string]any{"groups": groups, "staffOnline": s.serverView(x, st).StaffOnline}) } func (s *Server) publicPlayer(w http.ResponseWriter, r *http.Request) { steamid := r.PathValue("steamid") if !steam.ValidID(steamid) { fail(w, http.StatusBadRequest, "That isn't a SteamID64.") return } d, err := s.st.Player(r.Context(), steamid) if errors.Is(err, store.ErrNotFound) { d = store.PlayerDetail{SteamID: steamid} } else if err != nil { internal(w, r, err) return } bans, _, err := s.st.Bans(r.Context(), store.Query{SteamID: steamid, Limit: 200}) if err != nil { internal(w, r, err) return } comms, _, err := s.st.Comms(r.Context(), store.Query{SteamID: steamid, Limit: 200}) if err != nil { internal(w, r, err) return } x, err := s.staffIdx(r.Context()) if err != nil { internal(w, r, err) return } _, online := s.poll.Current().Find(steamid) writeJSON(w, http.StatusOK, map[string]any{ "steamid": steamid, "name": d.Name, "online": online, "rank": s.rankOf(x, steamid), "bans": toPublic(bans), "comms": toPublic(comms), }) }