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 ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Net ;
2018-12-05 16:13:22 +08:00
using System.Net.Http ;
2020-12-29 14:27:22 +08:00
using System.Net.Sockets ;
2018-04-13 17:19:50 +08:00
using System.Threading ;
2019-11-29 19:03:14 +08:00
using System.Threading.Tasks ;
2024-01-25 04:33:34 +08:00
using JetBrains.Annotations ;
2018-12-05 16:13:22 +08:00
using Newtonsoft.Json.Linq ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2024-01-25 04:33:34 +08:00
using osu.Framework.Extensions ;
2019-12-03 19:20:49 +08:00
using osu.Framework.Extensions.ExceptionExtensions ;
2020-06-09 21:13:48 +08:00
using osu.Framework.Extensions.ObjectExtensions ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics ;
using osu.Framework.Logging ;
using osu.Game.Configuration ;
2023-06-08 07:50:14 +08:00
using osu.Game.Localisation ;
2018-04-13 17:19:50 +08:00
using osu.Game.Online.API.Requests ;
2021-11-04 17:02:44 +08:00
using osu.Game.Online.API.Requests.Responses ;
2022-11-01 20:34:34 +08:00
using osu.Game.Online.Notifications ;
using osu.Game.Online.Notifications.WebSocket ;
2018-04-13 17:19:50 +08:00
using osu.Game.Users ;
namespace osu.Game.Online.API
{
2022-11-24 13:32:20 +08:00
public partial class APIAccess : Component , IAPIProvider
2018-04-13 17:19:50 +08:00
{
2023-06-08 07:50:14 +08:00
private readonly OsuGameBase game ;
2020-02-14 23:18:56 +08:00
private readonly OsuConfigManager config ;
2020-02-14 21:27:21 +08:00
2021-02-14 22:31:57 +08:00
private readonly string versionHash ;
2018-04-13 17:19:50 +08:00
private readonly OAuth authentication ;
2018-09-01 11:55:11 +08:00
private readonly Queue < APIRequest > queue = new Queue < APIRequest > ( ) ;
2018-04-13 17:19:50 +08:00
2020-12-24 17:11:40 +08:00
public string APIEndpointUrl { get ; }
public string WebsiteRootUrl { get ; }
2020-12-24 16:58:38 +08:00
2022-07-12 03:04:21 +08:00
public int APIVersion = > 20220705 ; // We may want to pull this from the game version eventually.
2022-02-17 17:33:27 +08:00
2021-10-04 14:40:24 +08:00
public Exception LastLoginError { get ; private set ; }
2018-04-13 17:19:50 +08:00
public string ProvidedUsername { get ; private set ; }
2023-11-16 15:38:07 +08:00
public string SecondFactorCode { get ; private set ; }
2018-04-13 17:19:50 +08:00
private string password ;
2021-11-04 17:02:44 +08:00
public IBindable < APIUser > LocalUser = > localUser ;
public IBindableList < APIUser > Friends = > friends ;
2020-12-18 14:16:36 +08:00
public IBindable < UserActivity > Activity = > activity ;
2024-01-03 16:37:57 +08:00
public IBindable < UserStatistics > Statistics = > statistics ;
2018-04-13 17:19:50 +08:00
2023-06-08 07:50:14 +08:00
public Language Language = > game . CurrentLanguage . Value ;
2021-11-04 17:02:44 +08:00
private Bindable < APIUser > localUser { get ; } = new Bindable < APIUser > ( createGuestUser ( ) ) ;
2020-12-17 18:30:55 +08:00
2021-11-04 17:02:44 +08:00
private BindableList < APIUser > friends { get ; } = new BindableList < APIUser > ( ) ;
2020-12-18 14:16:36 +08:00
private Bindable < UserActivity > activity { get ; } = new Bindable < UserActivity > ( ) ;
2019-06-12 17:04:57 +08:00
2024-01-02 21:04:40 +08:00
private Bindable < UserStatus ? > configStatus { get ; } = new Bindable < UserStatus ? > ( ) ;
private Bindable < UserStatus ? > localUserStatus { get ; } = new Bindable < UserStatus ? > ( ) ;
2024-01-03 16:37:57 +08:00
private Bindable < UserStatistics > statistics { get ; } = new Bindable < UserStatistics > ( ) ;
2019-06-11 16:28:16 +08:00
protected bool HasLogin = > authentication . Token . Value ! = null | | ( ! string . IsNullOrEmpty ( ProvidedUsername ) & & ! string . IsNullOrEmpty ( password ) ) ;
2018-04-13 17:19:50 +08:00
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource ( ) ;
private readonly Logger log ;
2024-01-25 04:33:34 +08:00
private string webSocketEndpointUrl ;
[CanBeNull]
private OsuClientWebSocket webSocket ;
2023-06-08 07:50:14 +08:00
public APIAccess ( OsuGameBase game , OsuConfigManager config , EndpointConfiguration endpointConfiguration , string versionHash )
2018-04-13 17:19:50 +08:00
{
2023-06-08 07:50:14 +08:00
this . game = game ;
2018-04-13 17:19:50 +08:00
this . config = config ;
2021-02-14 22:31:57 +08:00
this . versionHash = versionHash ;
2018-04-13 17:19:50 +08:00
2020-12-24 17:11:40 +08:00
APIEndpointUrl = endpointConfiguration . APIEndpointUrl ;
WebsiteRootUrl = endpointConfiguration . WebsiteRootUrl ;
2024-01-25 04:33:34 +08:00
webSocketEndpointUrl = endpointConfiguration . NotificationsWebSocketEndpointUrl ;
2020-12-24 16:58:38 +08:00
2020-12-24 17:11:40 +08:00
authentication = new OAuth ( endpointConfiguration . APIClientID , endpointConfiguration . APIClientSecret , APIEndpointUrl ) ;
2018-04-13 17:19:50 +08:00
log = Logger . GetLogger ( LoggingTarget . Network ) ;
ProvidedUsername = config . Get < string > ( OsuSetting . Username ) ;
authentication . TokenString = config . Get < string > ( OsuSetting . Token ) ;
authentication . Token . ValueChanged + = onTokenChanged ;
2024-01-02 21:04:40 +08:00
config . BindWith ( OsuSetting . UserOnlineStatus , configStatus ) ;
2020-12-18 14:16:36 +08:00
localUser . BindValueChanged ( u = >
2019-06-12 17:04:57 +08:00
{
2020-12-18 14:16:36 +08:00
u . OldValue ? . Activity . UnbindFrom ( activity ) ;
u . NewValue . Activity . BindTo ( activity ) ;
2024-01-02 21:04:40 +08:00
if ( u . OldValue ! = null )
localUserStatus . UnbindFrom ( u . OldValue . Status ) ;
localUserStatus . BindTo ( u . NewValue . Status ) ;
2019-06-12 17:04:57 +08:00
} , true ) ;
2024-01-02 21:04:40 +08:00
localUserStatus . BindValueChanged ( val = > configStatus . Value = val . NewValue ) ;
2018-09-01 12:00:55 +08:00
var thread = new Thread ( run )
{
Name = "APIAccess" ,
IsBackground = true
} ;
thread . Start ( ) ;
2018-04-13 17:19:50 +08:00
}
2021-03-17 15:10:16 +08:00
private void onTokenChanged ( ValueChangedEvent < OAuthToken > e ) = > config . SetValue ( OsuSetting . Token , config . Get < bool > ( OsuSetting . SavePassword ) ? authentication . TokenString : string . Empty ) ;
2018-04-13 17:19:50 +08:00
internal new void Schedule ( Action action ) = > base . Schedule ( action ) ;
public string AccessToken = > authentication . RequestAccessToken ( ) ;
/// <summary>
/// Number of consecutive requests which failed due to network issues.
/// </summary>
private int failureCount ;
2022-08-11 14:43:39 +08:00
/// <summary>
/// The main API thread loop, which will continue to run until the game is shut down.
/// </summary>
2018-04-13 17:19:50 +08:00
private void run ( )
{
while ( ! cancellationToken . IsCancellationRequested )
{
2022-08-11 14:43:39 +08:00
if ( state . Value = = APIState . Failing )
{
// To recover from a failing state, falling through and running the full reconnection process seems safest for now.
// This could probably be replaced with a ping-style request if we want to avoid the reconnection overheads.
log . Add ( $@"{nameof(APIAccess)} is in a failing state, waiting a bit before we try again..." ) ;
Thread . Sleep ( 5000 ) ;
}
// Ensure that we have valid credentials.
// If not, setting the offline state will allow the game to prompt the user to provide new credentials.
if ( ! HasLogin )
2018-04-13 17:19:50 +08:00
{
2022-08-11 14:43:39 +08:00
state . Value = APIState . Offline ;
Thread . Sleep ( 50 ) ;
continue ;
}
Debug . Assert ( HasLogin ) ;
// Ensure that we are in an online state. If not, attempt a connect.
if ( state . Value ! = APIState . Online )
{
attemptConnect ( ) ;
if ( state . Value ! = APIState . Online )
continue ;
2018-04-13 17:19:50 +08:00
}
2020-05-05 09:31:11 +08:00
// hard bail if we can't get a valid access token.
2018-04-13 17:19:50 +08:00
if ( authentication . RequestAccessToken ( ) = = null )
{
2018-12-22 16:54:19 +08:00
Logout ( ) ;
2018-04-13 17:19:50 +08:00
continue ;
}
2022-08-11 14:43:39 +08:00
processQueuedRequests ( ) ;
Thread . Sleep ( 50 ) ;
}
}
/// <summary>
/// Dequeue from the queue and run each request synchronously until the queue is empty.
/// </summary>
private void processQueuedRequests ( )
{
while ( true )
{
APIRequest req ;
lock ( queue )
2018-09-06 16:38:15 +08:00
{
2022-08-11 14:43:39 +08:00
if ( queue . Count = = 0 ) return ;
2018-09-01 11:55:11 +08:00
2022-08-11 14:43:39 +08:00
req = queue . Dequeue ( ) ;
}
2019-02-28 12:31:40 +08:00
2022-08-11 14:43:39 +08:00
handleRequest ( req ) ;
}
}
2018-09-01 11:55:11 +08:00
2022-08-11 14:43:39 +08:00
/// <summary>
/// From a non-connected state, perform a full connection flow, obtaining OAuth tokens and populating the local user and friends.
/// </summary>
/// <remarks>
/// This method takes control of <see cref="state"/> and transitions from <see cref="APIState.Connecting"/> to either
2023-11-16 15:38:07 +08:00
/// - <see cref="APIState.RequiresSecondFactorAuth"/> (pending 2fa)
2022-08-11 14:43:39 +08:00
/// - <see cref="APIState.Online"/> (successful connection)
/// - <see cref="APIState.Failing"/> (failed connection but retrying)
/// - <see cref="APIState.Offline"/> (failed and can't retry, clear credentials and require user interaction)
/// </remarks>
/// <returns>Whether the connection attempt was successful.</returns>
private void attemptConnect ( )
{
if ( localUser . IsDefault )
{
// Show a placeholder user if saved credentials are available.
// This is useful for storing local scores and showing a placeholder username after starting the game,
// until a valid connection has been established.
setLocalUser ( new APIUser
{
Username = ProvidedUsername ,
2024-01-02 21:04:40 +08:00
Status = { Value = configStatus . Value ? ? UserStatus . Online }
2022-08-11 14:43:39 +08:00
} ) ;
}
// save the username at this point, if the user requested for it to be.
config . SetValue ( OsuSetting . Username , config . Get < bool > ( OsuSetting . SaveUsername ) ? ProvidedUsername : string . Empty ) ;
if ( ! authentication . HasValidAccessToken )
{
2023-11-16 15:38:07 +08:00
state . Value = APIState . Connecting ;
2022-08-11 14:43:39 +08:00
LastLoginError = null ;
try
{
authentication . AuthenticateWithLogin ( ProvidedUsername , password ) ;
2018-04-13 17:19:50 +08:00
}
2022-08-11 14:43:39 +08:00
catch ( Exception e )
{
//todo: this fails even on network-related issues. we should probably handle those differently.
LastLoginError = e ;
log . Add ( $@"Login failed for username {ProvidedUsername} ({LastLoginError.Message})!" ) ;
2018-04-13 17:19:50 +08:00
2022-08-11 14:43:39 +08:00
Logout ( ) ;
return ;
}
2018-04-13 17:19:50 +08:00
}
2020-12-18 14:19:38 +08:00
2024-01-24 21:22:57 +08:00
var userReq = new GetMeRequest ( ) ;
2022-08-11 14:43:39 +08:00
userReq . Failure + = ex = >
2020-12-18 14:19:38 +08:00
{
2022-08-11 14:43:39 +08:00
if ( ex is APIException )
{
LastLoginError = ex ;
log . Add ( $@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!" ) ;
Logout ( ) ;
}
else if ( ex is WebException webException & & webException . Message = = @"Unauthorized" )
{
log . Add ( @"Login no longer valid" ) ;
Logout ( ) ;
}
else
{
2020-12-18 14:19:38 +08:00
state . Value = APIState . Failing ;
2022-08-11 14:43:39 +08:00
}
} ;
2024-01-24 22:57:40 +08:00
userReq . Success + = me = >
2022-08-11 14:43:39 +08:00
{
2024-01-24 22:57:40 +08:00
me . Status . Value = configStatus . Value ? ? UserStatus . Online ;
2022-08-11 14:43:39 +08:00
2024-01-24 22:57:40 +08:00
setLocalUser ( me ) ;
2022-08-11 14:43:39 +08:00
2024-01-25 04:33:34 +08:00
if ( me . SessionVerified )
state . Value = APIState . Online ;
else
setUpSecondFactorAuthentication ( ) ;
2022-08-11 14:43:39 +08:00
failureCount = 0 ;
} ;
if ( ! handleRequest ( userReq ) )
{
state . Value = APIState . Failing ;
return ;
2020-12-18 14:19:38 +08:00
}
2022-08-11 14:43:39 +08:00
2024-01-24 22:57:40 +08:00
if ( state . Value = = APIState . RequiresSecondFactorAuth )
{
if ( string . IsNullOrEmpty ( SecondFactorCode ) )
return ;
state . Value = APIState . Connecting ;
LastLoginError = null ;
var verificationRequest = new VerifySessionRequest ( SecondFactorCode ) ;
verificationRequest . Success + = ( ) = > state . Value = APIState . Online ;
verificationRequest . Failure + = ex = >
{
state . Value = APIState . RequiresSecondFactorAuth ;
LastLoginError = ex ;
SecondFactorCode = null ;
} ;
if ( ! handleRequest ( verificationRequest ) )
{
state . Value = APIState . Failing ;
return ;
}
if ( state . Value ! = APIState . Online )
return ;
}
2022-08-11 14:43:39 +08:00
var friendsReq = new GetFriendsRequest ( ) ;
friendsReq . Failure + = _ = > state . Value = APIState . Failing ;
2023-01-07 03:39:46 +08:00
friendsReq . Success + = res = >
{
friends . Clear ( ) ;
friends . AddRange ( res ) ;
} ;
2022-08-11 14:43:39 +08:00
if ( ! handleRequest ( friendsReq ) )
{
state . Value = APIState . Failing ;
return ;
}
// The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
// Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
// before actually going online.
while ( State . Value = = APIState . Connecting & & ! cancellationToken . IsCancellationRequested )
Thread . Sleep ( 500 ) ;
2018-04-13 17:19:50 +08:00
}
2019-11-29 19:03:14 +08:00
public void Perform ( APIRequest request )
{
try
{
request . Perform ( this ) ;
}
catch ( Exception e )
{
// todo: fix exception handling
request . Fail ( e ) ;
}
}
public Task PerformAsync ( APIRequest request ) = >
Task . Factory . StartNew ( ( ) = > Perform ( request ) , TaskCreationOptions . LongRunning ) ;
2018-04-13 17:19:50 +08:00
public void Login ( string username , string password )
{
2020-10-22 13:19:12 +08:00
Debug . Assert ( State . Value = = APIState . Offline ) ;
2018-04-13 17:19:50 +08:00
ProvidedUsername = username ;
this . password = password ;
}
2024-01-25 04:33:34 +08:00
private void setUpSecondFactorAuthentication ( )
{
if ( state . Value = = APIState . RequiresSecondFactorAuth )
return ;
state . Value = APIState . RequiresSecondFactorAuth ;
try
{
webSocket ? . DisposeAsync ( ) . AsTask ( ) . WaitSafely ( ) ;
var newSocket = new OsuClientWebSocket ( this , webSocketEndpointUrl ) ;
newSocket . MessageReceived + = async msg = >
{
if ( msg . Event = = @"verified" )
{
state . Value = APIState . Online ;
await newSocket . DisposeAsync ( ) . ConfigureAwait ( false ) ;
if ( webSocket = = newSocket )
webSocket = null ;
}
} ;
newSocket . Closed + = ex = >
{
Logger . Error ( ex , "Connection with account verification endpoint closed unexpectedly. Please supply account verification code manually." , LoggingTarget . Network ) ;
return Task . CompletedTask ;
} ;
webSocket = newSocket ;
webSocket . ConnectAsync ( cancellationToken . Token ) . WaitSafely ( ) ;
}
catch ( Exception ex )
{
Logger . Error ( ex , "Failed to set up connection with account verification endpoint. Please supply account verification code manually." , LoggingTarget . Network ) ;
}
}
2023-11-16 15:38:07 +08:00
public void AuthenticateSecondFactor ( string code )
{
Debug . Assert ( State . Value = = APIState . RequiresSecondFactorAuth ) ;
SecondFactorCode = code ;
}
2021-08-02 13:44:51 +08:00
public IHubClientConnector GetHubConnector ( string clientName , string endpoint , bool preferMessagePack ) = >
new HubClientConnector ( clientName , endpoint , this , versionHash , preferMessagePack ) ;
2021-02-15 15:31:00 +08:00
2022-11-01 20:34:34 +08:00
public NotificationsClientConnector GetNotificationsConnector ( ) = >
new WebSocketNotificationsClientConnector ( this ) ;
2018-12-05 16:13:22 +08:00
public RegistrationRequest . RegistrationRequestErrors CreateAccount ( string email , string username , string password )
2018-12-04 19:33:29 +08:00
{
2020-10-22 13:19:12 +08:00
Debug . Assert ( State . Value = = APIState . Offline ) ;
2018-12-05 12:08:35 +08:00
2018-12-05 16:13:22 +08:00
var req = new RegistrationRequest
{
2020-12-24 17:11:40 +08:00
Url = $@"{APIEndpointUrl}/users" ,
2018-12-05 16:13:22 +08:00
Method = HttpMethod . Post ,
Username = username ,
Email = email ,
Password = password
} ;
try
{
req . Perform ( ) ;
}
catch ( Exception e )
{
try
{
2023-01-13 14:32:53 +08:00
return JObject . Parse ( req . GetResponseString ( ) . AsNonNull ( ) ) . SelectToken ( @"form_error" , true ) . AsNonNull ( ) . ToObject < RegistrationRequest . RegistrationRequestErrors > ( ) ;
2018-12-05 16:13:22 +08:00
}
catch
{
2023-01-13 14:32:53 +08:00
try
{
// attempt to parse a non-form error message
var response = JObject . Parse ( req . GetResponseString ( ) . AsNonNull ( ) ) ;
string redirect = ( string ) response . SelectToken ( @"url" , true ) ;
string message = ( string ) response . SelectToken ( @"error" , false ) ;
if ( ! string . IsNullOrEmpty ( redirect ) )
{
return new RegistrationRequest . RegistrationRequestErrors
{
Redirect = redirect ,
Message = message ,
} ;
}
// if we couldn't deserialize the error message let's throw the original exception outwards.
e . Rethrow ( ) ;
}
catch
{
// if we couldn't deserialize the error message let's throw the original exception outwards.
e . Rethrow ( ) ;
}
2018-12-05 16:13:22 +08:00
}
}
2018-12-05 12:08:35 +08:00
2018-12-05 16:13:22 +08:00
return null ;
2018-12-04 19:33:29 +08:00
}
2018-04-13 17:19:50 +08:00
/// <summary>
/// Handle a single API request.
2018-12-18 19:19:40 +08:00
/// Ensures all exceptions are caught and dealt with correctly.
2018-04-13 17:19:50 +08:00
/// </summary>
/// <param name="req">The request.</param>
2018-12-17 13:29:11 +08:00
/// <returns>true if the request succeeded.</returns>
2018-04-13 17:19:50 +08:00
private bool handleRequest ( APIRequest req )
{
try
{
req . Perform ( this ) ;
2021-08-20 11:11:41 +08:00
if ( req . CompletionState ! = APIRequestCompletionState . Completed )
return false ;
2022-08-11 13:36:28 +08:00
// Reset failure count if this request succeeded.
2018-04-13 17:19:50 +08:00
failureCount = 0 ;
2021-08-20 11:11:41 +08:00
return true ;
2018-04-13 17:19:50 +08:00
}
2020-12-29 14:27:22 +08:00
catch ( HttpRequestException re )
{
log . Add ( $"{nameof(HttpRequestException)} while performing request {req}: {re.Message}" ) ;
handleFailure ( ) ;
return false ;
}
catch ( SocketException se )
{
log . Add ( $"{nameof(SocketException)} while performing request {req}: {se.Message}" ) ;
handleFailure ( ) ;
return false ;
}
2018-04-13 17:19:50 +08:00
catch ( WebException we )
{
2020-12-29 14:27:22 +08:00
log . Add ( $"{nameof(WebException)} while performing request {req}: {we.Message}" ) ;
2018-12-18 19:19:40 +08:00
handleWebException ( we ) ;
2018-12-17 13:29:11 +08:00
return false ;
2018-04-13 17:19:50 +08:00
}
2019-06-15 23:31:14 +08:00
catch ( Exception ex )
2018-04-13 17:19:50 +08:00
{
2019-06-15 23:31:14 +08:00
Logger . Error ( ex , "Error occurred while handling an API request." ) ;
2018-12-17 13:29:11 +08:00
return false ;
2018-04-13 17:19:50 +08:00
}
}
2020-10-22 13:19:12 +08:00
private readonly Bindable < APIState > state = new Bindable < APIState > ( ) ;
2018-04-13 17:19:50 +08:00
2020-10-22 13:19:12 +08:00
/// <summary>
/// The current connectivity state of the API.
/// </summary>
public IBindable < APIState > State = > state ;
2018-04-13 17:19:50 +08:00
2020-12-29 14:27:22 +08:00
private void handleWebException ( WebException we )
2018-12-14 14:48:34 +08:00
{
HttpStatusCode statusCode = ( we . Response as HttpWebResponse ) ? . StatusCode
? ? ( we . Status = = WebExceptionStatus . UnknownError ? HttpStatusCode . NotAcceptable : HttpStatusCode . RequestTimeout ) ;
// special cases for un-typed but useful message responses.
switch ( we . Message )
{
case "Unauthorized" :
case "Forbidden" :
statusCode = HttpStatusCode . Unauthorized ;
break ;
}
switch ( statusCode )
{
case HttpStatusCode . Unauthorized :
2018-12-22 16:54:19 +08:00
Logout ( ) ;
2020-12-29 14:27:22 +08:00
break ;
2019-04-01 11:16:05 +08:00
2018-12-14 14:48:34 +08:00
case HttpStatusCode . RequestTimeout :
2020-12-29 14:27:22 +08:00
handleFailure ( ) ;
break ;
}
}
2018-12-14 14:48:34 +08:00
2020-12-29 14:27:22 +08:00
private void handleFailure ( )
{
failureCount + + ;
log . Add ( $@"API failure count is now {failureCount}" ) ;
2018-12-19 13:32:43 +08:00
2022-11-18 13:21:37 +08:00
if ( failureCount > = 3 )
2020-12-29 14:27:22 +08:00
{
state . Value = APIState . Failing ;
flushQueue ( ) ;
2018-12-14 14:48:34 +08:00
}
}
2022-08-09 15:38:59 +08:00
public bool IsLoggedIn = > State . Value > APIState . Offline ;
2018-04-13 17:19:50 +08:00
2018-09-01 11:55:11 +08:00
public void Queue ( APIRequest request )
{
2021-02-24 18:57:42 +08:00
lock ( queue )
{
if ( state . Value = = APIState . Offline )
2022-02-03 12:16:54 +08:00
{
2022-02-03 13:09:27 +08:00
request . Fail ( new WebException ( @"User not logged in" ) ) ;
2021-02-24 18:57:42 +08:00
return ;
2022-02-03 12:16:54 +08:00
}
2021-02-24 18:57:42 +08:00
queue . Enqueue ( request ) ;
}
2018-09-01 11:55:11 +08:00
}
2018-04-13 17:19:50 +08:00
private void flushQueue ( bool failOldRequests = true )
{
2018-09-01 11:55:11 +08:00
lock ( queue )
{
var oldQueueRequests = queue . ToArray ( ) ;
2018-04-13 17:19:50 +08:00
2018-09-01 11:55:11 +08:00
queue . Clear ( ) ;
2018-04-13 17:19:50 +08:00
2018-09-01 11:55:11 +08:00
if ( failOldRequests )
{
foreach ( var req in oldQueueRequests )
2022-02-03 13:09:27 +08:00
req . Fail ( new WebException ( $@"Request failed from flush operation (state {state.Value})" ) ) ;
2018-09-01 11:55:11 +08:00
}
2018-04-13 17:19:50 +08:00
}
}
2018-12-22 16:54:19 +08:00
public void Logout ( )
2018-04-13 17:19:50 +08:00
{
password = null ;
2023-11-16 15:38:07 +08:00
SecondFactorCode = null ;
2018-04-13 17:19:50 +08:00
authentication . Clear ( ) ;
2019-05-09 12:32:18 +08:00
2020-12-17 18:30:55 +08:00
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
Schedule ( ( ) = >
{
2022-08-11 11:44:58 +08:00
setLocalUser ( createGuestUser ( ) ) ;
2020-12-18 14:16:36 +08:00
friends . Clear ( ) ;
2020-12-17 18:30:55 +08:00
} ) ;
2019-05-09 12:42:04 +08:00
2020-10-22 13:19:12 +08:00
state . Value = APIState . Offline ;
2021-02-24 18:57:42 +08:00
flushQueue ( ) ;
2018-04-13 17:19:50 +08:00
}
2024-01-03 16:37:57 +08:00
public void UpdateStatistics ( UserStatistics newStatistics )
{
statistics . Value = newStatistics ;
2024-01-03 20:15:32 +08:00
if ( IsLoggedIn )
localUser . Value . Statistics = newStatistics ;
2024-01-03 16:37:57 +08:00
}
2021-11-04 17:02:44 +08:00
private static APIUser createGuestUser ( ) = > new GuestUser ( ) ;
2018-04-13 17:19:50 +08:00
2024-01-03 16:37:57 +08:00
private void setLocalUser ( APIUser user ) = > Scheduler . Add ( ( ) = >
{
localUser . Value = user ;
statistics . Value = user . Statistics ;
} , false ) ;
2022-08-11 11:44:58 +08:00
2018-04-13 17:19:50 +08:00
protected override void Dispose ( bool isDisposing )
{
base . Dispose ( isDisposing ) ;
flushQueue ( ) ;
cancellationToken . Cancel ( ) ;
2024-01-25 04:33:34 +08:00
webSocket ? . DisposeAsync ( ) . AsTask ( ) . WaitSafely ( ) ;
2018-04-13 17:19:50 +08:00
}
}
2021-11-04 17:02:44 +08:00
internal class GuestUser : APIUser
2019-01-23 10:06:29 +08:00
{
public GuestUser ( )
{
Username = @"Guest" ;
2022-05-06 16:32:55 +08:00
Id = SYSTEM_USER_ID ;
2019-01-23 10:06:29 +08:00
}
}
2018-04-13 17:19:50 +08:00
public enum APIState
{
/// <summary>
/// We cannot login (not enough credentials).
/// </summary>
Offline ,
/// <summary>
/// We are having connectivity issues.
/// </summary>
Failing ,
2023-11-15 19:00:09 +08:00
/// <summary>
/// Waiting on second factor authentication.
/// </summary>
2023-11-16 15:38:07 +08:00
RequiresSecondFactorAuth ,
2023-11-15 19:00:09 +08:00
2018-04-13 17:19:50 +08:00
/// <summary>
/// We are in the process of (re-)connecting.
/// </summary>
Connecting ,
/// <summary>
/// We are online.
/// </summary>
Online
}
}