1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-14 21:17:33 +08:00
osu-lazer/osu.Game/Online/API/APIAccess.cs

328 lines
11 KiB
C#
Raw Normal View History

// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
2016-08-31 18:49:34 +08:00
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
2016-11-30 15:54:15 +08:00
using System.Diagnostics;
2016-08-31 18:49:34 +08:00
using System.Net;
using System.Threading;
2016-09-27 18:22:02 +08:00
using osu.Framework;
using osu.Framework.Configuration;
2016-08-31 18:49:34 +08:00
using osu.Framework.Logging;
2016-09-27 18:22:02 +08:00
using osu.Framework.Threading;
2016-08-31 18:49:34 +08:00
using osu.Game.Online.API.Requests;
2017-03-27 23:04:07 +08:00
using osu.Game.Users;
2016-08-31 18:49:34 +08:00
namespace osu.Game.Online.API
{
2016-09-27 18:22:02 +08:00
public class APIAccess : IUpdateable
2016-08-31 18:49:34 +08:00
{
private readonly OAuth authentication;
2016-08-31 18:49:34 +08:00
2017-05-16 20:37:55 +08:00
public string Endpoint = @"https://osu.ppy.sh";
2017-03-07 09:59:19 +08:00
private const string client_id = @"5";
private const string client_secret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
2016-08-31 18:49:34 +08:00
2017-03-07 09:59:19 +08:00
private ConcurrentQueue<APIRequest> queue = new ConcurrentQueue<APIRequest>();
2016-08-31 18:49:34 +08:00
2016-09-27 18:22:02 +08:00
public Scheduler Scheduler = new Scheduler();
2016-08-31 18:49:34 +08:00
public string Username;
//private SecurePassword password;
2016-08-31 18:49:34 +08:00
public string Password;
2016-08-31 18:49:34 +08:00
2017-05-16 19:08:22 +08:00
public Bindable<User> LocalUser = new Bindable<User>(createGuestUser());
2016-08-31 18:49:34 +08:00
public string Token
{
get { return authentication.Token?.ToString(); }
2017-03-07 09:59:19 +08:00
set { authentication.Token = string.IsNullOrEmpty(value) ? null : OAuthToken.Parse(value); }
2016-08-31 18:49:34 +08:00
}
protected bool HasLogin => Token != null || !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password);
2016-08-31 18:49:34 +08:00
2017-03-07 09:59:19 +08:00
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable (should dispose of this or at very least keep a reference).
private readonly Thread thread;
2016-08-31 18:49:34 +08:00
private readonly Logger log;
2016-08-31 18:49:34 +08:00
public APIAccess()
2016-08-31 18:49:34 +08:00
{
2017-02-07 15:15:45 +08:00
authentication = new OAuth(client_id, client_secret, Endpoint);
2016-08-31 18:49:34 +08:00
log = Logger.GetLogger(LoggingTarget.Network);
thread = new Thread(run) { IsBackground = true };
thread.Start();
}
private readonly List<IOnlineComponent> components = new List<IOnlineComponent>();
public void Register(IOnlineComponent component)
{
Scheduler.Add(delegate
{
components.Add(component);
component.APIStateChanged(this, state);
});
}
public void Unregister(IOnlineComponent component)
{
Scheduler.Add(delegate
{
components.Remove(component);
});
}
public string AccessToken => authentication.RequestAccessToken();
2016-08-31 18:49:34 +08:00
/// <summary>
/// Number of consecutive requests which failed due to network issues.
/// </summary>
2017-03-07 09:59:19 +08:00
private int failureCount;
2016-08-31 18:49:34 +08:00
private void run()
{
2017-03-07 09:59:19 +08:00
while (thread.IsAlive)
2016-08-31 18:49:34 +08:00
{
switch (State)
{
case APIState.Failing:
//todo: replace this with a ping request.
2017-03-07 09:59:19 +08:00
log.Add(@"In a failing state, waiting a bit before we try again...");
2016-08-31 18:49:34 +08:00
Thread.Sleep(5000);
if (queue.Count == 0)
{
2017-03-07 09:59:19 +08:00
log.Add(@"Queueing a ping request");
Queue(new ListChannelsRequest { Timeout = 5000 });
2016-08-31 18:49:34 +08:00
}
break;
case APIState.Offline:
case APIState.Connecting:
2016-08-31 18:49:34 +08:00
//work to restore a connection...
if (!HasLogin)
{
State = APIState.Offline;
Thread.Sleep(50);
2016-08-31 18:49:34 +08:00
continue;
}
State = APIState.Connecting;
2016-08-31 18:49:34 +08:00
if (!authentication.HasValidAccessToken && !authentication.AuthenticateWithLogin(Username, Password))
2016-08-31 18:49:34 +08:00
{
//todo: this fails even on network-related issues. we should probably handle those differently.
//NotificationOverlay.ShowMessage("Login failed!");
2016-08-31 18:49:34 +08:00
log.Add(@"Login failed!");
Password = null;
authentication.Clear();
2016-08-31 18:49:34 +08:00
continue;
}
var userReq = new GetUserRequest();
userReq.Success += u =>
{
LocalUser.Value = u;
failureCount = 0;
//we're connected!
State = APIState.Online;
};
if (!handleRequest(userReq))
{
Thread.Sleep(500);
continue;
}
// 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 != APIState.Online)
Thread.Sleep(500);
2016-08-31 18:49:34 +08:00
break;
}
//hard bail if we can't get a valid access token.
if (authentication.RequestAccessToken() == null)
{
Logout(false);
2016-08-31 18:49:34 +08:00
State = APIState.Offline;
continue;
}
//process the request queue.
APIRequest req;
while (queue.TryPeek(out req))
{
if (handleRequest(req))
{
//we have succeeded, so let's unqueue.
queue.TryDequeue(out req);
}
}
Thread.Sleep(1);
}
}
2016-11-30 15:54:15 +08:00
public void Login(string username, string password)
{
Debug.Assert(State == APIState.Offline);
Username = username;
Password = password;
}
2016-08-31 18:49:34 +08:00
/// <summary>
/// Handle a single API request.
/// </summary>
/// <param name="req">The request.</param>
/// <returns>true if we should remove this request from the queue.</returns>
private bool handleRequest(APIRequest req)
{
try
{
2016-11-30 15:54:15 +08:00
Logger.Log($@"Performing request {req}", LoggingTarget.Network);
2016-08-31 18:49:34 +08:00
req.Perform(this);
//we could still be in initialisation, at which point we don't want to say we're Online yet.
2017-05-16 18:55:45 +08:00
if (IsLoggedIn)
State = APIState.Online;
2016-08-31 18:49:34 +08:00
failureCount = 0;
return true;
}
catch (WebException we)
{
HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode ?? HttpStatusCode.RequestTimeout;
switch (statusCode)
{
case HttpStatusCode.Unauthorized:
Logout(false);
2016-08-31 18:49:34 +08:00
return true;
case HttpStatusCode.RequestTimeout:
failureCount++;
log.Add($@"API failure count is now {failureCount}");
if (failureCount < 3)
//we might try again at an api level.
return false;
State = APIState.Failing;
flushQueue();
2016-08-31 18:49:34 +08:00
return true;
}
req.Fail(we);
return true;
}
catch (Exception e)
{
if (e is TimeoutException)
log.Add(@"API level timeout exception was hit");
req.Fail(e);
return true;
}
}
private APIState state;
public APIState State
{
get { return state; }
private set
2016-08-31 18:49:34 +08:00
{
APIState oldState = state;
APIState newState = value;
state = value;
if (oldState != newState)
{
log.Add($@"We just went {newState}!");
Scheduler.Add(delegate
2016-08-31 18:49:34 +08:00
{
components.ForEach(c => c.APIStateChanged(this, newState));
OnStateChange?.Invoke(oldState, newState);
});
2016-08-31 18:49:34 +08:00
}
}
}
2017-05-16 18:55:45 +08:00
public bool IsLoggedIn => LocalUser.Value.Id > 1;
public void Queue(APIRequest request)
2016-08-31 18:49:34 +08:00
{
queue.Enqueue(request);
}
public event StateChangeDelegate OnStateChange;
2016-08-31 18:49:34 +08:00
public delegate void StateChangeDelegate(APIState oldState, APIState newState);
2016-08-31 18:49:34 +08:00
private void flushQueue(bool failOldRequests = true)
{
var oldQueue = queue;
//flush the queue.
queue = new ConcurrentQueue<APIRequest>();
if (failOldRequests)
{
APIRequest req;
2017-03-07 09:59:19 +08:00
while (oldQueue.TryDequeue(out req))
2017-05-18 02:46:12 +08:00
req.Fail(new WebException(@"Disconnected from server"));
2016-08-31 18:49:34 +08:00
}
}
public void Logout(bool clearUsername = true)
2016-08-31 18:49:34 +08:00
{
flushQueue();
if (clearUsername) Username = null;
Password = null;
2016-08-31 18:49:34 +08:00
authentication.Clear();
2017-05-16 19:08:22 +08:00
LocalUser.Value = createGuestUser();
2016-08-31 18:49:34 +08:00
}
2016-09-27 18:22:02 +08:00
2017-05-16 19:08:22 +08:00
private static User createGuestUser() => new User
2017-05-16 18:49:50 +08:00
{
Username = @"Guest",
Id = 1,
};
2016-09-27 18:22:02 +08:00
public void Update()
{
Scheduler.Update();
}
2016-08-31 18:49:34 +08:00
}
public enum APIState
{
/// <summary>
/// We cannot login (not enough credentials).
/// </summary>
Offline,
/// <summary>
/// We are having connectivity issues.
/// </summary>
Failing,
/// <summary>
/// We are in the process of (re-)connecting.
/// </summary>
Connecting,
/// <summary>
/// We are online.
/// </summary>
Online
}
2016-08-31 18:49:34 +08:00
}