GroupVerify/GroupVerifyApi/GroupVerifyBridge.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

120 lines
3.9 KiB
C#

using System.ComponentModel;
using CounterStrikeSharp.API.Core;
namespace GroupVerifyApi;
/// <summary>What GroupVerify itself implements behind the API. Not for other plugins.</summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public interface IGroupVerifyBackend
{
/// <summary>See <see cref="IGroupVerifyApi.IsActive"/>.</summary>
bool IsActive { get; }
/// <summary>See <see cref="IGroupVerifyApi.GetStatus"/>.</summary>
VerifyStatus GetStatus(ulong steamId64);
/// <summary>The configured clan tag; the bridge blanks it while inactive.</summary>
string ClanTag { get; }
}
/// <summary>
/// The one API object, living in this shared assembly so it outlives GroupVerify reloads: CSS keeps
/// the first capability supplier registered forever, and subscribers keep their handlers here.
/// GroupVerify attaches its backend on load and detaches it on unload. Not for other plugins.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public sealed class GroupVerifyBridge : IGroupVerifyApi
{
/// <summary>The instance GroupVerify registers as the capability.</summary>
public static GroupVerifyBridge Instance { get; } = new();
private IGroupVerifyBackend? _backend;
private GroupVerifyBridge()
{
}
/// <inheritdoc />
public event Action<CCSPlayerController, VerifyStatus>? OnVerifiedChanged;
/// <inheritdoc />
public event Action? OnClanTagChanged;
/// <inheritdoc />
public bool IsActive => _backend?.IsActive ?? false;
/// <inheritdoc />
public string ClanTag => _backend is { IsActive: true } backend ? backend.ClanTag : "";
/// <inheritdoc />
public bool IsVerified(CCSPlayerController player)
{
if (player is not { IsValid: true, IsBot: false, IsHLTV: false })
return false;
var steamId = player.AuthorizedSteamID?.SteamId64;
return steamId != null && IsVerified(steamId.Value);
}
/// <inheritdoc />
public bool IsVerified(ulong steamId64) => GetStatus(steamId64).Verified;
/// <inheritdoc />
public VerifyStatus GetStatus(ulong steamId64) =>
_backend is { IsActive: true } backend ? backend.GetStatus(steamId64) : default;
/// <summary>Called by GroupVerify on load.</summary>
public void Attach(IGroupVerifyBackend backend) => _backend = backend;
/// <summary>
/// Called by GroupVerify on unload; ignored if a newer backend has attached since. Tells
/// subscribers the clan tag is gone, if there was one.
/// </summary>
public void Detach(IGroupVerifyBackend backend)
{
if (!ReferenceEquals(_backend, backend))
return;
var had = ClanTag;
_backend = null;
if (had.Length > 0)
RaiseClanTagChanged();
}
/// <summary>Called by GroupVerify on the game thread; returns handlers' exceptions, like RaiseVerifiedChanged.</summary>
public List<Exception> RaiseClanTagChanged()
{
var errors = new List<Exception>();
foreach (var handler in OnClanTagChanged?.GetInvocationList() ?? [])
{
try
{
((Action)handler)();
}
catch (Exception ex)
{
errors.Add(ex);
}
}
return errors;
}
/// <summary>
/// Called by GroupVerify on the game thread. Each handler runs on its own, so one that throws
/// doesn't stop the others; the exceptions are returned for GroupVerify to log.
/// </summary>
public List<Exception> RaiseVerifiedChanged(CCSPlayerController player, VerifyStatus status)
{
var errors = new List<Exception>();
foreach (var handler in OnVerifiedChanged?.GetInvocationList() ?? [])
{
try
{
((Action<CCSPlayerController, VerifyStatus>)handler)(player, status);
}
catch (Exception ex)
{
errors.Add(ex);
}
}
return errors;
}
}