Public rank change log: sa_rank_log, written on rank changes, listed on the Staff page
This commit is contained in:
parent
861692c534
commit
611b79cdba
11 changed files with 346 additions and 17 deletions
140
internal/store/ranklog.go
Normal file
140
internal/store/ranklog.go
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Rank history lives in sa_rank_log, which the panel owns (sql/rank-log.sql). Our SimpleAdmin fork
|
||||
// writes the same rows for in-game changes. A player's rank is their highest-immunity group, the
|
||||
// one the staff page lists them under.
|
||||
|
||||
// Actor is who made a change. An empty SteamID is Console.
|
||||
type Actor struct {
|
||||
SteamID string
|
||||
Name string
|
||||
}
|
||||
|
||||
// Where a rank change was made, stored in sa_rank_log.source.
|
||||
const (
|
||||
SourcePanel = "panel"
|
||||
SourceGame = "game"
|
||||
)
|
||||
|
||||
// RankChange is a row of sa_rank_log.
|
||||
type RankChange struct {
|
||||
ID int64 `json:"id"`
|
||||
SteamID string `json:"steamid"`
|
||||
Name string `json:"name"`
|
||||
OldRank string `json:"oldRank,omitempty"`
|
||||
NewRank string `json:"newRank,omitempty"`
|
||||
Kind string `json:"kind"`
|
||||
AdminSteamID string `json:"adminSteamid,omitempty"`
|
||||
AdminName string `json:"admin"`
|
||||
Source string `json:"source"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
// EnsureRankLog creates sa_rank_log if it's missing.
|
||||
func (s *Store) EnsureRankLog(ctx context.Context, ddl string) error {
|
||||
_, err := s.db.ExecContext(ctx, strings.TrimSuffix(strings.TrimSpace(ddl), ";"))
|
||||
return err
|
||||
}
|
||||
|
||||
// RankChanges lists rank changes, newest first.
|
||||
func (s *Store) RankChanges(ctx context.Context, limit, offset int) ([]RankChange, int, error) {
|
||||
var total int
|
||||
if err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM sa_rank_log`).Scan(&total); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
rows, err := s.db.QueryContext(ctx, `SELECT id, player_steamid, player_name, COALESCE(old_rank,''), COALESCE(new_rank,''),
|
||||
kind, admin_steamid, admin_name, source, created
|
||||
FROM sa_rank_log ORDER BY id DESC LIMIT ? OFFSET ?`, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []RankChange{}
|
||||
for rows.Next() {
|
||||
var c RankChange
|
||||
var player, admin sql.NullInt64
|
||||
if err := rows.Scan(&c.ID, &player, &c.Name, &c.OldRank, &c.NewRank, &c.Kind, &admin, &c.AdminName, &c.Source, &c.Created); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
c.SteamID, c.AdminSteamID = sid(player), sid(admin)
|
||||
if c.AdminSteamID == "" {
|
||||
c.AdminName = "Console"
|
||||
}
|
||||
c.Created = s.fromWall(c.Created)
|
||||
out = append(out, c)
|
||||
}
|
||||
return out, total, rows.Err()
|
||||
}
|
||||
|
||||
type rankState struct {
|
||||
group string // "" for no rank
|
||||
immunity int
|
||||
}
|
||||
|
||||
// rankTx reads a player's current rank inside tx, counting only unexpired admin rows and groups
|
||||
// that apply to this server, as Admins and Groups do.
|
||||
func (s *Store) rankTx(ctx context.Context, tx *sql.Tx, n int64) (rankState, error) {
|
||||
gsCond, gsArgs := s.serverFilter("gs.server_id")
|
||||
aCond, aArgs := s.serverFilter("a.server_id")
|
||||
args := append(gsArgs, n, s.wall(time.Now()))
|
||||
args = append(args, aArgs...)
|
||||
var r rankState
|
||||
err := tx.QueryRowContext(ctx, `SELECT g.name, g.immunity
|
||||
FROM sa_admins a
|
||||
JOIN sa_admins_flags f ON f.admin_id = a.id
|
||||
JOIN sa_groups g ON g.name = f.flag
|
||||
JOIN sa_groups_servers gs ON gs.group_id = g.id AND `+gsCond+`
|
||||
WHERE a.player_steamid = ? AND (a.ends IS NULL OR a.ends > ?) AND `+aCond+`
|
||||
ORDER BY g.immunity DESC, g.id LIMIT 1`, args...).Scan(&r.group, &r.immunity)
|
||||
if err == sql.ErrNoRows {
|
||||
return rankState{}, nil
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
func rankKind(before, after rankState) string {
|
||||
switch {
|
||||
case before.group == "":
|
||||
return "added"
|
||||
case after.group == "":
|
||||
return "removed"
|
||||
case after.immunity > before.immunity:
|
||||
return "promoted"
|
||||
case after.immunity < before.immunity:
|
||||
return "demoted"
|
||||
}
|
||||
return "changed"
|
||||
}
|
||||
|
||||
func nullIfEmpty(s string) any {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// logRank records a rank change inside tx, if the rank actually changed.
|
||||
func (s *Store) logRank(ctx context.Context, tx *sql.Tx, n int64, name string, before, after rankState, by Actor) error {
|
||||
if before.group == after.group {
|
||||
return nil
|
||||
}
|
||||
a, _ := parseSID(by.SteamID)
|
||||
_, err := tx.ExecContext(ctx, `INSERT INTO sa_rank_log (player_steamid, player_name, old_rank, new_rank, kind, admin_steamid, admin_name, source, created)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
n, name, nullIfEmpty(before.group), nullIfEmpty(after.group), rankKind(before, after), a, by.Name, SourcePanel, s.wall(time.Now()))
|
||||
return err
|
||||
}
|
||||
|
||||
// adminNameTx is the name on a player's admin rows, or "".
|
||||
func adminNameTx(ctx context.Context, tx *sql.Tx, n int64) string {
|
||||
var name sql.NullString
|
||||
_ = tx.QueryRowContext(ctx, `SELECT player_name FROM sa_admins WHERE player_steamid = ? ORDER BY server_id IS NULL DESC, id LIMIT 1`, n).Scan(&name)
|
||||
return name.String
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue