This commit is contained in:
こけっち 2022-07-02 22:17:31 +09:00
parent 646d2a0e1d
commit 35d12d7b1c
No known key found for this signature in database
GPG Key ID: 21460619C5FC4DD1
28 changed files with 2597 additions and 109 deletions

View File

@ -17,7 +17,7 @@ indent_size = 4
trim_trailing_whitespace = true
#license header
file_header_template = Copyright (c) ppy Pty Ltd <contact@ppy.sh>, sim1222 <kokt@sim1222.com>. Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text.
file_header_template = Copyright (c) sim1222 <kokt@sim1222.com>. Licensed under the MIT Licence.\nSee the LICENCE file in the repository root for full licence text.
#Roslyn naming styles

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="accountSettings">
<option name="activeRegion" value="us-east-1" />
<option name="recentlyUsedRegions">
<list>
<option value="us-east-1" />
</list>
</option>
</component>
</project>

View File

@ -65,8 +65,8 @@ namespace osu.Android
Debug.Assert(Window != null);
Window.AddFlags(WindowManagerFlags.Fullscreen);
Window.AddFlags(WindowManagerFlags.KeepScreenOn);
// Window.AddFlags(WindowManagerFlags.Fullscreen);
// Window.AddFlags(WindowManagerFlags.KeepScreenOn);
Debug.Assert(WindowManager?.DefaultDisplay != null);
Debug.Assert(Resources?.DisplayMetrics != null);
@ -76,7 +76,7 @@ namespace osu.Android
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
bool isTablet = smallestWidthDp >= 600f;
RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.User;
}
protected override void OnNewIntent(Intent intent) => handleIntent(intent);

View File

@ -163,6 +163,8 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f);
SetDefault(OsuSetting.MisskeyToken, string.Empty);
}
public IDictionary<OsuSetting, string> GetLoggableState() =>
@ -358,5 +360,9 @@ namespace osu.Game.Configuration
DiscordRichPresence,
AutomaticallyDownloadWhenSpectating,
ShowOnlineExplicitContent,
//Misskey
MisskeyToken,
}
}

View File

@ -0,0 +1,483 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>, sim1222 <kokt@sim1222.com>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Online.MisskeyAPI.Requests;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Users;
namespace osu.Game.Online.MisskeyAPI
{
public class APIAccess : Component, IAPIProvider
{
private readonly OsuConfigManager config;
private readonly string versionHash;
private readonly Auth authentication;
private readonly Queue<APIRequest> queue = new Queue<APIRequest>();
public string APIEndpointUrl { get; }
public int APIVersion => 20220217; // We may want to pull this from the game version eventually.
public Exception LastLoginError { get; private set; }
public string ProvidedUsername { get; private set; }
private string password;
public IBindable<Requests.Responses.I> LocalUser => localUser;
//public IBindableList<I> Friends => friends;
public IBindable<UserActivity> Activity => activity;
private Bindable<Requests.Responses.I> localUser { get; } = new Bindable<Requests.Responses.I>(createGuestUser());
//private BindableList<I> friends { get; } = new BindableList<I>();
private Bindable<UserActivity> activity { get; } = new Bindable<UserActivity>();
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
private readonly Logger log;
public APIAccess(OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash)
{
this.config = config;
this.versionHash = versionHash;
APIEndpointUrl = endpointConfiguration.APIEndpointUrl;
authentication = new Auth(APIEndpointUrl);
log = Logger.GetLogger(LoggingTarget.Network);
ProvidedUsername = config.Get<string>(OsuSetting.Username);
authentication.TokenString = config.Get<string>(OsuSetting.MisskeyToken);
authentication.Token.ValueChanged += onTokenChanged;
var thread = new Thread(run)
{
Name = "APIAccess",
IsBackground = true
};
thread.Start();
}
private void onTokenChanged(ValueChangedEvent<AuthToken> e) => config.SetValue(OsuSetting.MisskeyToken, config.Get<bool>(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty);
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;
private void run()
{
while (!cancellationToken.IsCancellationRequested)
{
switch (State.Value)
{
case APIState.Failing:
//todo: replace this with a ping request.
log.Add(@"In a failing state, waiting a bit before we try again...");
Thread.Sleep(5000);
if (!IsLoggedIn) goto case APIState.Connecting;
if (queue.Count == 0)
{
log.Add(@"Queueing a ping request");
Queue(new Requests.I());
}
break;
case APIState.Offline:
case APIState.Connecting:
// work to restore a connection...
if (!HasLogin)
{
state.Value = APIState.Offline;
Thread.Sleep(50);
continue;
}
state.Value = APIState.Connecting;
// 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)
{
LastLoginError = null;
try
{
authentication.AuthenticateWithLogin(ProvidedUsername, password);
}
catch (Exception e)
{
//todo: this fails even on network-related issues. we should probably handle those differently.
LastLoginError = e;
log.Add(@"Login failed!");
password = null;
authentication.Clear();
continue;
}
}
var userReq = new Requests.I();
userReq.Failure += ex =>
{
if (ex is WebException webException && webException.Message == @"Unauthorized")
{
log.Add(@"Login no longer valid");
Logout();
}
else
failConnectionProcess();
};
userReq.Success += u =>
{
localUser.Value = u;
// todo: save/pull from settings
// localUser.Value.Status.Value = new UserStatusOnline();
failureCount = 0;
};
if (!handleRequest(userReq))
{
failConnectionProcess();
continue;
}
// getting user's friends is considered part of the connection process.
// var friendsReq = new GetFriendsRequest();
//
// friendsReq.Failure += _ => failConnectionProcess();
// friendsReq.Success += res =>
// {
// friends.AddRange(res);
//
// //we're connected!
// state.Value = APIState.Online;
// };
//
// if (!handleRequest(friendsReq))
// {
// failConnectionProcess();
// 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.Value > APIState.Offline && State.Value < APIState.Online)
Thread.Sleep(500);
break;
}
// hard bail if we can't get a valid access token.
if (authentication.RequestAccessToken() == null)
{
Logout();
continue;
}
while (true)
{
APIRequest req;
lock (queue)
{
if (queue.Count == 0) break;
req = queue.Dequeue();
}
handleRequest(req);
}
Thread.Sleep(50);
}
void failConnectionProcess()
{
// if something went wrong during the connection process, we want to reset the state (but only if still connecting).
if (State.Value == APIState.Connecting)
state.Value = APIState.Failing;
}
}
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);
public void Login(string username, string password)
{
Debug.Assert(State.Value == APIState.Offline);
ProvidedUsername = username;
this.password = password;
}
// public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack) =>
// new HubClientConnector(clientName, endpoint, this, versionHash, preferMessagePack);
public IHubClientConnector GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true)
{
throw new NotImplementedException();
}
public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password)
{
Debug.Assert(State.Value == APIState.Offline);
var req = new RegistrationRequest
{
Url = $@"{APIEndpointUrl}/users",
Method = HttpMethod.Post,
Username = username,
Email = email,
Password = password
};
try
{
req.Perform();
}
catch (Exception e)
{
try
{
return JObject.Parse(req.GetResponseString().AsNonNull()).SelectToken("form_error", true).AsNonNull().ToObject<RegistrationRequest.RegistrationRequestErrors>();
}
catch
{
// if we couldn't deserialize the error message let's throw the original exception outwards.
e.Rethrow();
}
}
return null;
}
/// <summary>
/// Handle a single API request.
/// Ensures all exceptions are caught and dealt with correctly.
/// </summary>
/// <param name="req">The request.</param>
/// <returns>true if the request succeeded.</returns>
private bool handleRequest(APIRequest req)
{
try
{
req.Perform(this);
if (req.CompletionState != APIRequestCompletionState.Completed)
return false;
// we could still be in initialisation, at which point we don't want to say we're Online yet.
if (IsLoggedIn) state.Value = APIState.Online;
failureCount = 0;
return true;
}
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;
}
catch (WebException we)
{
log.Add($"{nameof(WebException)} while performing request {req}: {we.Message}");
handleWebException(we);
return false;
}
catch (Exception ex)
{
Logger.Error(ex, "Error occurred while handling an API request.");
return false;
}
}
private readonly Bindable<APIState> state = new Bindable<APIState>();
/// <summary>
/// The current connectivity state of the API.
/// </summary>
public IBindable<APIState> State => state;
private void handleWebException(WebException we)
{
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:
Logout();
break;
case HttpStatusCode.RequestTimeout:
handleFailure();
break;
}
}
private void handleFailure()
{
failureCount++;
log.Add($@"API failure count is now {failureCount}");
if (failureCount >= 3 && State.Value == APIState.Online)
{
state.Value = APIState.Failing;
flushQueue();
}
}
public bool IsLoggedIn => localUser.Value.Id != "system" && localUser.Value.Id != "guest"; // TODO: should this also be true if attempting to connect?
public void Queue(APIRequest request)
{
lock (queue)
{
if (state.Value == APIState.Offline)
{
request.Fail(new WebException(@"User not logged in"));
return;
}
queue.Enqueue(request);
}
}
private void flushQueue(bool failOldRequests = true)
{
lock (queue)
{
var oldQueueRequests = queue.ToArray();
queue.Clear();
if (failOldRequests)
{
foreach (var req in oldQueueRequests)
req.Fail(new WebException($@"Request failed from flush operation (state {state.Value})"));
}
}
}
public void Logout()
{
password = null;
authentication.Clear();
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
Schedule(() =>
{
localUser.Value = createGuestUser();
// friends.Clear();
});
state.Value = APIState.Offline;
flushQueue();
}
private static Requests.Responses.I createGuestUser() => new GuestUser();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
flushQueue();
cancellationToken.Cancel();
}
}
internal class GuestUser : Requests.Responses.I
{
public GuestUser()
{
Username = @"Guest";
Id = SYSTEM_USER_ID;
}
}
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
}
}

View File

@ -1,13 +1,15 @@
// 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.
using System;
namespace osu.Game.Online.MisskeyAPI
{
public class Login
public class APIException : InvalidOperationException
{
public void login(string username, string password)
public APIException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}

View File

@ -0,0 +1,236 @@
// 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.
using System;
using System.Globalization;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.IO.Network;
using osu.Framework.Logging;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
namespace osu.Game.Online.MisskeyAPI
{
/// <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
{
protected override WebRequest CreateWebRequest() => new JsonWebRequest<T>(Uri);
/// <summary>
/// The deserialised response object. May be null if the request or deserialisation failed.
/// </summary>
[CanBeNull]
public T Response { get; private set; }
/// <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>
public new event APISuccessHandler<T> Success;
protected APIRequest()
{
base.Success += () => Success?.Invoke(Response);
}
protected override void PostProcess()
{
base.PostProcess();
if (WebRequest != null)
{
Response = ((JsonWebRequest<T>)WebRequest).ResponseObject;
Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network);
}
}
internal void TriggerSuccess(T result)
{
if (Response != null)
throw new InvalidOperationException("Attempted to trigger success more than once");
Response = result;
TriggerSuccess();
}
}
/// <summary>
/// AN API request with no specified response type.
/// </summary>
public abstract class APIRequest
{
protected abstract string Target { get; }
protected virtual WebRequest CreateWebRequest() => new WebRequest(Uri);
protected virtual string Uri => $@"{API.APIEndpointUrl}/api/{Target}";
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; }
/// <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>
public event APISuccessHandler Success;
/// <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>
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; }
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 (isFailing) return;
WebRequest = CreateWebRequest();
WebRequest.Failed += Fail;
WebRequest.AllowRetryOnTimeout = false;
WebRequest.AddHeader("x-api-version", API.APIVersion.ToString(CultureInfo.InvariantCulture));
if (!string.IsNullOrEmpty(API.AccessToken))
// WebRequest.AddRaw("i", $"{API.AccessToken}");
if (isFailing) return;
Logger.Log($@"Performing request {this}", LoggingTarget.Network);
WebRequest.Perform();
if (isFailing) return;
PostProcess();
TriggerSuccess();
}
/// <summary>
/// Perform any post-processing actions after a successful request.
/// </summary>
protected virtual void PostProcess()
{
}
internal void TriggerSuccess()
{
lock (completionStateLock)
{
if (CompletionState != APIRequestCompletionState.Waiting)
return;
CompletionState = APIRequestCompletionState.Completed;
}
if (API == null)
Success?.Invoke();
else
API.Schedule(() => Success?.Invoke());
}
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));
}
public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled"));
public void Fail(Exception e)
{
lock (completionStateLock)
{
if (CompletionState != APIRequestCompletionState.Waiting)
return;
WebRequest?.Abort();
// in the case of a cancellation we don't care about whether there's an error in the response.
if (!(e is OperationCanceledException))
{
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
{
}
}
}
Logger.Log($@"Failing request {this} ({e})", LoggingTarget.Network);
TriggerFailure(e);
}
}
/// <summary>
/// Whether this request is in a failing or failed state.
/// </summary>
private bool isFailing
{
get
{
lock (completionStateLock)
return CompletionState == APIRequestCompletionState.Failed;
}
}
private class DisplayableError
{
[JsonProperty("error")]
public string ErrorMessage { get; set; }
}
}
public delegate void APIFailureHandler(Exception e);
public delegate void APISuccessHandler();
public delegate void APIProgressHandler(long current, long total);
public delegate void APISuccessHandler<in T>(T content);
}

View File

@ -0,0 +1,23 @@
// 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.
namespace osu.Game.Online.MisskeyAPI
{
public enum APIRequestCompletionState
{
/// <summary>
/// Not yet run or currently waiting on response.
/// </summary>
Waiting,
/// <summary>
/// Ran to completion.
/// </summary>
Completed,
/// <summary>
/// Cancelled or failed due to error.
/// </summary>
Failed
}
}

View File

@ -0,0 +1,216 @@
// 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.
using System;
using System.Diagnostics;
using System.Net.Http;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.IO.Network;
namespace osu.Game.Online.MisskeyAPI
{
public class Auth
{
// private readonly string clientId;
private readonly string endpoint;
public readonly Bindable<AuthToken> Token = new Bindable<AuthToken>();
public string TokenString
{
get => Token.Value?.ToString();
set => Token.Value = string.IsNullOrEmpty(value) ? null : AuthToken.Parse(value);
}
internal Auth(
// string clientId,
string endpoint)
{
// Debug.Assert(clientId != null);
Debug.Assert(endpoint != null);
// this.clientId = clientId;
this.endpoint = endpoint;
}
internal void AuthenticateWithLogin(string username, string password)
{
if (string.IsNullOrEmpty(username)) throw new ArgumentException("Missing username.");
if (string.IsNullOrEmpty(password)) throw new ArgumentException("Missing password.");
var accessTokenRequest = new AccessTokenRequestPassword(username, password)
{
Url = $@"{endpoint}/oauth/token",
Method = HttpMethod.Post,
// ClientId = clientId
};
using (accessTokenRequest)
{
try
{
accessTokenRequest.Perform();
}
catch (Exception ex)
{
Token.Value = null;
var throwableException = ex;
try
{
// attempt to decode a displayable error string.
var error = JsonConvert.DeserializeObject<OAuthError>(accessTokenRequest.GetResponseString() ?? string.Empty);
if (error != null)
throwableException = new APIException(error.UserDisplayableError, ex);
}
catch
{
}
throw throwableException;
}
Token.Value = accessTokenRequest.ResponseObject;
}
}
internal bool AuthenticateWithRefresh(string refresh)
{
try
{
var refreshRequest = new AccessTokenRequestRefresh(refresh)
{
Url = $@"{endpoint}/oauth/token",
Method = HttpMethod.Post,
// ClientId = clientId
};
using (refreshRequest)
{
refreshRequest.Perform();
Token.Value = refreshRequest.ResponseObject;
return true;
}
}
catch
{
//todo: potentially only kill the refresh token on certain exception types.
Token.Value = null;
return false;
}
}
private static readonly object access_token_retrieval_lock = new object();
/// <summary>
/// Should be run before any API request to make sure we have a valid key.
/// </summary>
private bool ensureAccessToken()
{
// if we already have a valid access token, let's use it.
if (accessTokenValid) return true;
// we want to ensure only a single authentication update is happening at once.
lock (access_token_retrieval_lock)
{
// re-check if valid, in case another request completed and revalidated our access.
if (accessTokenValid) return true;
// if not, let's try using our refresh token to request a new access token.
if (!string.IsNullOrEmpty(Token.Value?.RefreshToken))
// ReSharper disable once PossibleNullReferenceException
AuthenticateWithRefresh(Token.Value.RefreshToken);
return accessTokenValid;
}
}
private bool accessTokenValid => Token.Value?.IsValid ?? false;
internal bool HasValidAccessToken => RequestAccessToken() != null;
internal string RequestAccessToken()
{
if (!ensureAccessToken()) return null;
return Token.Value.AccessToken;
}
internal void Clear()
{
Token.Value = null;
}
private class AccessTokenRequestRefresh : AccessTokenRequest
{
internal readonly string RefreshToken;
internal AccessTokenRequestRefresh(string refreshToken)
{
RefreshToken = refreshToken;
GrantType = @"refresh_token";
}
protected override void PrePerform()
{
AddParameter("refresh_token", RefreshToken);
base.PrePerform();
}
}
private class AccessTokenRequestPassword : AccessTokenRequest
{
internal readonly string Username;
internal readonly string Password;
internal AccessTokenRequestPassword(string username, string password)
{
Username = username;
Password = password;
GrantType = @"password";
}
protected override void PrePerform()
{
AddParameter("username", Username);
AddParameter("password", Password);
base.PrePerform();
}
}
private class AccessTokenRequest : JsonWebRequest<AuthToken>
{
protected string GrantType;
// internal string ClientId;
protected override void PrePerform()
{
AddParameter("grant_type", GrantType);
// AddParameter("client_id", ClientId);
AddParameter("scope", "*");
base.PrePerform();
}
}
private class OAuthError
{
public string UserDisplayableError => !string.IsNullOrEmpty(Hint) ? Hint : ErrorIdentifier;
[JsonProperty("error")]
public string ErrorIdentifier { get; set; }
[JsonProperty("hint")]
public string Hint { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
}
}
}

View File

@ -0,0 +1,57 @@
// 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.
using System;
using System.Globalization;
using Newtonsoft.Json;
namespace osu.Game.Online.MisskeyAPI
{
[Serializable]
public class AuthToken
{
/// <summary>
/// OAuth 2.0 access token.
/// </summary>
[JsonProperty(@"access_token")]
public string AccessToken;
[JsonProperty(@"expires_in")]
public long ExpiresIn
{
get => AccessTokenExpiry - DateTimeOffset.UtcNow.ToUnixTimeSeconds();
set => AccessTokenExpiry = DateTimeOffset.Now.AddSeconds(value).ToUnixTimeSeconds();
}
public bool IsValid => !string.IsNullOrEmpty(AccessToken) && ExpiresIn > 30;
public long AccessTokenExpiry;
/// <summary>
/// OAuth 2.0 refresh token.
/// </summary>
[JsonProperty(@"refresh_token")]
public string RefreshToken;
public override string ToString() => $@"{AccessToken}|{AccessTokenExpiry.ToString(NumberFormatInfo.InvariantInfo)}|{RefreshToken}";
public static AuthToken Parse(string value)
{
try
{
string[] parts = value.Split('|');
return new AuthToken
{
AccessToken = parts[0],
AccessTokenExpiry = long.Parse(parts[1], NumberFormatInfo.InvariantInfo),
RefreshToken = parts[2]
};
}
catch
{
}
return null;
}
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) sim1222 <kokt@sim1222.com>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.MisskeyAPI
{
public class DefaultEndpointConfigration : EndpointConfiguration
{
public DefaultEndpointConfigration()
{
APIEndpointUrl = "https://misskey.io";
}
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) sim1222 <kokt@sim1222.com>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.MisskeyAPI
{
public class EndpointConfiguration
{
/// <summary>
/// The endpoint for the main (osu-web) API.
/// </summary>
public string APIEndpointUrl { get; set; }
}
}

View File

@ -1,15 +1,123 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// 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.
#nullable enable
using System;
using System.Threading.Tasks;
using osu.Framework.Bindables;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osu.Game.Users;
namespace osu.Game.Online.MisskeyAPI
{
public interface IAPIProvider
{
/// <summary>
/// The local user.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary>
IBindable<Requests.Responses.I> LocalUser { get; }
/// <summary>
/// The current user's activity.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary>
IBindable<UserActivity> Activity { get; }
/// <summary>
/// Retrieve the OAuth access token.
/// </summary>
string AccessToken { get; }
/// <summary>
/// Returns whether the local user is logged in.
/// </summary>
bool IsLoggedIn { get; }
/// <summary>
/// The last username provided by the end-user.
/// May not be authenticated.
/// </summary>
string ProvidedUsername { get; }
/// <summary>
/// The URL endpoint for this API. Does not include a trailing slash.
/// </summary>
string APIEndpointUrl { get; }
// /// <summary>
// /// The root URL of of the website, excluding the trailing slash.
// /// </summary>
// string WebsiteRootUrl { get; }
/// <summary>
/// The version of the API.
/// </summary>
int APIVersion { get; }
/// <summary>
/// The last login error that occurred, if any.
/// </summary>
Exception? LastLoginError { get; }
/// <summary>
/// The current connection state of the API.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary>
IBindable<APIState> State { get; }
/// <summary>
/// Queue a new request.
/// </summary>
/// <param name="request">The request to perform.</param>
void Queue(APIRequest request);
/// <summary>
/// Perform a request immediately, bypassing any API state checks.
/// </summary>
/// <remarks>
/// Can be used to run requests as a guest user.
/// </remarks>
/// <param name="request">The request to perform.</param>
void Perform(APIRequest request);
/// <summary>
/// Perform a request immediately, bypassing any API state checks.
/// </summary>
/// <remarks>
/// Can be used to run requests as a guest user.
/// </remarks>
/// <param name="request">The request to perform.</param>
Task PerformAsync(APIRequest request);
/// <summary>
/// Attempt to login using the provided credentials. This is a non-blocking operation.
/// </summary>
/// <param name="username">The user's username.</param>
/// <param name="password">The user's password.</param>
void Login(string username, string password);
/// <summary>
/// Log out the current user.
/// </summary>
void Logout();
/// <summary>
/// Constructs a new <see cref="IHubClientConnector"/>. May be null if not supported.
/// </summary>
/// <param name="clientName">The name of the client this connector connects for, used for logging.</param>
/// <param name="endpoint">The endpoint to the hub.</param>
/// <param name="preferMessagePack">Whether to use MessagePack for serialisation if available on this platform.</param>
IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack = true);
/// <summary>
/// Create a new user account. This is a blocking operation.
/// </summary>
/// <param name="email">The email to create the account with.</param>
/// <param name="username">The username to create the account with.</param>
/// <param name="password">The password to create the account with.</param>
/// <returns>Any errors encoutnered during account creation.</returns>
RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password);
}
}

View File

@ -0,0 +1,41 @@
// 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.
using Newtonsoft.Json;
using osu.Framework.IO.Network;
namespace osu.Game.Online.MisskeyAPI
{
public class RegistrationRequest : WebRequest
{
internal string Username;
internal string Email;
internal string Password;
protected override void PrePerform()
{
AddParameter("user[username]", Username);
AddParameter("user[user_email]", Email);
AddParameter("user[password]", Password);
base.PrePerform();
}
public class RegistrationRequestErrors
{
public UserErrors User;
public class UserErrors
{
[JsonProperty("username")]
public string[] Username;
[JsonProperty("user_email")]
public string[] Email;
[JsonProperty("password")]
public string[] Password;
}
}
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>, sim1222 <kokt@sim1222.com>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using osu.Framework.IO.Network;
namespace osu.Game.Online.MisskeyAPI.Requests
{
public class I : APIRequest<MisskeyAPI.Requests.Responses.I>
{
public I()
{
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
return req;
}
protected override string Target => @"meta";
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>, sim1222 <kokt@sim1222.com>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Net.Http;
using osu.Framework.IO.Network;
namespace osu.Game.Online.MisskeyAPI.Requests
{
public class Meta : APIRequest<MisskeyAPI.Requests.Responses.Meta>
{
public string instanceUrl { get; }
public Meta(string instanceUrl)
{
this.instanceUrl = instanceUrl;
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
return req;
}
public string APIEndpointUrl => $"https://{instanceUrl}";
protected override string Target => @"meta";
}
}

View File

@ -1,74 +0,0 @@
// 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.
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
namespace osu.Game.Online.MisskeyAPI.Requests.Responses
{
public class UsersShow
{
public partial class Emojis
{
[CanBeNull]
public List<Emoji> Emoji { get; set; }
}
public partial class Emoji
{
public string Name { get; set; }
public Uri Url { get; set; }
}
[JsonProperty("id")]
public string Id;
[JsonProperty("name")]
[CanBeNull]
public string Name;
[JsonProperty("username")]
public string Username;
[JsonProperty("host")]
[CanBeNull]
public string Host;
[JsonProperty("avatarUrl")]
[CanBeNull]
public Uri AvatarUrl;
[JsonProperty("avatarBlurhash")]
[CanBeNull]
public string AvatarBlurhash;
[JsonProperty("avatarColor")]
[CanBeNull]
public string AvatarColor;
[JsonProperty("isAdmin")]
public bool isAdmin;
[JsonProperty("isModerator")]
public bool isModerator;
[JsonProperty("isBot")]
public bool isBot;
[JsonProperty("isCat")]
public bool isCat;
[JsonProperty("emojis")]
public Emojis emojis;
[JsonProperty("onlineStatus")]
[CanBeNull]
public string onlineStatus;
}
}

View File

@ -0,0 +1,29 @@
// 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.
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace osu.Game.Online.MisskeyAPI.Requests.Responses
{
public class Emoji : IEquatable<Emoji>
{
[JsonProperty("id")]
public string Id;
[JsonProperty("aliases")]
public List<string> Aliases;
[JsonProperty("category")]
public string Category;
[JsonProperty("host")]
public string Host;
[JsonProperty("url")]
public string Url;
public bool Equals(Emoji other) => Id == other?.Id;
}
}

View File

@ -0,0 +1,574 @@
// 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.
using System;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
namespace osu.Game.Online.MisskeyAPI.Requests.Responses
{
[JsonObject(MemberSerialization.OptIn)]
public class I //: IEquatable<I>
{
/// <summary>
/// A user ID which can be used to represent any system user which is not attached to a user profile.
/// </summary>
public const string SYSTEM_USER_ID = "system";
// Root myDeserializedClass = JsonConvert.DeserializeObject<Root>(myJsonResponse);
public class Channel
{
}
public class Emoji
{
[JsonProperty("name")]
public string Name;
[JsonProperty("url")]
public string Url;
}
public class Field
{
[JsonProperty("name")]
public string Name;
[JsonProperty("value")]
public string Value;
}
public class File
{
[JsonProperty("id")]
public string Id;
[JsonProperty("createdAt")]
public DateTime CreatedAt;
[JsonProperty("name")]
public string Name;
[JsonProperty("type")]
public string Type;
[JsonProperty("md5")]
public string Md5;
[JsonProperty("size")]
public int Size;
[JsonProperty("isSensitive")]
public bool IsSensitive;
[JsonProperty("blurhash")]
public string Blurhash;
[JsonProperty("properties")]
public Properties Properties;
[JsonProperty("url")]
public string Url;
[JsonProperty("thumbnailUrl")]
public string ThumbnailUrl;
[JsonProperty("comment")]
public string Comment;
[JsonProperty("folderId")]
public string FolderId;
[JsonProperty("folder")]
[CanBeNull]
public Folder Folder;
[JsonProperty("userId")]
public string UserId;
[JsonProperty("user")]
[CanBeNull]
public User User;
}
public class Folder
{
[JsonProperty("id")]
public string Id;
[JsonProperty("createdAt")]
public DateTime CreatedAt;
[JsonProperty("name")]
public string Name;
[JsonProperty("foldersCount")]
public int? FoldersCount;
[JsonProperty("filesCount")]
public int? FilesCount;
[JsonProperty("parentId")]
public string ParentId;
[JsonProperty("parent")]
[CanBeNull]
public Parent Parent;
}
public class Integration
{
}
public class MyReaction
{
}
public class Parent
{
}
public class PinnedNote
{
[JsonProperty("id")]
public string Id;
[JsonProperty("createdAt")]
public DateTime CreatedAt;
[JsonProperty("text")]
public string Text;
[JsonProperty("cw")]
[CanBeNull]
public string Cw;
[JsonProperty("userId")]
public string UserId;
[JsonProperty("user")]
public User User;
[JsonProperty("replyId")]
[CanBeNull]
public string ReplyId;
[JsonProperty("renoteId")]
[CanBeNull]
public string RenoteId;
[JsonProperty("reply")]
[CanBeNull]
public Reply Reply;
[JsonProperty("renote")]
[CanBeNull]
public Renote Renote;
[JsonProperty("isHidden")]
public bool? IsHidden;
[JsonProperty("visibility")]
public string Visibility;
[JsonProperty("mentions")]
[CanBeNull]
public List<string> Mentions;
[JsonProperty("visibleUserIds")]
[CanBeNull]
public List<string> VisibleUserIds;
[JsonProperty("fileIds")]
[CanBeNull]
public List<string> FileIds;
[JsonProperty("files")]
[CanBeNull]
public List<File> Files;
[JsonProperty("tags")]
[CanBeNull]
public List<string> Tags;
[JsonProperty("poll")]
[CanBeNull]
public Poll Poll;
[JsonProperty("channelId")]
[CanBeNull]
public string ChannelId;
[JsonProperty("channel")]
[CanBeNull]
public Channel Channel;
[JsonProperty("localOnly")]
public bool? LocalOnly;
[JsonProperty("emojis")]
public List<Emoji> Emojis;
[JsonProperty("reactions")]
public Reactions Reactions;
[JsonProperty("renoteCount")]
public int RenoteCount;
[JsonProperty("repliesCount")]
public int RepliesCount;
[JsonProperty("uri")]
[CanBeNull]
public string Uri;
[JsonProperty("url")]
[CanBeNull]
public string Url;
[JsonProperty("myReaction")]
[CanBeNull]
public MyReaction MyReaction;
}
public class PinnedPages
{
[JsonProperty("id")]
public string Id;
[JsonProperty("createdAt")]
public DateTime CreatedAt;
[JsonProperty("updatedAt")]
public DateTime UpdatedAt;
[JsonProperty("title")]
public string Title;
[JsonProperty("name")]
public string Name;
[JsonProperty("summary")]
public string Summary;
[JsonProperty("content")]
public List<object> Content;
[JsonProperty("variables")]
public List<object> Variables;
[JsonProperty("userId")]
public string UserId;
[JsonProperty("user")]
public User User;
}
public class Poll
{
}
public class Properties
{
[JsonProperty("width")]
public int? Width;
[JsonProperty("height")]
public int? Height;
[JsonProperty("orientation")]
public int? Orientation;
[JsonProperty("avgColor")]
[CanBeNull]
public string AvgColor;
}
public class Reactions
{
}
public class Renote
{
}
public class Reply
{
}
[JsonProperty("id")]
public string Id { get; set; } = "guest";
[JsonProperty("name")]
public string Name;
[JsonProperty("username")]
public string Username;
[JsonProperty("host")]
public string Host;
[JsonProperty("avatarUrl")]
public string AvatarUrl;
[JsonProperty("avatarBlurhash")]
public object AvatarBlurhash;
[JsonProperty("avatarColor")]
public object AvatarColor;
[JsonProperty("isAdmin")]
public bool? IsAdmin;
[JsonProperty("isModerator")]
public bool? IsModerator;
[JsonProperty("isBot")]
public bool? IsBot;
[JsonProperty("isCat")]
public bool? IsCat;
[JsonProperty("emojis")]
public List<Emoji> Emojis;
[JsonProperty("onlineStatus")]
public string OnlineStatus;
[JsonProperty("url")]
public string Url;
[JsonProperty("uri")]
public string Uri;
[JsonProperty("createdAt")]
public DateTime CreatedAt;
[JsonProperty("updatedAt")]
public DateTime UpdatedAt;
[JsonProperty("lastFetchedAt")]
public DateTime LastFetchedAt;
[JsonProperty("bannerUrl")]
public string BannerUrl;
[JsonProperty("bannerBlurhash")]
public object BannerBlurhash;
[JsonProperty("bannerColor")]
public object BannerColor;
[JsonProperty("isLocked")]
public bool IsLocked;
[JsonProperty("isSilenced")]
public bool IsSilenced;
[JsonProperty("isSuspended")]
public bool IsSuspended;
[JsonProperty("description")]
public string Description;
[JsonProperty("location")]
public string Location;
[JsonProperty("birthday")]
public string Birthday;
[JsonProperty("lang")]
public string Lang;
[JsonProperty("fields")]
public List<Field> Fields;
[JsonProperty("followersCount")]
public int FollowersCount;
[JsonProperty("followingCount")]
public int FollowingCount;
[JsonProperty("notesCount")]
public int NotesCount;
[JsonProperty("pinnedNoteIds")]
public List<string> PinnedNoteIds;
[JsonProperty("pinnedNotes")]
public List<PinnedNote> PinnedNotes;
[JsonProperty("pinnedPageId")]
public string PinnedPageId;
[JsonProperty("pinnedPage")]
public PinnedPages PinnedPage;
[JsonProperty("publicReactions")]
public bool PublicReactions;
[JsonProperty("twoFactorEnabled")]
public bool TwoFactorEnabled;
[JsonProperty("usePasswordLessLogin")]
public bool UsePasswordLessLogin;
[JsonProperty("securityKeys")]
public bool SecurityKeys;
[JsonProperty("isFollowing")]
public bool? IsFollowing;
[JsonProperty("isFollowed")]
public bool? IsFollowed;
[JsonProperty("hasPendingFollowRequestFromYou")]
public bool? HasPendingFollowRequestFromYou;
[JsonProperty("hasPendingFollowRequestToYou")]
public bool? HasPendingFollowRequestToYou;
[JsonProperty("isBlocking")]
public bool? IsBlocking;
[JsonProperty("isBlocked")]
public bool? IsBlocked;
[JsonProperty("isMuted")]
public bool? IsMuted;
[JsonProperty("avatarId")]
public string AvatarId;
[JsonProperty("bannerId")]
public string BannerId;
[JsonProperty("injectFeaturedNote")]
public bool InjectFeaturedNote;
[JsonProperty("receiveAnnouncementEmail")]
public bool ReceiveAnnouncementEmail;
[JsonProperty("alwaysMarkNsfw")]
public bool AlwaysMarkNsfw;
[JsonProperty("carefulBot")]
public bool CarefulBot;
[JsonProperty("autoAcceptFollowed")]
public bool AutoAcceptFollowed;
[JsonProperty("noCrawle")]
public bool NoCrawle;
[JsonProperty("isExplorable")]
public bool IsExplorable;
[JsonProperty("isDeleted")]
public bool IsDeleted;
[JsonProperty("hideOnlineStatus")]
public bool HideOnlineStatus;
[JsonProperty("hasUnreadSpecifiedNotes")]
public bool HasUnreadSpecifiedNotes;
[JsonProperty("hasUnreadMentions")]
public bool HasUnreadMentions;
[JsonProperty("hasUnreadAnnouncement")]
public bool HasUnreadAnnouncement;
[JsonProperty("hasUnreadAntenna")]
public bool HasUnreadAntenna;
[JsonProperty("hasUnreadChannel")]
public bool HasUnreadChannel;
[JsonProperty("hasUnreadMessagingMessage")]
public bool HasUnreadMessagingMessage;
[JsonProperty("hasUnreadNotification")]
public bool HasUnreadNotification;
[JsonProperty("hasPendingReceivedFollowRequest")]
public bool HasPendingReceivedFollowRequest;
[JsonProperty("integrations")]
public Integration Integrations;
[JsonProperty("mutedWords")]
public List<List<string>> MutedWords;
[JsonProperty("mutedInstances")]
public List<string> MutedInstances;
[JsonProperty("mutingNotificationTypes")]
public List<string> MutingNotificationTypes;
[JsonProperty("emailNotificationTypes")]
public List<string> EmailNotificationTypes;
[JsonProperty("email")]
[CanBeNull]
public string Email;
[JsonProperty("emailVerified")]
public bool? EmailVerified;
[JsonProperty("securityKeysList")]
[CanBeNull]
public List<SecurityKeysLists> SecurityKeysList;
public class SecurityKeysLists
{
}
public class User
{
[JsonProperty("id")]
public string Id;
[JsonProperty("name")]
public string Name;
[JsonProperty("username")]
public string Username;
[JsonProperty("host")]
public string Host;
[JsonProperty("avatarUrl")]
public string AvatarUrl;
[JsonProperty("avatarBlurhash")]
public object AvatarBlurhash;
[JsonProperty("avatarColor")]
public object AvatarColor;
[JsonProperty("isAdmin")]
public bool IsAdmin;
[JsonProperty("isModerator")]
public bool IsModerator;
[JsonProperty("isBot")]
public bool IsBot;
[JsonProperty("isCat")]
public bool IsCat;
[JsonProperty("emojis")]
public List<Emoji> Emojis;
[JsonProperty("onlineStatus")]
public string OnlineStatus;
//public bool Equals(I other) => this.MatchesOnlineID(other);
}
}
}

View File

@ -0,0 +1,110 @@
// 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.
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace osu.Game.Online.MisskeyAPI.Requests.Responses
{
[JsonObject(MemberSerialization.OptIn)]
public class Meta //: IEquatable<Meta>
{
public class Feature
{
[JsonProperty("registration")]
public bool Registration;
[JsonProperty("localTimeLine")]
public bool LocalTimeLine;
[JsonProperty("globalTimeLine")]
public bool GlobalTimeLine;
[JsonProperty("hcaptcha")]
public bool Hcaptcha;
[JsonProperty("recaptcha")]
public bool Recaptcha;
[JsonProperty("serviceWorker")]
public bool ServiceWorker;
[JsonProperty("miauth")]
public bool? Miauth;
}
[JsonProperty("maintainerName")]
public string MaintainerName;
[JsonProperty("maintainerEmail")]
public string MaintainerEmail;
[JsonProperty("version")]
public string Version;
[JsonProperty("name")]
public string Name;
[JsonProperty("uri")]
public string Uri;
[JsonProperty("description")]
public string Description;
[JsonProperty("langs")]
public List<string> Langs;
[JsonProperty("tosUrl")]
public string TosUrl;
[JsonProperty("defaultDarkTheme")]
public string DefaultDarkTheme;
[JsonProperty("defaultLightTheme")]
public string DefaultLightTheme;
[JsonProperty("driveCapacityPerLocalUserMb")]
public int DriveCapacityPerLocalUserMb;
[JsonProperty("emailRequiredForSignup")]
public bool EmailRequiredForSignup;
[JsonProperty("recaptchaSiteKey")]
public string RecaptchaSiteKey;
[JsonProperty("swPublickey")]
public string SwPublickey;
[JsonProperty("mascotImageUrl")]
public string MascotImageUrl;
[JsonProperty("bannerUrl")]
public string BannerUrl;
[JsonProperty("iconUrl")]
public string IconUrl;
[JsonProperty("maxNoteTextLength")]
public int MaxNoteTextLength;
[JsonProperty("emojis")]
public List<Emoji> Emojis;
[JsonProperty("requireSetup")]
public bool RequireSetup;
[JsonProperty("enableEmail")]
public bool EnableEmail;
[JsonProperty("enableServiceWorker")]
public bool EnableServiceWorker;
[JsonProperty("translatorAvailable")]
public bool TranslatorAvailable;
[JsonProperty("features")]
public Feature Features;
}
}

View File

@ -7,13 +7,10 @@ namespace osu.Game.Online.MisskeyAPI.Requests.Responses
{
public class Signin
{
[JsonProperty("id")]
public string Id;
[JsonProperty("i")]
public string i;
}
}

View File

@ -1,10 +1,13 @@
// 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.
namespace osu.Game.Online.MisskeyAPI
using Newtonsoft.Json;
namespace osu.Game.Online.MisskeyAPI.Requests.Responses
{
public class OAuth
public class UsersNotes
{
}
}

View File

@ -0,0 +1,381 @@
// 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.
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
namespace osu.Game.Online.MisskeyAPI.Requests.Responses
{
public class UsersShow
{
// Root myDeserializedClass = JsonConvert.DeserializeObject<Root>(myJsonResponse);
public class Emoji
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
}
public class Field
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("value")]
public string Value { get; set; }
}
public class File
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("md5")]
public string Md5 { get; set; }
[JsonProperty("size")]
public int Size { get; set; }
[JsonProperty("isSensitive")]
public bool IsSensitive { get; set; }
[JsonProperty("blurhash")]
public object Blurhash { get; set; }
[JsonProperty("properties")]
public Properties Properties { get; set; }
[JsonProperty("url")]
public string Url { get; set; }
[JsonProperty("thumbnailUrl")]
public object ThumbnailUrl { get; set; }
[JsonProperty("comment")]
public object Comment { get; set; }
[JsonProperty("folderId")]
public object FolderId { get; set; }
[JsonProperty("folder")]
public object Folder { get; set; }
[JsonProperty("userId")]
public object UserId { get; set; }
[JsonProperty("user")]
public object User { get; set; }
}
public class Integrations
{
}
public class PinnedNote
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonProperty("userId")]
public string UserId { get; set; }
[JsonProperty("user")]
public User User { get; set; }
[JsonProperty("text")]
public string Text { get; set; }
[JsonProperty("cw")]
public object Cw { get; set; }
[JsonProperty("visibility")]
public string Visibility { get; set; }
[JsonProperty("renoteCount")]
public int RenoteCount { get; set; }
[JsonProperty("repliesCount")]
public int RepliesCount { get; set; }
[JsonProperty("reactions")]
public Reactions Reactions { get; set; }
[JsonProperty("emojis")]
public List<Emoji> Emojis { get; set; }
[JsonProperty("fileIds")]
public List<string> FileIds { get; set; }
[JsonProperty("files")]
public List<File> Files { get; set; }
[JsonProperty("replyId")]
public object ReplyId { get; set; }
[JsonProperty("renoteId")]
public object RenoteId { get; set; }
}
public class Properties
{
}
public class Reactions
{
[JsonProperty(":thinkhappy@miss.nem.one:")]
public int ThinkhappyMissNemOne { get; set; }
}
public class Root
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
[JsonProperty("host")]
public object Host { get; set; }
[JsonProperty("avatarUrl")]
public string AvatarUrl { get; set; }
[JsonProperty("avatarBlurhash")]
public string AvatarBlurhash { get; set; }
[JsonProperty("avatarColor")]
public object AvatarColor { get; set; }
[JsonProperty("isAdmin")]
public bool IsAdmin { get; set; }
[JsonProperty("isModerator")]
public bool IsModerator { get; set; }
[JsonProperty("isBot")]
public bool IsBot { get; set; }
[JsonProperty("isCat")]
public bool IsCat { get; set; }
[JsonProperty("emojis")]
public List<Emoji> Emojis { get; set; }
[JsonProperty("onlineStatus")]
public string OnlineStatus { get; set; }
[JsonProperty("url")]
public object Url { get; set; }
[JsonProperty("uri")]
public object Uri { get; set; }
[JsonProperty("createdAt")]
public DateTime CreatedAt { get; set; }
[JsonProperty("updatedAt")]
public DateTime UpdatedAt { get; set; }
[JsonProperty("lastFetchedAt")]
public object LastFetchedAt { get; set; }
[JsonProperty("bannerUrl")]
public string BannerUrl { get; set; }
[JsonProperty("bannerBlurhash")]
public string BannerBlurhash { get; set; }
[JsonProperty("bannerColor")]
public object BannerColor { get; set; }
[JsonProperty("isLocked")]
public bool IsLocked { get; set; }
[JsonProperty("isSilenced")]
public bool IsSilenced { get; set; }
[JsonProperty("isSuspended")]
public bool IsSuspended { get; set; }
[JsonProperty("description")]
public string Description { get; set; }
[JsonProperty("location")]
public string Location { get; set; }
[JsonProperty("birthday")]
public string Birthday { get; set; }
[JsonProperty("lang")]
public object Lang { get; set; }
[JsonProperty("fields")]
public List<Field> Fields { get; set; }
[JsonProperty("followersCount")]
public int FollowersCount { get; set; }
[JsonProperty("followingCount")]
public int FollowingCount { get; set; }
[JsonProperty("notesCount")]
public int NotesCount { get; set; }
[JsonProperty("pinnedNoteIds")]
public List<string> PinnedNoteIds { get; set; }
[JsonProperty("pinnedNotes")]
public List<PinnedNote> PinnedNotes { get; set; }
[JsonProperty("pinnedPageId")]
public object PinnedPageId { get; set; }
[JsonProperty("pinnedPage")]
public object PinnedPage { get; set; }
[JsonProperty("publicReactions")]
public bool PublicReactions { get; set; }
[JsonProperty("ffVisibility")]
public string FfVisibility { get; set; }
[JsonProperty("twoFactorEnabled")]
public bool TwoFactorEnabled { get; set; }
[JsonProperty("usePasswordLessLogin")]
public bool UsePasswordLessLogin { get; set; }
[JsonProperty("securityKeys")]
public bool SecurityKeys { get; set; }
[JsonProperty("avatarId")]
public string AvatarId { get; set; }
[JsonProperty("bannerId")]
public string BannerId { get; set; }
[JsonProperty("injectFeaturedNote")]
public bool InjectFeaturedNote { get; set; }
[JsonProperty("receiveAnnouncementEmail")]
public bool ReceiveAnnouncementEmail { get; set; }
[JsonProperty("alwaysMarkNsfw")]
public bool AlwaysMarkNsfw { get; set; }
[JsonProperty("carefulBot")]
public bool CarefulBot { get; set; }
[JsonProperty("autoAcceptFollowed")]
public bool AutoAcceptFollowed { get; set; }
[JsonProperty("noCrawle")]
public bool NoCrawle { get; set; }
[JsonProperty("isExplorable")]
public bool IsExplorable { get; set; }
[JsonProperty("isDeleted")]
public bool IsDeleted { get; set; }
[JsonProperty("hideOnlineStatus")]
public bool HideOnlineStatus { get; set; }
[JsonProperty("hasUnreadSpecifiedNotes")]
public bool HasUnreadSpecifiedNotes { get; set; }
[JsonProperty("hasUnreadMentions")]
public bool HasUnreadMentions { get; set; }
[JsonProperty("hasUnreadAnnouncement")]
public bool HasUnreadAnnouncement { get; set; }
[JsonProperty("hasUnreadAntenna")]
public bool HasUnreadAntenna { get; set; }
[JsonProperty("hasUnreadChannel")]
public bool HasUnreadChannel { get; set; }
[JsonProperty("hasUnreadMessagingMessage")]
public bool HasUnreadMessagingMessage { get; set; }
[JsonProperty("hasUnreadNotification")]
public bool HasUnreadNotification { get; set; }
[JsonProperty("hasPendingReceivedFollowRequest")]
public bool HasPendingReceivedFollowRequest { get; set; }
[JsonProperty("integrations")]
public Integrations Integrations { get; set; }
[JsonProperty("mutedWords")]
public List<object> MutedWords { get; set; }
[JsonProperty("mutedInstances")]
public List<object> MutedInstances { get; set; }
[JsonProperty("mutingNotificationTypes")]
public List<object> MutingNotificationTypes { get; set; }
[JsonProperty("emailNotificationTypes")]
public List<string> EmailNotificationTypes { get; set; }
[JsonProperty("showTimelineReplies")]
public bool ShowTimelineReplies { get; set; }
}
public class User
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
[JsonProperty("host")]
public object Host { get; set; }
[JsonProperty("avatarUrl")]
public string AvatarUrl { get; set; }
[JsonProperty("avatarBlurhash")]
public string AvatarBlurhash { get; set; }
[JsonProperty("avatarColor")]
public object AvatarColor { get; set; }
[JsonProperty("isAdmin")]
public bool IsAdmin { get; set; }
[JsonProperty("isCat")]
public bool IsCat { get; set; }
[JsonProperty("emojis")]
public List<Emoji> Emojis { get; set; }
[JsonProperty("onlineStatus")]
public string OnlineStatus { get; set; }
}
}
}

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Net.Mime;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
@ -15,12 +17,14 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays.Settings;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.Misskey;
using osuTK;
namespace osu.Game.Overlays.Login
{
public class LoginForm : FillFlowContainer
{
private TextBox hostname;
private TextBox username;
private TextBox password;
private ShakeContainer shakeSignIn;
@ -33,8 +37,8 @@ namespace osu.Game.Overlays.Login
private void performLogin()
{
if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
// api?.Login(username.Text, password.Text);
shakeSignIn.Shake();
api?.Login(username.Text, password.Text);
//shakeSignIn.Shake();
else
shakeSignIn.Shake();
}
@ -51,6 +55,12 @@ namespace osu.Game.Overlays.Login
Children = new Drawable[]
{
hostname = new OsuTextBox()
{
PlaceholderText = "instanceHostName",
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
},
username = new OsuTextBox
{
PlaceholderText = UsersStrings.LoginUsername.ToLower(),

View File

@ -112,7 +112,7 @@ namespace osu.Game.Screens.Menu
OnSolo = loadSoloSongSelect,
OnMultiplayer = () => this.Push(new Multiplayer()),
OnPlaylists = () => this.Push(new Playlists()),
OnMisskey = () => this.Push(new MisskeyLogin()),
OnMisskey = () => this.Push(new MisskeyScreenSelector()),
OnExit = confirmAndExit,
}
}

View File

@ -0,0 +1,115 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>, sim1222 <kokt@sim1222.com>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using DiffPlex;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.MisskeyAPI;
using osu.Game.Online.MisskeyAPI.Requests;
using osu.Game.Online.MisskeyAPI.Requests.Responses;
using osuTK;
using Meta = osu.Game.Online.MisskeyAPI.Requests.Meta;
namespace osu.Game.Screens.Misskey
{
public class MisskeyInstanceSelect : OsuScreen
{
private Container contentContainer;
private SeekLimitedSearchTextBox searchTextBox;
// private OsuButton submitButton;
// private string instanceName = String.Empty;
// [Resolved]
private IAPIProvider api { get; set; }
// private Meta getMeta;
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader(true)]
private void load()
{
InternalChild = contentContainer = new Container
{
Masking = true,
CornerRadius = 10,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.5f, 0.4f),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.6f,
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Masking = true,
AutoSizeEasing = Easing.OutQuint,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Height = 3,
Colour = colours.Yellow,
Alpha = 1,
},
searchTextBox = new SeekLimitedSearchTextBox()
{
RelativeSizeAxes = Axes.X,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Text = "misskey.io"
},
new OsuButton()
{
Text = "Submit",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 15f,
Width = 0.6f,
Action = insetanceFetch,
}
}
},
}
};
// insetanceFetch();
//
// searchTextBox.Current.ValueChanged += _ => insetanceFetch();
}
private void insetanceFetch()
{
var getMeta = new Meta(searchTextBox.Text);
getMeta.Success += response =>
{
Logger.Log($"{response}");
};
api.Queue(getMeta);
}
}
}

View File

@ -4,15 +4,22 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Overlays.Login;
using osuTK;
namespace osu.Game.Screens.Misskey
{
public class MisskeyLogin : OsuScreen
{
private LoginPanel panel;
private const float transition_time = 400;
public override bool HideOverlaysOnEnter => false;
private Container contentContainer;
@ -23,9 +30,6 @@ namespace osu.Game.Screens.Misskey
//private LoginPanel loginPanel;
[Resolved]
private OsuGameBase game { get; set; }
[Resolved]
private OsuColour colours { get; set; }
@ -44,13 +48,34 @@ namespace osu.Game.Screens.Misskey
{
new Box
{
Colour = new Color4(255, 255, 255, 255),
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f, 0.4f),
Anchor = Anchor.Centre,
Colour = Color4.Black,
Alpha = 0.6f,
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Masking = true,
AutoSizeDuration = transition_time,
AutoSizeEasing = Easing.OutQuint,
Children = new Drawable[]
{
panel = new LoginPanel
{
Padding = new MarginPadding(10),
RequestHide = Hide,
},
new Box
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Height = 3,
Colour = colours.Yellow,
Alpha = 1,
},
}
}
}
};

View File

@ -0,0 +1,79 @@
// 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.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Misskey
{
public class MisskeyScreenSelector : OsuScreen
{
private Container contentContainer;
[Resolved]
private OsuGameBase game { get; set; }
[Resolved]
private OsuColour colours { get; set; }
[BackgroundDependencyLoader(true)]
private void load()
{
InternalChild = contentContainer = new Container()
{
CornerRadius = 10,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.9f, 0.8f),
Children = new Drawable[]
{
new Box
{
Colour = colours.GreySeaFoamDark,
RelativeSizeAxes = Axes.Both,
},
new Container
{
RelativeSizeAxes = Axes.Both,
//Width = 0.35f,
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Children = new Drawable[]
{
new BasicButton()
{
Text = "MisskeyLogin",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 60f,
Width = 0.9f,
Action = () => this.Push(new MisskeyLogin())
},
new BasicButton()
{
Text = "MisskeyInstanceSelect",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 60f,
Width = 0.9f,
Action = () => this.Push(new MisskeyInstanceSelect())
}
}
}
}
};
}
}
}