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.
60 lines
2.5 KiB
C#
60 lines
2.5 KiB
C#
using System.Xml.Linq;
|
|
|
|
namespace GroupVerify;
|
|
|
|
// Reads a Steam group's public member list (memberslistxml), runs off the game thread.
|
|
internal static class SteamGroup
|
|
{
|
|
// 1000 members a page, so this allows groups of up to 100,000.
|
|
private const int MaxPages = 100;
|
|
// A pause between pages, to stay clear of Steam's rate limit.
|
|
private static readonly TimeSpan PageDelay = TimeSpan.FromMilliseconds(500);
|
|
|
|
private static readonly HttpClient Http = CreateClient();
|
|
|
|
private static HttpClient CreateClient()
|
|
{
|
|
var client = new HttpClient { Timeout = TimeSpan.FromSeconds(30) };
|
|
client.DefaultRequestHeaders.UserAgent.ParseAdd("GroupVerify/1.0 (CounterStrikeSharp)");
|
|
return client;
|
|
}
|
|
|
|
// Every member's SteamID64, following nextPageLink. Throws if any page fails, so a partial list
|
|
// never replaces a whole one.
|
|
public static async Task<HashSet<ulong>> FetchMembersAsync(ulong groupId64, CancellationToken cancel)
|
|
{
|
|
var members = new HashSet<ulong>();
|
|
Uri? url = new($"https://steamcommunity.com/gid/{groupId64}/memberslistxml/?xml=1");
|
|
|
|
for (var page = 1; url != null; page++)
|
|
{
|
|
if (page > MaxPages)
|
|
throw new InvalidDataException($"the member list has more than {MaxPages} pages");
|
|
if (page > 1)
|
|
await Task.Delay(PageDelay, cancel);
|
|
|
|
var root = XDocument.Parse(await Http.GetStringAsync(url, cancel)).Root;
|
|
if (root?.Name.LocalName != "memberList")
|
|
throw new InvalidDataException($"page {page} isn't a member list (<{root?.Name.LocalName}>)");
|
|
|
|
foreach (var id in root.Element("members")?.Elements("steamID64") ?? [])
|
|
{
|
|
if (ulong.TryParse(id.Value.Trim(), out var steamId))
|
|
members.Add(steamId);
|
|
}
|
|
|
|
url = NextPage(root.Element("nextPageLink")?.Value);
|
|
}
|
|
return members;
|
|
}
|
|
|
|
// Only ever follows links back to steamcommunity.com, over https.
|
|
private static Uri? NextPage(string? link)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(link) || !Uri.TryCreate(link.Trim(), UriKind.Absolute, out var uri))
|
|
return null;
|
|
if (uri.Host != "steamcommunity.com" || (uri.Scheme != Uri.UriSchemeHttps && uri.Scheme != Uri.UriSchemeHttp))
|
|
throw new InvalidDataException($"unexpected nextPageLink {uri}");
|
|
return new UriBuilder(uri) { Scheme = Uri.UriSchemeHttps, Port = -1 }.Uri;
|
|
}
|
|
}
|