// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using Newtonsoft.Json; using osu.Framework.IO.Network; using osu.Framework.Logging; using osu.Game.Users; namespace osu.Game.Online.API { /// /// An API request with a well-defined response type. /// /// Type of the response (used for deserialisation). public abstract class APIRequest : APIRequest where T : class { protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest(Uri); public T Result { get; private set; } /// /// Invoked on successful completion of an API request. /// This will be scheduled to the API's internal scheduler (run on update thread automatically). /// public new event APISuccessHandler Success; protected override void PostProcess() { base.PostProcess(); Result = ((OsuJsonWebRequest)WebRequest)?.ResponseObject; } internal void TriggerSuccess(T result) { if (Result != null) throw new InvalidOperationException("Attempted to trigger success more than once"); Result = result; TriggerSuccess(); } internal override void TriggerSuccess() { base.TriggerSuccess(); Success?.Invoke(Result); } } /// /// AN API request with no specified response type. /// public abstract class APIRequest { protected abstract string Target { get; } protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri); protected virtual string Uri => $@"{API.APIEndpointUrl}/api/v2/{Target}"; protected APIAccess API; protected WebRequest WebRequest; /// /// The currently logged in user. Note that this will only be populated during . /// protected User User { get; private set; } /// /// Invoked on successful completion of an API request. /// This will be scheduled to the API's internal scheduler (run on update thread automatically). /// public event APISuccessHandler Success; /// /// Invoked on failure to complete an API request. /// This will be scheduled to the API's internal scheduler (run on update thread automatically). /// public event APIFailureHandler Failure; private bool cancelled; private Action pendingFailure; public void Perform(IAPIProvider api) { if (!(api is APIAccess apiAccess)) { Fail(new NotSupportedException($"A {nameof(APIAccess)} is required to perform requests.")); return; } API = apiAccess; User = apiAccess.LocalUser.Value; if (checkAndScheduleFailure()) return; WebRequest = CreateWebRequest(); WebRequest.Failed += Fail; WebRequest.AllowRetryOnTimeout = false; WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}"); if (checkAndScheduleFailure()) return; if (!WebRequest.Aborted) // could have been aborted by a Cancel() call { Logger.Log($@"Performing request {this}", LoggingTarget.Network); WebRequest.Perform(); } if (checkAndScheduleFailure()) return; PostProcess(); API.Schedule(delegate { if (cancelled) return; TriggerSuccess(); }); } /// /// Perform any post-processing actions after a successful request. /// protected virtual void PostProcess() { } internal virtual void TriggerSuccess() { Success?.Invoke(); } internal void TriggerFailure(Exception e) { Failure?.Invoke(e); } public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled")); public void Fail(Exception e) { if (WebRequest?.Completed == true) return; if (cancelled) return; cancelled = true; WebRequest?.Abort(); string responseString = WebRequest?.GetResponseString(); if (!string.IsNullOrEmpty(responseString)) { try { // attempt to decode a displayable error string. var error = JsonConvert.DeserializeObject(responseString); if (error != null) e = new APIException(error.ErrorMessage, e); } catch { } } Logger.Log($@"Failing request {this} ({e})", LoggingTarget.Network); pendingFailure = () => TriggerFailure(e); checkAndScheduleFailure(); } /// /// Checked for cancellation or error. Also queues up the Failed event if we can. /// /// Whether we are in a failed or cancelled state. private bool checkAndScheduleFailure() { if (API == null || pendingFailure == null) return cancelled; API.Schedule(pendingFailure); pendingFailure = null; return true; } private class DisplayableError { [JsonProperty("error")] public string ErrorMessage { get; set; } } } public class APIException : InvalidOperationException { public APIException(string messsage, Exception innerException) : base(messsage, innerException) { } } public delegate void APIFailureHandler(Exception e); public delegate void APISuccessHandler(); public delegate void APIProgressHandler(long current, long total); public delegate void APISuccessHandler(T content); }