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.
97 lines
4 KiB
C#
97 lines
4 KiB
C#
using System.Net;
|
|
using System.Text.Json;
|
|
|
|
namespace GroupVerify;
|
|
|
|
// What Steam said about one player and our group.
|
|
internal enum Membership
|
|
{
|
|
Member,
|
|
NotMember,
|
|
Unknown // Steam wouldn't say: private profile, bad key, rate limit, an outage
|
|
}
|
|
|
|
// Asks the Steam Web API (ISteamUser/GetUserGroupList) which groups a player is in. Runs off the
|
|
// game thread. Needs a Web API key, from GroupVerify.json.
|
|
internal static class SteamApi
|
|
{
|
|
private static readonly HttpClient Http = CreateClient();
|
|
|
|
private static HttpClient CreateClient()
|
|
{
|
|
var client = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
|
|
client.DefaultRequestHeaders.UserAgent.ParseAdd("GroupVerify/1.0 (CounterStrikeSharp)");
|
|
return client;
|
|
}
|
|
|
|
// Whether the player is in the group. Problem says why when the answer is Unknown. Never
|
|
// includes the key: exception messages and the URL stay out of what's returned.
|
|
public static async Task<(Membership Result, string? Problem)> CheckAsync(string apiKey, ulong steamId64,
|
|
ulong groupId64, CancellationToken cancel)
|
|
{
|
|
var url = "https://api.steampowered.com/ISteamUser/GetUserGroupList/v1/" +
|
|
$"?key={Uri.EscapeDataString(apiKey)}&steamid={steamId64}";
|
|
HttpResponseMessage response;
|
|
try
|
|
{
|
|
response = await Http.GetAsync(url, cancel);
|
|
}
|
|
catch (HttpRequestException ex)
|
|
{
|
|
return (Membership.Unknown, $"couldn't reach Steam ({ex.HttpRequestError})");
|
|
}
|
|
catch (TaskCanceledException) when (!cancel.IsCancellationRequested)
|
|
{
|
|
return (Membership.Unknown, "Steam didn't answer in time");
|
|
}
|
|
|
|
using (response)
|
|
{
|
|
switch (response.StatusCode)
|
|
{
|
|
case HttpStatusCode.OK:
|
|
break;
|
|
case HttpStatusCode.Unauthorized:
|
|
case HttpStatusCode.Forbidden:
|
|
return (Membership.Unknown, $"Steam refused the request ({(int)response.StatusCode}): a bad API key, or a private profile");
|
|
case HttpStatusCode.TooManyRequests:
|
|
return (Membership.Unknown, "Steam is rate-limiting the API key (429)");
|
|
default:
|
|
return (Membership.Unknown, $"Steam answered {(int)response.StatusCode}");
|
|
}
|
|
|
|
JsonElement root;
|
|
try
|
|
{
|
|
using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync(cancel));
|
|
root = doc.RootElement.GetProperty("response").Clone();
|
|
}
|
|
catch (Exception ex) when (ex is JsonException or KeyNotFoundException or InvalidOperationException)
|
|
{
|
|
return (Membership.Unknown, "Steam's answer wasn't the expected JSON");
|
|
}
|
|
|
|
// A private profile (or group list) comes back as success: false.
|
|
if (!root.TryGetProperty("success", out var success) || success.ValueKind != JsonValueKind.True)
|
|
{
|
|
var error = root.TryGetProperty("error", out var e) ? e.ToString() : "no reason given";
|
|
return (Membership.Unknown, $"Steam wouldn't list their groups ({error}); their profile may be private");
|
|
}
|
|
|
|
// gid is the group's 32-bit account ID, the low half of its 64-bit ID. Accept the full
|
|
// 64-bit ID too, in case Steam ever sends that.
|
|
var account = groupId64 & 0xFFFFFFFF;
|
|
if (root.TryGetProperty("groups", out var groups) && groups.ValueKind == JsonValueKind.Array)
|
|
{
|
|
foreach (var group in groups.EnumerateArray())
|
|
{
|
|
if (!group.TryGetProperty("gid", out var gid))
|
|
continue;
|
|
if (ulong.TryParse(gid.ToString(), out var id) && (id == account || id == groupId64))
|
|
return (Membership.Member, null);
|
|
}
|
|
}
|
|
return (Membership.NotMember, null);
|
|
}
|
|
}
|
|
}
|