2019-01-24 16:43:03 +08:00
|
|
|
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
|
|
|
// See the LICENCE file in the repository root for full licence text.
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-06-17 15:37:17 +08:00
|
|
|
|
#nullable disable
|
|
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
|
using System;
|
2022-02-17 17:33:27 +08:00
|
|
|
|
using System.Globalization;
|
2021-10-05 13:28:56 +08:00
|
|
|
|
using JetBrains.Annotations;
|
2019-11-12 18:34:20 +08:00
|
|
|
|
using Newtonsoft.Json;
|
2023-10-30 14:07:35 +08:00
|
|
|
|
using osu.Framework.Extensions.TypeExtensions;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
using osu.Framework.IO.Network;
|
2018-12-18 19:19:40 +08:00
|
|
|
|
using osu.Framework.Logging;
|
2023-06-08 07:50:29 +08:00
|
|
|
|
using osu.Game.Extensions;
|
2021-11-04 17:02:44 +08:00
|
|
|
|
using osu.Game.Online.API.Requests.Responses;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
|
|
namespace osu.Game.Online.API
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// An API request with a well-defined response type.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <typeparam name="T">Type of the response (used for deserialisation).</typeparam>
|
2020-03-30 14:05:40 +08:00
|
|
|
|
public abstract class APIRequest<T> : APIRequest where T : class
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2019-12-28 23:07:55 +08:00
|
|
|
|
protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest<T>(Uri);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-10-05 13:28:56 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// The deserialised response object. May be null if the request or deserialisation failed.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[CanBeNull]
|
|
|
|
|
public T Response { get; private set; }
|
2018-06-08 13:37:48 +08:00
|
|
|
|
|
2018-06-08 13:37:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Invoked on successful completion of an API request.
|
|
|
|
|
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
|
|
|
|
|
/// </summary>
|
2018-04-13 17:19:50 +08:00
|
|
|
|
public new event APISuccessHandler<T> Success;
|
2020-04-11 16:47:51 +08:00
|
|
|
|
|
2021-07-22 13:23:24 +08:00
|
|
|
|
protected APIRequest()
|
|
|
|
|
{
|
2021-10-05 13:28:56 +08:00
|
|
|
|
base.Success += () => Success?.Invoke(Response);
|
2021-07-22 13:23:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-20 16:48:02 +08:00
|
|
|
|
protected override void PostProcess()
|
|
|
|
|
{
|
|
|
|
|
base.PostProcess();
|
2021-11-30 18:08:51 +08:00
|
|
|
|
|
|
|
|
|
if (WebRequest != null)
|
|
|
|
|
{
|
|
|
|
|
Response = ((OsuJsonWebRequest<T>)WebRequest).ResponseObject;
|
2023-10-30 14:07:35 +08:00
|
|
|
|
Logger.Log($"{GetType().ReadableName()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network);
|
2021-11-30 18:08:51 +08:00
|
|
|
|
}
|
2020-04-20 16:48:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-11 17:02:49 +08:00
|
|
|
|
internal void TriggerSuccess(T result)
|
|
|
|
|
{
|
2021-10-05 13:28:56 +08:00
|
|
|
|
if (Response != null)
|
2020-04-13 20:24:47 +08:00
|
|
|
|
throw new InvalidOperationException("Attempted to trigger success more than once");
|
2020-04-11 17:02:49 +08:00
|
|
|
|
|
2021-10-05 13:28:56 +08:00
|
|
|
|
Response = result;
|
2020-04-20 16:48:02 +08:00
|
|
|
|
|
|
|
|
|
TriggerSuccess();
|
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// AN API request with no specified response type.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public abstract class APIRequest
|
|
|
|
|
{
|
2018-12-12 18:03:27 +08:00
|
|
|
|
protected abstract string Target { get; }
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2019-12-28 23:07:55 +08:00
|
|
|
|
protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri);
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-12-24 17:11:40 +08:00
|
|
|
|
protected virtual string Uri => $@"{API.APIEndpointUrl}/api/v2/{Target}";
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
|
|
protected APIAccess API;
|
|
|
|
|
protected WebRequest WebRequest;
|
|
|
|
|
|
2020-07-14 12:07:17 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// The currently logged in user. Note that this will only be populated during <see cref="Perform"/>.
|
|
|
|
|
/// </summary>
|
2021-11-04 17:02:44 +08:00
|
|
|
|
protected APIUser User { get; private set; }
|
2020-07-14 12:07:17 +08:00
|
|
|
|
|
2018-06-08 13:37:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Invoked on successful completion of an API request.
|
|
|
|
|
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
|
|
|
|
|
/// </summary>
|
2018-04-13 17:19:50 +08:00
|
|
|
|
public event APISuccessHandler Success;
|
2018-06-08 13:37:27 +08:00
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Invoked on failure to complete an API request.
|
|
|
|
|
/// This will be scheduled to the API's internal scheduler (run on update thread automatically).
|
|
|
|
|
/// </summary>
|
2018-04-13 17:19:50 +08:00
|
|
|
|
public event APIFailureHandler Failure;
|
|
|
|
|
|
2021-07-19 19:27:00 +08:00
|
|
|
|
private readonly object completionStateLock = new object();
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// The state of this request, from an outside perspective.
|
|
|
|
|
/// This is used to ensure correct notification events are fired.
|
|
|
|
|
/// </summary>
|
2021-08-19 18:55:10 +08:00
|
|
|
|
public APIRequestCompletionState CompletionState { get; private set; }
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2019-03-13 11:56:47 +08:00
|
|
|
|
public void Perform(IAPIProvider api)
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2019-03-13 11:56:47 +08:00
|
|
|
|
if (!(api is APIAccess apiAccess))
|
2019-09-25 14:00:08 +08:00
|
|
|
|
{
|
|
|
|
|
Fail(new NotSupportedException($"A {nameof(APIAccess)} is required to perform requests."));
|
|
|
|
|
return;
|
|
|
|
|
}
|
2019-03-13 11:56:47 +08:00
|
|
|
|
|
|
|
|
|
API = apiAccess;
|
2020-07-14 12:07:17 +08:00
|
|
|
|
User = apiAccess.LocalUser.Value;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-07-28 22:58:57 +08:00
|
|
|
|
if (isFailing) return;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
|
|
|
|
WebRequest = CreateWebRequest();
|
2018-06-03 12:01:52 +08:00
|
|
|
|
WebRequest.Failed += Fail;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
WebRequest.AllowRetryOnTimeout = false;
|
2022-02-17 17:33:27 +08:00
|
|
|
|
|
2023-06-08 07:50:29 +08:00
|
|
|
|
WebRequest.AddHeader(@"Accept-Language", API.Language.ToCultureCode());
|
|
|
|
|
WebRequest.AddHeader(@"x-api-version", API.APIVersion.ToString(CultureInfo.InvariantCulture));
|
2022-02-17 17:33:27 +08:00
|
|
|
|
|
2021-11-03 03:09:31 +08:00
|
|
|
|
if (!string.IsNullOrEmpty(API.AccessToken))
|
2023-06-08 07:50:29 +08:00
|
|
|
|
WebRequest.AddHeader(@"Authorization", $@"Bearer {API.AccessToken}");
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-07-28 22:58:57 +08:00
|
|
|
|
if (isFailing) return;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2022-06-02 13:28:48 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
Logger.Log($@"Performing request {this}", LoggingTarget.Network);
|
|
|
|
|
WebRequest.Perform();
|
|
|
|
|
}
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
{
|
|
|
|
|
// ignore this. internally Perform is running async and the fail state may have changed since
|
|
|
|
|
// the last check of `isFailing` above.
|
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-07-28 22:58:57 +08:00
|
|
|
|
if (isFailing) return;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2020-04-20 16:48:02 +08:00
|
|
|
|
PostProcess();
|
|
|
|
|
|
2021-07-28 22:58:57 +08:00
|
|
|
|
TriggerSuccess();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-20 16:48:02 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
/// Perform any post-processing actions after a successful request.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected virtual void PostProcess()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2021-07-22 13:23:24 +08:00
|
|
|
|
internal void TriggerSuccess()
|
2020-04-11 17:02:43 +08:00
|
|
|
|
{
|
2021-07-19 19:27:00 +08:00
|
|
|
|
lock (completionStateLock)
|
|
|
|
|
{
|
2021-08-19 18:55:10 +08:00
|
|
|
|
if (CompletionState != APIRequestCompletionState.Waiting)
|
2021-07-19 19:27:00 +08:00
|
|
|
|
return;
|
|
|
|
|
|
2021-08-19 18:55:10 +08:00
|
|
|
|
CompletionState = APIRequestCompletionState.Completed;
|
2021-07-19 19:27:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-07-28 22:58:57 +08:00
|
|
|
|
if (API == null)
|
|
|
|
|
Success?.Invoke();
|
|
|
|
|
else
|
|
|
|
|
API.Schedule(() => Success?.Invoke());
|
2020-04-11 17:02:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
2020-07-31 19:55:26 +08:00
|
|
|
|
internal void TriggerFailure(Exception e)
|
|
|
|
|
{
|
2021-07-19 19:27:00 +08:00
|
|
|
|
lock (completionStateLock)
|
|
|
|
|
{
|
2021-08-19 18:55:10 +08:00
|
|
|
|
if (CompletionState != APIRequestCompletionState.Waiting)
|
2021-07-19 19:27:00 +08:00
|
|
|
|
return;
|
|
|
|
|
|
2021-08-19 18:55:10 +08:00
|
|
|
|
CompletionState = APIRequestCompletionState.Failed;
|
2021-07-19 19:27:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-07-28 22:58:57 +08:00
|
|
|
|
if (API == null)
|
|
|
|
|
Failure?.Invoke(e);
|
|
|
|
|
else
|
|
|
|
|
API.Schedule(() => Failure?.Invoke(e));
|
2020-07-31 19:55:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
|
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
|
|
|
|
|
|
|
|
|
|
public void Fail(Exception e)
|
|
|
|
|
{
|
2021-07-19 19:27:00 +08:00
|
|
|
|
lock (completionStateLock)
|
|
|
|
|
{
|
2021-08-19 18:55:10 +08:00
|
|
|
|
if (CompletionState != APIRequestCompletionState.Waiting)
|
2021-07-19 19:27:00 +08:00
|
|
|
|
return;
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-07-28 22:58:57 +08:00
|
|
|
|
WebRequest?.Abort();
|
2018-04-13 17:19:50 +08:00
|
|
|
|
|
2021-07-28 22:58:57 +08:00
|
|
|
|
// in the case of a cancellation we don't care about whether there's an error in the response.
|
|
|
|
|
if (!(e is OperationCanceledException))
|
2019-11-12 18:34:20 +08:00
|
|
|
|
{
|
2021-07-28 22:58:57 +08:00
|
|
|
|
string responseString = WebRequest?.GetResponseString();
|
|
|
|
|
|
|
|
|
|
// naive check whether there's an error in the response to avoid unnecessary JSON deserialisation.
|
|
|
|
|
if (!string.IsNullOrEmpty(responseString) && responseString.Contains(@"""error"""))
|
2021-07-28 19:13:40 +08:00
|
|
|
|
{
|
2021-07-28 22:58:57 +08:00
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// attempt to decode a displayable error string.
|
|
|
|
|
var error = JsonConvert.DeserializeObject<DisplayableError>(responseString);
|
|
|
|
|
if (error != null)
|
|
|
|
|
e = new APIException(error.ErrorMessage, e);
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
}
|
2021-07-28 19:13:40 +08:00
|
|
|
|
}
|
2019-11-12 18:34:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-07-28 22:58:57 +08:00
|
|
|
|
Logger.Log($@"Failing request {this} ({e})", LoggingTarget.Network);
|
|
|
|
|
TriggerFailure(e);
|
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2021-07-28 22:58:57 +08:00
|
|
|
|
/// Whether this request is in a failing or failed state.
|
2018-04-13 17:19:50 +08:00
|
|
|
|
/// </summary>
|
2021-07-28 22:58:57 +08:00
|
|
|
|
private bool isFailing
|
2018-04-13 17:19:50 +08:00
|
|
|
|
{
|
2021-07-28 22:58:57 +08:00
|
|
|
|
get
|
2021-07-19 19:27:00 +08:00
|
|
|
|
{
|
2021-07-28 22:58:57 +08:00
|
|
|
|
lock (completionStateLock)
|
2021-08-19 18:55:10 +08:00
|
|
|
|
return CompletionState == APIRequestCompletionState.Failed;
|
2021-07-19 19:27:00 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
2019-11-12 18:34:20 +08:00
|
|
|
|
|
|
|
|
|
private class DisplayableError
|
|
|
|
|
{
|
|
|
|
|
[JsonProperty("error")]
|
2019-11-25 10:30:55 +08:00
|
|
|
|
public string ErrorMessage { get; set; }
|
2019-11-12 18:34:20 +08:00
|
|
|
|
}
|
2018-04-13 17:19:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public delegate void APIFailureHandler(Exception e);
|
2019-02-28 12:31:40 +08:00
|
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
|
public delegate void APISuccessHandler();
|
2019-02-28 12:31:40 +08:00
|
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
|
public delegate void APIProgressHandler(long current, long total);
|
2019-02-28 12:31:40 +08:00
|
|
|
|
|
2018-04-13 17:19:50 +08:00
|
|
|
|
public delegate void APISuccessHandler<in T>(T content);
|
|
|
|
|
}
|