244 lines
7.1 KiB
Go
244 lines
7.1 KiB
Go
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),
|
|
})
|
|
}
|