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> FetchMembersAsync(ulong groupId64, CancellationToken cancel) { var members = new HashSet(); 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; } }