GroupVerify 0.2.0: Steam group / override verification with an API for other plugins
Players are verified when they're in the configured Steam group (public memberslistxml, refetched every 10 minutes, kept on failure) or have an unexpired override row in gv_verified, written by the web panel. Settings and data live in CS2-SimpleAdmin's MySQL database (gv_verified, gv_settings, gv_migrations), reached through its API. - css_gv_group <groupId64|0>, css_gv_enable, css_gv_disable (@css/root): verification runs only when enabled with a group set. - css_verify <target>: why a player is or isn't verified. Admin and above (by immunity from SimpleAdmin's groups) plus Guardian, unless admin_overrides.json says otherwise. - groupverify:api (shared/GroupVerifyApi): IsVerified, GetStatus, IsActive and OnVerifiedChanged. The API object lives in the shared assembly, so it survives hot reloads of GroupVerify.
This commit is contained in:
commit
1f61958099
13 changed files with 1711 additions and 0 deletions
227
README.md
Normal file
227
README.md
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
# 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.
|
||||
|
||||
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.
|
||||
|
||||
## 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 fetch its members now
|
||||
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_disable turn it off, keeping the group
|
||||
css_gv_enable turn it back on
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.** 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.
|
||||
- **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.
|
||||
|
||||
## `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.
|
||||
|
||||
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
|
||||
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: last seen in the list |
|
||||
|
||||
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: the next fetch rewrites them, and they never verify anyone on their own.
|
||||
|
||||
### `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`) |
|
||||
| `refresh_minutes` | `10` | how often the member list is fetched |
|
||||
| `min_forced_refresh_seconds` | `60` | `css_verify` refreshes only data older than this |
|
||||
| `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` |
|
||||
|
||||
## 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.
|
||||
|
||||
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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue