No description
Find a file
2026-10-04 18:34:12 +01:00
GroupVerifyApi GroupVerify 0.3.0: check players with the Steam Web API as they join, clan tag for other plugins 2026-10-02 18:42:41 +01:00
lib GroupVerify 0.2.0: Steam group / override verification with an API for other plugins 2026-10-02 15:37:47 +01:00
Migrations GroupVerify 0.3.0: check players with the Steam Web API as they join, clan tag for other plugins 2026-10-02 18:42:41 +01:00
.gitignore GroupVerify 0.2.0: Steam group / override verification with an API for other plugins 2026-10-02 15:37:47 +01:00
Config.cs GroupVerify 0.3.0: check players with the Steam Web API as they join, clan tag for other plugins 2026-10-02 18:42:41 +01:00
Database.cs GroupVerify 0.3.0: check players with the Steam Web API as they join, clan tag for other plugins 2026-10-02 18:42:41 +01:00
GroupVerify.cs GroupVerify 0.3.0: check players with the Steam Web API as they join, clan tag for other plugins 2026-10-02 18:42:41 +01:00
GroupVerify.csproj GroupVerify 0.2.0: Steam group / override verification with an API for other plugins 2026-10-02 15:37:47 +01:00
README.md GroupVerify 0.3.0: check players with the Steam Web API as they join, clan tag for other plugins 2026-10-02 18:42:41 +01:00
release.sh release.sh: read the Forgejo token from ../../.forgejo-token when FORGEJO_TOKEN isn't set 2026-10-04 18:34:12 +01:00
Settings.cs GroupVerify 0.3.0: check players with the Steam Web API as they join, clan tag for other plugins 2026-10-02 18:42:41 +01:00
SteamApi.cs GroupVerify 0.3.0: check players with the Steam Web API as they join, clan tag for other plugins 2026-10-02 18:42:41 +01:00

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) 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:

{
  "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:

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:

<Reference Include="GroupVerifyApi">
  <HintPath>lib/GroupVerifyApi.dll</HintPath>
  <Private>false</Private>
</Reference>
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).