// Project Makoto // Copyright (C) 2024 Fortunevale // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY using System.Net; using ProjectMakoto.Entities.ScoreSaber; using ProjectMakoto.Enums.ScoreSaber; using ProjectMakoto.Exceptions; namespace ProjectMakoto.Plugins.ScoreSaber.Util; public class ScoreSaberClient { internal ScoreSaberClient() { this.QueueHandler(); } ~ScoreSaberClient() { this._disposed = true; } bool _disposed = false; readonly Dictionary Queue = new(); private void QueueHandler() { _ = Task.Run(async () => { HttpClient client = new(); while (!this._disposed) { if (this.Queue.Count == 0 || !this.Queue.Any(x => !x.Value.Resolved && !x.Value.Failed)) { await Task.Delay(100); continue; } var b = this.Queue.First(x => !x.Value.Resolved && !x.Value.Failed); try { ScoreSaberPlugin.Plugin!._logger.LogDebug("Sending Request to '{Url}'..", b.Value.Url); var response = await client.GetAsync(b.Value.Url); this.Queue[b.Key].StatusCode = response.StatusCode; if (!response.IsSuccessStatusCode) { if (response.StatusCode == HttpStatusCode.NotFound) throw new NotFoundException(""); if (response.StatusCode == HttpStatusCode.UnprocessableEntity) throw new UnprocessableEntityException(""); if (response.StatusCode == HttpStatusCode.InternalServerError) throw new InternalServerErrorException(""); if (response.StatusCode == HttpStatusCode.Forbidden) throw new ForbiddenException(""); throw new Exception($"Unsuccessful request: {response.StatusCode}"); } this.Queue[b.Key].Response = await response.Content.ReadAsStringAsync(); this.Queue[b.Key].Resolved = true; } catch (Exception ex) { this.Queue[b.Key].Failed = true; this.Queue[b.Key].Exception = ex; } finally { await Task.Delay(100); } } }); } private async Task MakeRequest(string url) { var key = Guid.NewGuid().ToString(); this.Queue.Add(key, new WebRequestItem { Url = url }); while (this.Queue.ContainsKey(key) && !this.Queue[key].Resolved && !this.Queue[key].Failed) await Task.Delay(100); if (!this.Queue.TryGetValue(key, out var value)) throw new Exception("The request has been removed from the queue prematurely."); var response = value; _ = this.Queue.Remove(key); if (response.Resolved) return response.Response; if (response.Failed) throw response.Exception; throw new Exception("This exception should be impossible to get."); } /// /// Gets a player by the given id /// /// The id to request. /// The requested player. public async Task GetPlayerById(string id) { var response = await this.MakeRequest($"https://scoresaber.com/api/player/{id}/full"); var request = JsonConvert.DeserializeObject(response); return request; } /// /// Gets a scoreboard by the given id. /// /// The id to request. /// The requested scoreboard. public async Task GetScoreboardById(string id) { var response = await this.MakeRequest($"https://scoresaber.com/api/ranking/request/by-id/{id}"); var request = JsonConvert.DeserializeObject(response); return request; } /// /// Gets a scoreboard by the map hash. /// /// The hash to request. /// The requested scoreboard. public async Task GetScoreboardByHash(string hash) { var response = await this.MakeRequest($"https://scoresaber.com/api/leaderboard/by-hash/{hash}"); var request = JsonConvert.DeserializeObject(response); return request; } /// /// Gets a scoreboard by it's id. /// /// The id to request. /// The page to request. /// Only gets scores from this country. /// The leaderboard and request metadata. public async Task GetScoreboardScoresById(string id, uint page = 1, string? country = null) { if (page < 1) page = 1; Dictionary parameters = new() { { "page", page.ToString() } }; if (country is not null) parameters.Add("countries", country.ToLower()); var query = await new FormUrlEncodedContent(parameters).ReadAsStringAsync(); var response = await this.MakeRequest($"https://scoresaber.com/api/leaderboard/by-id/{id}/scores?{query}"); var parsedResponse = JsonConvert.DeserializeObject(response); return parsedResponse; } /// /// Gets scoreboard by the map hash and difficulty. /// /// The hash to request. /// The difficulty to request. /// The page to request. /// Only gets scores from this country. /// The leaderboard and request metadata. public async Task GetScoreboardScoresByHash(string hash, Difficulty difficulty, uint page = 1, string? country = null) { if (page < 1) page = 1; Dictionary parameters = new() { { "difficulty", ((int)difficulty).ToString() }, { "page", page.ToString() }, }; if (country is not null) parameters.Add("countries", country.ToLower()); var query = await new FormUrlEncodedContent(parameters).ReadAsStringAsync(); var response = await this.MakeRequest($"https://scoresaber.com/api/leaderboard/by-hash/{hash}/scores?{query}"); var request = JsonConvert.DeserializeObject(response); return request; } /// /// Gets a player's scores by the player's id. /// /// The id to request. /// The type to sort by. /// How many scores should be shown. /// Which page to request. /// The player's scores. public async Task GetScoresById(string id, ScoreType sortType, uint limit = 10, uint page = 1) { if (page < 1) page = 1; Dictionary parameters = new() { { "limit", limit.ToString() }, { "sort", (sortType == ScoreType.Top ? "top" : "recent") }, { "page", page.ToString() }, { "withMetadata", "true" } }; var query = await new FormUrlEncodedContent(parameters).ReadAsStringAsync(); var response = await this.MakeRequest($"https://scoresaber.com/api/player/{id}/scores?{query}"); var request = JsonConvert.DeserializeObject(response); return request; } /// /// Searches for players matching a string. /// /// The name to search for. /// The page of the search to request. /// Only show players from this country. /// A list of users of the specified filter. public async Task SearchPlayer(string name, int page = 1, string? country = null) { if (page < 1) page = 1; Dictionary parameters = new() { { "search", name }, { "page", page.ToString() } }; if (country is not null) parameters.Add("countries", country.ToLower()); var query = await new FormUrlEncodedContent(parameters).ReadAsStringAsync(); var response = await this.MakeRequest($"https://scoresaber.com/api/players?{query}"); var request = JsonConvert.DeserializeObject(response); return request; } }