GroupVerify 0.3.0: check players with the Steam Web API as they join, clan tag for other plugins

Steam rate-limits its community member lists by IP (429), so the whole-list download is gone.
Each player is checked as they join with ISteamUser/GetUserGroupList: a member's 'group' row is
added or refreshed, a non-member's deleted, and when Steam won't say (private profile, bad key,
outage) the database stands. A player is asked about at most once per
min_forced_refresh_seconds; css_verify rechecks its targets, offline SteamIDs too. Changing the
group clears the old group's rows. refresh_minutes is gone.

The Web API key goes in configs/plugins/GroupVerify/GroupVerify.json (SteamWebApiKey); every
other setting stays in the database.

New clan_tag setting and css_gv_clantag <tag|-> (@css/root): the scoreboard tag for verified
players without a rank, applied by CS2-Tags. The API gains ClanTag and OnClanTagChanged, so
shared/GroupVerifyApi changes: needs a server restart.
This commit is contained in:
Astra 2026-10-02 18:42:41 +01:00
parent 1f61958099
commit 10e5fe9f3c
11 changed files with 436 additions and 204 deletions

View file

@ -4,9 +4,21 @@ Marks players as **verified** when they're in our Steam group, or when staff hav
manual override on the web panel. It gives out nothing itself: other plugins ask it through its
API (`GroupVerifyApi`, see [For other plugins](#for-other-plugins)) and hand out their own perks.
Everything lives in CS2-SimpleAdmin's MySQL database, reached through SimpleAdmin's API
(`GetConnectionString()`), so there's no config file. Needs CS2-SimpleAdmin on MySQL; on SQLite,
or without SimpleAdmin, it logs an error and verifies nobody.
Its data and settings live in CS2-SimpleAdmin's MySQL database, reached through SimpleAdmin's API
(`GetConnectionString()`). Needs CS2-SimpleAdmin on MySQL; on SQLite, or without SimpleAdmin, it
logs an error and verifies nobody. The one exception is the Steam Web API key, a secret, which goes
in `configs/plugins/GroupVerify/GroupVerify.json`:
```json
{
"SteamWebApiKey": "<from https://steamcommunity.com/dev/apikey>",
"ConfigVersion": 1
}
```
CSS writes that file with a blank key on first load. Fill it in, then `css_plugins reload
GroupVerify` (or restart). Without a key nobody's group membership is checked: players already
saved as members stay verified, overrides still work, and the log says so once.
## Turning it on
@ -15,17 +27,19 @@ verified, including players with an override, and other plugins see `IsActive ==
install is enabled with no group, so it's off until you set one:
```
css_gv_group 103582791429521408 set the group (its 64-bit ID) and fetch its members now
css_gv_group 103582791429521408 set the group (its 64-bit ID) and check everyone online
css_gv_group 0 clear the group: verification is off
css_gv_group show whether it's on, the group, and how old the list is
css_gv_group show whether it's on and the group
css_gv_disable turn it off, keeping the group
css_gv_enable turn it back on
css_gv_clantag zio the scoreboard tag for verified players without a rank (- clears it)
```
These need `@css/root` (an `admin_overrides.json` entry for a command changes that) and work from
the server console and RCON. Changes are saved to `gv_settings` and take effect at once: turning
it off tells other plugins every verified player stopped being verified, and turning it on (or
changing the group) fetches the member list straight away.
changing the group) checks everyone on the server with Steam straight away. Changing the group
deletes the saved members of the old one.
The group ID is the number in `<groupID64>` at
`https://steamcommunity.com/groups/<name>/memberslistxml/?xml=1`. `css_gv_group` refuses anything
@ -33,18 +47,26 @@ that isn't a Steam group ID.
## How it works
- **Steam group.** The group's public member list
(`https://steamcommunity.com/gid/<group_id64>/memberslistxml/?xml=1`, every `nextPageLink` page)
is fetched off the game thread every `refresh_minutes`. A failed or empty fetch keeps the old
list. Each good fetch is saved to `gv_verified` as `source = 'group'` rows, and the rows of
anyone who left are deleted; at startup those rows stand in for the list until Steam answers.
- **Steam group, checked per player as they join.** Once Steam has authorized a player, they're
verified straight away from what the database knows, then GroupVerify asks the Steam Web API
which groups they're in
(`https://api.steampowered.com/ISteamUser/GetUserGroupList/v1/?key=<key>&steamid=<steamid64>`,
off the game thread). In the group: their `source = 'group'` row is added or refreshed. Not in
it: the row is deleted. Steam wouldn't say (a private profile, a bad key, an outage, a rate
limit): nothing changes, what the database has stands, and the log says why. A player is asked
about at most once per `min_forced_refresh_seconds`, so reconnecting doesn't spam Steam.
- **Only on join.** A player who leaves the group while on the server stays verified until they
next join (or someone runs `css_verify` on them).
- **Overrides.** `gv_verified` is reread every `db_reload_seconds`, so web panel changes show up
without a restart. Expired rows are ignored, and deleted on each reload.
- **Verified** = in the group list, **or** an unexpired row with `override` = 1.
- Online players are checked when they connect (once Steam has authorized them), after each group
fetch, after each database reload and after each settings command. Other plugins get
`OnVerifiedChanged` only when a player's status changes. Their state is dropped when they
disconnect.
- **Verified** = a `group` row (or this session's Steam answer, which wins), **or** an unexpired
row with `override` = 1.
- Online players are re-evaluated after each Steam answer, database reload and settings command.
Other plugins get `OnVerifiedChanged` only when a player's status changes. Their state is
dropped when they disconnect.
- **Why not the clan tag?** The tag shown on the scoreboard is CS2-Tags' rank tag on our server,
not the player's, and the group a client says it represents (`cl_clanid`) is a client setting
anyone can set to any group. Only Steam's own answer is trusted.
## `css_verify` (`!verify`)
@ -53,10 +75,11 @@ that isn't a Steam group ID.
(SteamID64, `STEAM_X:Y:Z` or `[U:1:N]`), which works for offline players too. No target means
`@me`. Works from the server console.
If the group list or the database was last loaded more than `min_forced_refresh_seconds` ago,
both are brought up to date first. Then the targets are re-evaluated, and the answer goes to chat:
one line per player for up to 5 targets (verified by Steam group, override with its expiry, or
not verified), a summary for more, and how old the group list and overrides are. Every use is
Each target (offline SteamIDs too) is checked with Steam again, and the database reread, where
that was last done more than `min_forced_refresh_seconds` ago. Then the targets are re-evaluated,
and the answer goes to chat: one line per player for up to 5 targets (verified by Steam group,
override with its expiry, or not verified, and why Steam couldn't be asked if it couldn't), a
summary for more, and how old the overrides are. Every use is
logged through SimpleAdmin's `LogCommand` (server log and its Discord log) and the plugin's log.
**Who can use it:** Admin and above, plus Guardian. A player qualifies when one of their
@ -86,7 +109,7 @@ match SimpleAdmin's `Timezone` (as the web panel already assumes for SimpleAdmin
| `expires_at` | `datetime NULL` | NULL = never. Expired rows are ignored, then deleted. |
| `added_by` | `varchar(128)` | free text: who added it (`steam-group` for group rows) |
| `created_at` | `datetime` | first added |
| `updated_at` | `datetime` | group rows: last seen in the list |
| `updated_at` | `datetime` | group rows: when Steam last confirmed them |
One row per player and source, so a manual override and a group row never overwrite each other.
`override` is backticked in the plugin's SQL; do the same, it's close to a reserved word.
@ -102,7 +125,8 @@ ON DUPLICATE KEY UPDATE `override` = 1, expires_at = VALUES(expires_at),
`NOW()` is only right if MySQL's time zone matches `timezone`; otherwise pass the time from the
panel. To remove one: `DELETE FROM gv_verified WHERE steamid64 = ? AND source = 'manual'`. Leave
`group` rows alone: the next fetch rewrites them, and they never verify anyone on their own.
`group` rows alone: they verify the player, and the next Steam check as they join adds or deletes
them.
### `gv_settings`
@ -114,12 +138,12 @@ applies at the next reload (`db_reload_seconds`), timers included.
|------------------------------|------------------|-----------------------------------------------------------------|
| `enabled` | `1` | `0` turns verification off (`css_gv_enable`/`css_gv_disable`) |
| `group_id64` | `0` | the Steam group's 64-bit ID; `0` turns it off (`css_gv_group`) |
| `refresh_minutes` | `10` | how often the member list is fetched |
| `min_forced_refresh_seconds` | `60` | `css_verify` refreshes only data older than this |
| `min_forced_refresh_seconds` | `60` | a player is rechecked with Steam at most this often |
| `db_reload_seconds` | `60` | how often `gv_settings` and `gv_verified` are reread |
| `timezone` | `UTC` | time zone of the `datetime` columns; match SimpleAdmin's |
| `verify_min_rank` | `#rank/admin` | `css_verify` needs this group's immunity or higher |
| `verify_extra_ranks` | `#rank/guardian` | comma-separated groups that may also use `css_verify` |
| `clan_tag` | (blank) | scoreboard tag for verified players without a rank, max 12 |
## For other plugins
@ -135,6 +159,11 @@ Use it on the game thread:
- `OnVerifiedChanged(player, status)`: fired when an online player becomes verified (including
just after connecting) or stops being verified (left the group, override ran out, or
verification was turned off). Not fired on disconnect.
- `ClanTag`: the scoreboard tag for verified players who have no rank tag (`css_gv_clantag`, the
`clan_tag` setting), "" when none is set or while `IsActive` is false. GroupVerify doesn't set
tags itself; CS2-Tags (1.0.4c-zio6+) does.
- `OnClanTagChanged()`: fired when `ClanTag` changes, including to or from "" as GroupVerify is
turned on or off, loaded or unloaded.
In the other plugin, copy `GroupVerifyApi.dll` from the server's `shared/GroupVerifyApi/` (or
`GroupVerifyApi/bin/Release/net10.0/` here) to its `lib/`, and reference it compile-only. It must