1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-27 13:02:56 +08:00
osu-lazer/osu.Game/Online/API/APIRequest.cs

247 lines
7.9 KiB
C#
Raw Normal View History

// 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;
using JetBrains.Annotations;
2019-11-12 18:34:20 +08:00
using Newtonsoft.Json;
2018-04-13 17:19:50 +08:00
using osu.Framework.IO.Network;
using osu.Framework.Logging;
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>
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
/// <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: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
protected APIRequest()
{
base.Success += () => Success?.Invoke(Response);
}
protected override void PostProcess()
{
base.PostProcess();
if (WebRequest != null)
{
Response = ((OsuJsonWebRequest<T>)WebRequest).ResponseObject;
Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network);
}
}
2020-04-11 17:02:49 +08:00
internal void TriggerSuccess(T result)
{
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
Response = result;
TriggerSuccess();
}
2018-04-13 17:19:50 +08:00
}
/// <summary>
/// AN API request with no specified response type.
/// </summary>
public abstract class APIRequest
{
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
protected virtual string Uri => $@"{API.APIEndpointUrl}/api/v2/{Target}";
2018-04-13 17:19:50 +08:00
protected APIAccess API;
protected WebRequest WebRequest;
/// <summary>
/// The currently logged in user. Note that this will only be populated during <see cref="Perform"/>.
/// </summary>
protected APIUser User { get; private set; }
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;
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>
public APIRequestCompletionState CompletionState { get; private set; }
2018-04-13 17:19:50 +08:00
public void Perform(IAPIProvider api)
2018-04-13 17:19:50 +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;
}
API = apiAccess;
User = apiAccess.LocalUser.Value;
2018-04-13 17:19:50 +08:00
if (isFailing) return;
2018-04-13 17:19:50 +08:00
WebRequest = CreateWebRequest();
WebRequest.Failed += Fail;
2018-04-13 17:19:50 +08:00
WebRequest.AllowRetryOnTimeout = false;
2022-02-17 17:33:27 +08:00
WebRequest.AddHeader("x-api-version", API.APIVersion.ToString(CultureInfo.InvariantCulture));
2021-11-03 03:09:31 +08:00
if (!string.IsNullOrEmpty(API.AccessToken))
WebRequest.AddHeader("Authorization", $"Bearer {API.AccessToken}");
2018-04-13 17:19:50 +08:00
if (isFailing) return;
2018-04-13 17:19:50 +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
if (isFailing) return;
2018-04-13 17:19:50 +08:00
PostProcess();
TriggerSuccess();
2018-04-13 17:19:50 +08:00
}
/// <summary>
/// Perform any post-processing actions after a successful request.
/// </summary>
protected virtual void PostProcess()
{
}
internal void TriggerSuccess()
2020-04-11 17:02:43 +08:00
{
lock (completionStateLock)
{
if (CompletionState != APIRequestCompletionState.Waiting)
return;
CompletionState = APIRequestCompletionState.Completed;
}
if (API == null)
Success?.Invoke();
else
API.Schedule(() => Success?.Invoke());
2020-04-11 17:02:43 +08:00
}
internal void TriggerFailure(Exception e)
{
lock (completionStateLock)
{
if (CompletionState != APIRequestCompletionState.Waiting)
return;
CompletionState = APIRequestCompletionState.Failed;
}
if (API == null)
Failure?.Invoke(e);
else
API.Schedule(() => Failure?.Invoke(e));
}
2018-04-13 17:19:50 +08:00
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
public void Fail(Exception e)
{
lock (completionStateLock)
{
if (CompletionState != APIRequestCompletionState.Waiting)
return;
2018-04-13 17:19:50 +08:00
WebRequest?.Abort();
2018-04-13 17:19:50 +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
{
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"""))
{
try
{
// attempt to decode a displayable error string.
var error = JsonConvert.DeserializeObject<DisplayableError>(responseString);
if (error != null)
e = new APIException(error.ErrorMessage, e);
}
catch
{
}
}
2019-11-12 18:34:20 +08:00
}
Logger.Log($@"Failing request {this} ({e})", LoggingTarget.Network);
TriggerFailure(e);
}
2018-04-13 17:19:50 +08:00
}
/// <summary>
/// Whether this request is in a failing or failed state.
2018-04-13 17:19:50 +08:00
/// </summary>
private bool isFailing
2018-04-13 17:19:50 +08:00
{
get
{
lock (completionStateLock)
return CompletionState == APIRequestCompletionState.Failed;
}
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);
}