GroupVerify/Settings.cs
Astra 10e5fe9f3c 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.
2026-10-02 18:42:41 +01:00

99 lines
4.5 KiB
C#

namespace GroupVerify;
// The plugin's settings, from gv_settings. Immutable: a reload swaps in a new one.
internal sealed record Settings(
bool Enabled,
ulong GroupId64,
int MinForcedRefreshSeconds,
int DbReloadSeconds,
TimeZoneInfo TimeZone,
string VerifyMinRank,
string[] VerifyExtraRanks,
string ClanTag)
{
// Written to gv_settings when missing. The rank defaults are our real SimpleAdmin groups (see
// the fork's Helper.StaffRanks and the web panel's site config). Only the names are set here:
// "Admin or higher" is decided by immunity read from sa_groups.
public static readonly (string Name, string Value)[] Defaults =
[
("enabled", "1"), // 0 turns verification off (css_gv_enable/css_gv_disable)
("group_id64", "0"), // the Steam group's 64-bit ID (103582791...); 0 turns verification off (css_gv_group)
("min_forced_refresh_seconds", "60"), // a player is rechecked with Steam (css_verify, rejoining) at most this often
("db_reload_seconds", "60"), // how often gv_verified and gv_settings are reread
("timezone", "UTC"), // must match CS2-SimpleAdmin's Timezone
("verify_min_rank", "#rank/admin"), // css_verify: this group's immunity or higher
("verify_extra_ranks", "#rank/guardian"), // css_verify: these groups too, comma-separated
("clan_tag", "") // scoreboard tag for verified players without a rank (css_gv_clantag)
];
public static readonly Settings Default = Parse(new Dictionary<string, string>(), _ => { });
public static Settings Parse(IReadOnlyDictionary<string, string> values, Action<string> warn)
{
string Get(string name) =>
values.TryGetValue(name, out var value) ? value.Trim() : Defaults.First(d => d.Name == name).Value;
int GetInt(string name, int min)
{
if (int.TryParse(Get(name), out var value) && value >= min)
return value;
var fallback = int.Parse(Defaults.First(d => d.Name == name).Value);
warn($"gv_settings {name} must be a number of at least {min}; using {fallback}");
return fallback;
}
ulong groupId = 0;
var groupText = Get("group_id64");
if (groupText.Length > 0 && !ulong.TryParse(groupText, out groupId))
warn($"gv_settings group_id64 \"{groupText}\" isn't a 64-bit group ID; verification is off");
var timeZone = TimeZoneInfo.Utc;
var zoneName = Get("timezone");
try
{
timeZone = TimeZoneInfo.FindSystemTimeZoneById(zoneName);
}
catch (Exception ex) when (ex is TimeZoneNotFoundException or InvalidTimeZoneException)
{
warn($"gv_settings timezone \"{zoneName}\" isn't a known time zone; using UTC");
}
return new Settings(
Get("enabled") != "0",
groupId,
GetInt("min_forced_refresh_seconds", 0),
GetInt("db_reload_seconds", 10),
timeZone,
Get("verify_min_rank"),
Get("verify_extra_ranks").Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries),
ParseClanTag(Get("clan_tag"), warn));
}
// Steam's own group tags are at most 12 characters. Quotes and control characters are refused,
// since the tag travels in console commands.
public const int ClanTagMaxLength = 12;
public static bool ValidClanTag(string tag) =>
tag.Length <= ClanTagMaxLength && !tag.Any(c => char.IsControl(c) || c == '"' || c == ';');
private static string ParseClanTag(string tag, Action<string> warn)
{
if (ValidClanTag(tag))
return tag;
warn($"gv_settings clan_tag \"{tag}\" must be at most {ClanTagMaxLength} characters, without quotes, ; or control characters; ignoring it");
return "";
}
// The tag other plugins see: none while verification is off.
public string EffectiveClanTag => Active ? ClanTag : "";
// Verification only runs when enabled with a group set; otherwise nobody is verified.
public bool Active => Enabled && GroupId64 != 0;
// Now as a wall-clock time in the database's time zone, to the second (MySQL rounds fractions).
public DateTime Now()
{
var now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZone);
return DateTime.SpecifyKind(now.AddTicks(-(now.Ticks % TimeSpan.TicksPerSecond)), DateTimeKind.Unspecified);
}
}