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.
256 lines
14 KiB
Markdown
256 lines
14 KiB
Markdown
# GroupVerify
|
|
|
|
Marks players as **verified** when they're in our Steam group, or when staff have given them a
|
|
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.
|
|
|
|
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
|
|
|
|
Verification runs only when it's **enabled and has a Steam group set**. Otherwise nobody is
|
|
verified, including players with an override, and other plugins see `IsActive == false`. A new
|
|
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 check everyone online
|
|
css_gv_group 0 clear the group: verification is off
|
|
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) 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
|
|
that isn't a Steam group ID.
|
|
|
|
## How it works
|
|
|
|
- **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** = 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`)
|
|
|
|
`css_verify <target>`: `@all`, `@t`, `@ct`, `@spec`, `@alive`, `@dead`, `@me`, `#userid`, a name
|
|
(an exact match first, then a partial one; ambiguous names are refused), or a SteamID
|
|
(SteamID64, `STEAM_X:Y:Z` or `[U:1:N]`), which works for offline players too. No target means
|
|
`@me`. Works from the server console.
|
|
|
|
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
|
|
SimpleAdmin groups has at least the immunity of the `verify_min_rank` group (`#rank/admin`), or is
|
|
listed in `verify_extra_ranks` (`#rank/guardian`). Groups are read from the database the same way
|
|
our SimpleAdmin fork works out a rank (`PermissionManager.GetRank`): an unexpired `sa_admins` row for
|
|
this server, whose `sa_admins_flags` entry names an `sa_groups` group that applies to this server.
|
|
So promoting someone on the web panel takes effect at once, and a new rank above Admin passes
|
|
by its immunity without changes here.
|
|
|
|
An `admin_overrides.json` entry for `css_verify` replaces that check: CSS enforces the override's
|
|
flags before the command runs, and the rank check is skipped.
|
|
|
|
## Database
|
|
|
|
Created at load from the embedded `Migrations/*.sql`, tracked in `gv_migrations` (kept apart from
|
|
SimpleAdmin's `sa_migrations`). Times are wall-clock values in the `timezone` setting, which must
|
|
match SimpleAdmin's `Timezone` (as the web panel already assumes for SimpleAdmin's own tables).
|
|
|
|
### `gv_verified`
|
|
|
|
| column | type | notes |
|
|
|--------------|----------------------------|---------------------------------------------------------------|
|
|
| `steamid64` | `bigint` | primary key, with `source` |
|
|
| `source` | `enum('group','manual')` | `group`: written by the plugin. `manual`: the web panel's. |
|
|
| `override` | `tinyint(1)` | 1 verifies the player whether or not they're in the group |
|
|
| `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: 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.
|
|
|
|
The web panel only writes `manual` rows. To add or change an override:
|
|
|
|
```sql
|
|
INSERT INTO gv_verified (steamid64, source, `override`, expires_at, added_by, created_at, updated_at)
|
|
VALUES (76561198000000000, 'manual', 1, '2026-12-31 23:59:00', 'astra (76561198...)', NOW(), NOW())
|
|
ON DUPLICATE KEY UPDATE `override` = 1, expires_at = VALUES(expires_at),
|
|
added_by = VALUES(added_by), updated_at = VALUES(updated_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: they verify the player, and the next Steam check as they join adds or deletes
|
|
them.
|
|
|
|
### `gv_settings`
|
|
|
|
`name` → `value` (both strings). Missing names are added with their defaults at each reload.
|
|
`enabled` and `group_id64` are normally set with the commands above. Anything edited by hand
|
|
applies at the next reload (`db_reload_seconds`), timers included.
|
|
|
|
| name | default | meaning |
|
|
|------------------------------|------------------|-----------------------------------------------------------------|
|
|
| `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`) |
|
|
| `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
|
|
|
|
GroupVerify registers the capability `groupverify:api` (`IGroupVerifyApi`), from
|
|
`GroupVerifyApi.dll`, which is installed in `addons/counterstrikesharp/shared/GroupVerifyApi/`.
|
|
Use it on the game thread:
|
|
|
|
- `IsVerified(player)` / `IsVerified(steamId64)`: verified right now. False for bots, players
|
|
Steam hasn't authorized yet, and everyone while `IsActive` is false.
|
|
- `GetStatus(steamId64)`: a `VerifyStatus` with `Verified`, `InGroup`, `HasOverride` and
|
|
`OverrideExpires` (null when it never ends).
|
|
- `IsActive`: GroupVerify is loaded, enabled, has a group and has read its database.
|
|
- `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
|
|
not end up in that plugin's own folder, or that plugin gets its own copy of the types and the
|
|
capability won't match:
|
|
|
|
```xml
|
|
<Reference Include="GroupVerifyApi">
|
|
<HintPath>lib/GroupVerifyApi.dll</HintPath>
|
|
<Private>false</Private>
|
|
</Reference>
|
|
```
|
|
|
|
```csharp
|
|
using GroupVerifyApi;
|
|
|
|
private IGroupVerifyApi? _verify;
|
|
|
|
public override void OnAllPluginsLoaded(bool hotReload)
|
|
{
|
|
try
|
|
{
|
|
_verify = IGroupVerifyApi.Capability.Get();
|
|
}
|
|
catch (KeyNotFoundException)
|
|
{
|
|
}
|
|
if (_verify == null)
|
|
{
|
|
Logger.LogError("GroupVerify isn't installed, so nobody gets verified perks");
|
|
return;
|
|
}
|
|
_verify.OnVerifiedChanged += OnVerifiedChanged;
|
|
}
|
|
|
|
public override void Unload(bool hotReload)
|
|
{
|
|
if (_verify != null)
|
|
_verify.OnVerifiedChanged -= OnVerifiedChanged;
|
|
}
|
|
|
|
// React to changes...
|
|
private void OnVerifiedChanged(CCSPlayerController player, VerifyStatus status)
|
|
{
|
|
if (status.Verified)
|
|
GiveTag(player);
|
|
else
|
|
RemoveTag(player);
|
|
}
|
|
|
|
// ...or just ask whenever it matters.
|
|
private HookResult OnPlayerSpawn(EventPlayerSpawn @event, GameEventInfo info)
|
|
{
|
|
if (@event.Userid is { IsValid: true } player && _verify?.IsVerified(player) == true)
|
|
GiveVerifiedLoadout(player);
|
|
return HookResult.Continue;
|
|
}
|
|
```
|
|
|
|
Event handlers run one at a time, and one that throws is logged by GroupVerify without stopping
|
|
the others. Make them idempotent: after GroupVerify is hot-reloaded, it fires `true` again for
|
|
everyone verified.
|
|
|
|
**Hot reload.** Unlike SimpleAdmin's, this API survives reloads of GroupVerify. The object handed
|
|
out is `GroupVerifyBridge.Instance`, which lives in the shared assembly, not in the plugin. CSS
|
|
keeps the first capability supplier registered for the whole server run, and GroupVerify registers
|
|
only once, so `Capability.Get()` keeps returning that same object. GroupVerify attaches to it on
|
|
load and detaches on unload, and event subscriptions stay on it. While GroupVerify is unloaded,
|
|
`IsActive` is false and nobody is verified. Changing `GroupVerifyApi` itself needs a server
|
|
restart, and its plugins rebuilt if the interface changed.
|
|
|
|
`IGroupVerifyBackend`, and `GroupVerifyBridge`'s `Attach`, `Detach` and `RaiseVerifiedChanged`,
|
|
are for GroupVerify only.
|
|
|
|
## Building and releasing
|
|
|
|
`cd plugins/GroupVerify && ../../build.sh` builds into `compiled/GroupVerify/`.
|
|
`./release.sh v<ModuleVersion>` (needs `FORGEJO_TOKEN`) publishes `GroupVerify-<tag>.tar.gz`, laid
|
|
out like `game/csgo/`, to the `cs2/GroupVerify` repo on git.zio.sh. It bundles `MySqlConnector.dll`
|
|
(the same version as SimpleAdmin's) and its `Microsoft.Extensions.*` abstractions in the plugin
|
|
folder, and `GroupVerifyApi` in `shared/GroupVerifyApi/`.
|
|
|
|
`build.sh` doesn't put `GroupVerifyApi.dll` in `compiled/GroupVerify/` (it doesn't belong in the
|
|
plugin folder). Its build is in `GroupVerifyApi/bin/Release/net10.0/`; copy it to
|
|
`shared/GroupVerifyApi/` by hand when deploying without a release.
|
|
|
|
`lib/CS2-SimpleAdminApi.dll` is a compile-only copy of the server's `shared/CS2-SimpleAdminApi`.
|
|
|
|
Never hot-reload CS2-SimpleAdmin while this runs: CSS keeps handing out the old instance's API
|
|
until the server restarts. Hot-reloading GroupVerify is fine (see Hot reload above).
|