Root sets GroupVerify's clan_tag (the scoreboard tag CS2-Tags gives verified players without a rank) next to the Steam group. Validated like the plugin (12 UTF-16 units, no quotes, ; or \), saved to gv_settings and sent as css_gv_clantag when the plugin is loaded. Only changed fields are sent. GroupVerify 0.3.0 checks players with the Steam Web API as they join, so the group list is now "Confirmed group members" with first/last confirmed dates.
271 lines
9.7 KiB
Go
271 lines
9.7 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// GroupVerify's tables (plugins/GroupVerify in the cs2 repo) live in the same database. The plugin
|
|
// creates them; the panel reads them, writes 'manual' override rows and changes gv_settings. Their
|
|
// times are wall-clock values in gv_settings' timezone, the zone GroupVerify itself uses, so they're
|
|
// read and written in that location rather than SimpleAdmin's.
|
|
|
|
// VerifySettings is gv_settings.
|
|
type VerifySettings struct {
|
|
Enabled bool
|
|
GroupID string // "" when no group is set
|
|
ReloadSeconds int // how often GroupVerify rereads its tables
|
|
Location *time.Location
|
|
ClanTag string // scoreboard tag for verified players without a rank, "" for none
|
|
}
|
|
|
|
// Active matches GroupVerify: it only verifies anyone when enabled with a group set.
|
|
func (v VerifySettings) Active() bool { return v.Enabled && v.GroupID != "" }
|
|
|
|
// HasVerifyTables reports whether GroupVerify's tables exist in this database.
|
|
func (s *Store) HasVerifyTables(ctx context.Context) (bool, error) {
|
|
var n int
|
|
err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.tables
|
|
WHERE table_schema = DATABASE() AND table_name IN ('gv_verified', 'gv_settings')`).Scan(&n)
|
|
return n == 2, err
|
|
}
|
|
|
|
// VerifySettings reads gv_settings, with GroupVerify's defaults for anything missing.
|
|
func (s *Store) VerifySettings(ctx context.Context) (VerifySettings, error) {
|
|
v := VerifySettings{Enabled: true, ReloadSeconds: 60, Location: time.UTC}
|
|
rows, err := s.db.QueryContext(ctx, `SELECT name, value FROM gv_settings`)
|
|
if err != nil {
|
|
return v, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var name, value string
|
|
if err := rows.Scan(&name, &value); err != nil {
|
|
return v, err
|
|
}
|
|
value = strings.TrimSpace(value)
|
|
switch name {
|
|
case "enabled":
|
|
v.Enabled = value != "0"
|
|
case "group_id64":
|
|
if n, err := strconv.ParseUint(value, 10, 64); err == nil && n != 0 {
|
|
v.GroupID = value
|
|
}
|
|
case "db_reload_seconds":
|
|
if n, err := strconv.Atoi(value); err == nil && n >= 10 {
|
|
v.ReloadSeconds = n
|
|
}
|
|
case "clan_tag":
|
|
v.ClanTag = value
|
|
case "timezone":
|
|
if loc, err := time.LoadLocation(value); err == nil {
|
|
v.Location = loc
|
|
}
|
|
}
|
|
}
|
|
return v, rows.Err()
|
|
}
|
|
|
|
// SetVerifySetting writes one gv_settings value.
|
|
func (s *Store) SetVerifySetting(ctx context.Context, name, value string) error {
|
|
_, err := s.db.ExecContext(ctx, `INSERT INTO gv_settings (name, value) VALUES (?, ?)
|
|
ON DUPLICATE KEY UPDATE value = VALUES(value)`, name, value)
|
|
return err
|
|
}
|
|
|
|
func wallIn(t time.Time, loc *time.Location) time.Time {
|
|
t = t.In(loc)
|
|
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, time.UTC)
|
|
}
|
|
|
|
func fromWallIn(t time.Time, loc *time.Location) time.Time {
|
|
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), 0, loc)
|
|
}
|
|
|
|
// VerifyEntry is a row of gv_verified: an override, or a Steam group member.
|
|
type VerifyEntry struct {
|
|
SteamID string `json:"steamid"`
|
|
Name string `json:"name"` // the last name SimpleAdmin saw, "" if they never joined
|
|
Source string `json:"source"`
|
|
Expires *time.Time `json:"expires,omitempty"` // overrides only; nil never ends
|
|
AddedBy string `json:"addedBy,omitempty"`
|
|
Created time.Time `json:"created"`
|
|
Seen time.Time `json:"seen"` // group rows: last seen in the group's member list
|
|
}
|
|
|
|
// VerifyStatus is why a player is or isn't verified, as far as the database knows.
|
|
type VerifyStatus struct {
|
|
InGroup bool `json:"inGroup"`
|
|
GroupSeen *time.Time `json:"groupSeen,omitempty"`
|
|
Override *VerifyEntry `json:"override,omitempty"` // the unexpired override, if any
|
|
}
|
|
|
|
func (v VerifyStatus) Verified() bool { return v.InGroup || v.Override != nil }
|
|
|
|
// VerifyStatuses looks up several players at once. Players with no rows are left out.
|
|
func (s *Store) VerifyStatuses(ctx context.Context, loc *time.Location, steamids []string) (map[string]VerifyStatus, error) {
|
|
out := map[string]VerifyStatus{}
|
|
if len(steamids) == 0 {
|
|
return out, nil
|
|
}
|
|
in, args := inList(steamids)
|
|
rows, err := s.db.QueryContext(ctx, `SELECT steamid64, source, `+"`override`"+`, expires_at, added_by, created_at, updated_at
|
|
FROM gv_verified WHERE steamid64 IN (`+in+`)`, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
now := time.Now()
|
|
for rows.Next() {
|
|
var id int64
|
|
var e VerifyEntry
|
|
var override bool
|
|
var expires sql.NullTime
|
|
if err := rows.Scan(&id, &e.Source, &override, &expires, &e.AddedBy, &e.Created, &e.Seen); err != nil {
|
|
return nil, err
|
|
}
|
|
e.SteamID = strconv.FormatInt(id, 10)
|
|
e.Created, e.Seen = fromWallIn(e.Created, loc), fromWallIn(e.Seen, loc)
|
|
if expires.Valid {
|
|
t := fromWallIn(expires.Time, loc)
|
|
e.Expires = &t
|
|
}
|
|
st := out[e.SteamID]
|
|
if e.Source == "group" {
|
|
st.InGroup = true
|
|
st.GroupSeen = &e.Seen
|
|
}
|
|
// The same rule as GroupVerify: any unexpired override row counts, the longest one wins.
|
|
if override && (e.Expires == nil || e.Expires.After(now)) {
|
|
if st.Override == nil || (st.Override.Expires != nil && (e.Expires == nil || e.Expires.After(*st.Override.Expires))) {
|
|
entry := e
|
|
st.Override = &entry
|
|
}
|
|
}
|
|
out[e.SteamID] = st
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// What VerifyList lists.
|
|
const (
|
|
VerifyOverrides = "overrides" // unexpired rows with override = 1
|
|
VerifyGroup = "group" // players Steam confirmed are in the group as they joined
|
|
)
|
|
|
|
// VerifyList lists overrides or group members, newest first. Search matches a SteamID64 or a name
|
|
// SimpleAdmin has seen them use.
|
|
func (s *Store) VerifyList(ctx context.Context, loc *time.Location, kind, search string, lim, offset int) ([]VerifyEntry, int, error) {
|
|
var conds []string
|
|
var args []any
|
|
if kind == VerifyGroup {
|
|
conds = append(conds, "v.source = 'group'")
|
|
} else {
|
|
conds = append(conds, "v.`override` = 1", "(v.expires_at IS NULL OR v.expires_at > ?)")
|
|
args = append(args, wallIn(time.Now(), loc))
|
|
}
|
|
if t := strings.TrimSpace(search); t != "" {
|
|
or := "v.steamid64 IN (SELECT steamid FROM sa_players_ips WHERE name LIKE ?)"
|
|
orArgs := []any{"%" + escapeLike(t) + "%"}
|
|
if n, err := strconv.ParseInt(t, 10, 64); err == nil {
|
|
or += " OR v.steamid64 = ?"
|
|
orArgs = append(orArgs, n)
|
|
}
|
|
conds = append(conds, "("+or+")")
|
|
args = append(args, orArgs...)
|
|
}
|
|
where := " WHERE " + strings.Join(conds, " AND ")
|
|
|
|
var total int
|
|
if err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM gv_verified v"+where, args...).Scan(&total); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
rows, err := s.db.QueryContext(ctx, `SELECT v.steamid64, v.source, v.expires_at, v.added_by, v.created_at, v.updated_at,
|
|
COALESCE((SELECT p.name FROM sa_players_ips p WHERE p.steamid = v.steamid64 ORDER BY p.used_at DESC LIMIT 1), '')
|
|
FROM gv_verified v`+where+` ORDER BY v.created_at DESC, v.steamid64 LIMIT ? OFFSET ?`,
|
|
append(args, limit(lim), offset)...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
out := []VerifyEntry{}
|
|
for rows.Next() {
|
|
var id int64
|
|
var e VerifyEntry
|
|
var expires sql.NullTime
|
|
if err := rows.Scan(&id, &e.Source, &expires, &e.AddedBy, &e.Created, &e.Seen, &e.Name); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
e.SteamID = strconv.FormatInt(id, 10)
|
|
e.Created, e.Seen = fromWallIn(e.Created, loc), fromWallIn(e.Seen, loc)
|
|
if expires.Valid {
|
|
t := fromWallIn(expires.Time, loc)
|
|
e.Expires = &t
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return out, total, rows.Err()
|
|
}
|
|
|
|
// VerifyCounts is how many confirmed group members and unexpired overrides there are, and when
|
|
// Steam last confirmed a member (nil if it never has).
|
|
func (s *Store) VerifyCounts(ctx context.Context, loc *time.Location) (group, overrides int, fetched *time.Time, err error) {
|
|
var last sql.NullTime
|
|
err = s.db.QueryRowContext(ctx, `SELECT
|
|
COALESCE(SUM(source = 'group'), 0),
|
|
COALESCE(SUM(`+"`override`"+` = 1 AND (expires_at IS NULL OR expires_at > ?)), 0),
|
|
MAX(CASE WHEN source = 'group' THEN updated_at END)
|
|
FROM gv_verified`, wallIn(time.Now(), loc)).Scan(&group, &overrides, &last)
|
|
if last.Valid {
|
|
t := fromWallIn(last.Time, loc)
|
|
fetched = &t
|
|
}
|
|
return group, overrides, fetched, err
|
|
}
|
|
|
|
// SetOverride gives a player a manual override ending at expires (nil: never), or changes the one
|
|
// they have. created_at keeps when it was first given.
|
|
func (s *Store) SetOverride(ctx context.Context, loc *time.Location, steamid string, expires *time.Time, addedBy string) error {
|
|
n, err := parseSID(steamid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var ends any
|
|
if expires != nil {
|
|
ends = wallIn(*expires, loc)
|
|
}
|
|
now := wallIn(time.Now(), loc)
|
|
_, err = s.db.ExecContext(ctx, `INSERT INTO gv_verified (steamid64, source, `+"`override`"+`, expires_at, added_by, created_at, updated_at)
|
|
VALUES (?, 'manual', 1, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE `+"`override`"+` = 1, expires_at = VALUES(expires_at), added_by = VALUES(added_by), updated_at = VALUES(updated_at)`,
|
|
n, ends, addedBy, now, now)
|
|
return err
|
|
}
|
|
|
|
// RemoveOverride takes a player's override away: deletes their manual row, and clears override on
|
|
// their group row in case it was set by hand. Reports whether there was one.
|
|
func (s *Store) RemoveOverride(ctx context.Context, steamid string) (bool, error) {
|
|
n, err := parseSID(steamid)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer tx.Rollback()
|
|
res, err := tx.ExecContext(ctx, `DELETE FROM gv_verified WHERE steamid64 = ? AND source = 'manual'`, n)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
deleted, _ := res.RowsAffected()
|
|
res, err = tx.ExecContext(ctx, `UPDATE gv_verified SET `+"`override`"+` = 0 WHERE steamid64 = ? AND source = 'group' AND `+"`override`"+` = 1`, n)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
cleared, _ := res.RowsAffected()
|
|
return deleted+cleared > 0, tx.Commit()
|
|
}
|