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.
This commit is contained in:
parent
1f61958099
commit
10e5fe9f3c
11 changed files with 436 additions and 204 deletions
97
SteamApi.cs
Normal file
97
SteamApi.cs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue