diff --git a/Stardrop/Models/Nexus/Web/WebsocketResponse.cs b/Stardrop/Models/Nexus/Web/WebsocketResponse.cs new file mode 100644 index 00000000..4636ea42 --- /dev/null +++ b/Stardrop/Models/Nexus/Web/WebsocketResponse.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stardrop.Models.Nexus.Web +{ + internal class WebsocketResponse + { + public bool success { get; set; } + public WebsocketResponseData? data { get; set; } + + + } +} diff --git a/Stardrop/Models/Nexus/Web/WebsocketResponseData.cs b/Stardrop/Models/Nexus/Web/WebsocketResponseData.cs new file mode 100644 index 00000000..0d28d17d --- /dev/null +++ b/Stardrop/Models/Nexus/Web/WebsocketResponseData.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stardrop.Models.Nexus.Web +{ + internal class WebsocketResponseData + { + public string? connection_token { get; set; } + public string? api_key { get; set; } + } +} diff --git a/Stardrop/Utilities/NexusConnectionResult.cs b/Stardrop/Utilities/NexusConnectionResult.cs new file mode 100644 index 00000000..4d189db9 --- /dev/null +++ b/Stardrop/Utilities/NexusConnectionResult.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Stardrop.Utilities +{ + internal class NexusConnectionResult + { + public string? Error { get; set; } + public string? Message { get; set; } + + public string? ApiKey { get; set; } + } +} diff --git a/Stardrop/Utilities/NexusWebsocket.cs b/Stardrop/Utilities/NexusWebsocket.cs new file mode 100644 index 00000000..805b2bbc --- /dev/null +++ b/Stardrop/Utilities/NexusWebsocket.cs @@ -0,0 +1,143 @@ +using Stardrop.Models.Nexus.Web; +using Stardrop.ViewModels; +using System; +using System.Diagnostics; +using System.Net.WebSockets; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Timers; + +namespace Stardrop.Utilities +{ + internal class NexusWebsocket + { + private readonly Uri ssoWebsocketURI = new("wss://sso.nexusmods.com"); + private readonly string connectionUUID = Guid.NewGuid().ToString(); + private readonly string connectionSlug = "stardrop"; + + internal readonly string ssoUrl; + + private ClientWebSocket? _socket; + private System.Timers.Timer? _pingTimer; + private bool _hasResolved; + + public NexusWebsocket() + { + this.ssoUrl = $"https://www.nexusmods.com/sso?id={connectionUUID}&application={connectionSlug}"; + } + + public async Task ConnectAsync(CancellationToken cancellationToken = default) + { + var result = new NexusConnectionResult(); + _socket = new ClientWebSocket(); + + try + { + await _socket.ConnectAsync(ssoWebsocketURI, cancellationToken); + + var initialData = new + { + id = connectionUUID, + token = (string?)null, + protocol = 2 + }; + string json = JsonSerializer.Serialize(initialData); + var bytes = Encoding.UTF8.GetBytes(json); + await _socket.SendAsync( + new ArraySegment(bytes), WebSocketMessageType.Text, true, cancellationToken + ); + + // ping every 30 seconds as requested by docs + _pingTimer = new System.Timers.Timer(30_000); + _pingTimer.Elapsed += async (_, __) => + { + if (_socket?.State == WebSocketState.Open) + { + try + { + await _socket.SendAsync( + new ArraySegment(Array.Empty()), WebSocketMessageType.Text, true, CancellationToken.None + ); + } + catch + { + _pingTimer?.Stop(); + } + } + else + { + _pingTimer?.Stop(); + } + }; + _pingTimer.AutoReset = true; + _pingTimer.Start(); + + // Receive data + var buffer = new byte[4096]; + while (_socket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested) + { + var recv = await _socket.ReceiveAsync( + new ArraySegment(buffer), cancellationToken + ); + if (recv.MessageType == WebSocketMessageType.Close) break; + + var msg = Encoding.UTF8.GetString(buffer, 0, recv.Count); + Program.helper.Log($"[nexus websocket] received data {msg}", Helper.Status.Debug); + + var response = JsonSerializer.Deserialize(msg); + if (response != null && response.success && response.data != null) + { + // ignore connection_token + if (response.data.connection_token != null && response.data.api_key == null) + { + continue; + } + + result.Message = "successfully obtained api key"; + result.ApiKey = response.data.api_key; + _hasResolved = true; + await _socket.CloseAsync( + WebSocketCloseStatus.NormalClosure, "got key", CancellationToken.None + ); + break; + } + else + { + result.Error = "received invalid message"; + _hasResolved = true; + await _socket.CloseAsync( + WebSocketCloseStatus.NormalClosure, + "invalid", + CancellationToken.None + ); + break; + } + } + } + catch (Exception ex) + { + Program.helper.Log($"[nexus websocket] exception: {ex}", Helper.Status.Debug); + if (!_hasResolved) + { + result.Error = ex.Message; + _hasResolved = true; + } + } + finally + { + _pingTimer?.Stop(); + if (_socket?.State == WebSocketState.Open) + { + await _socket.CloseAsync( + WebSocketCloseStatus.NormalClosure, "shutdown", CancellationToken.None + ); + } + _socket?.Dispose(); + } + + return result; + } + } +} diff --git a/Stardrop/Views/NexusLogin.axaml b/Stardrop/Views/NexusLogin.axaml index 3e99760e..3f2b159b 100644 --- a/Stardrop/Views/NexusLogin.axaml +++ b/Stardrop/Views/NexusLogin.axaml @@ -81,11 +81,11 @@ - + diff --git a/Stardrop/Views/NexusLogin.axaml.cs b/Stardrop/Views/NexusLogin.axaml.cs index 4044e1aa..84a51eb5 100644 --- a/Stardrop/Views/NexusLogin.axaml.cs +++ b/Stardrop/Views/NexusLogin.axaml.cs @@ -2,15 +2,20 @@ using Avalonia.Controls; using Avalonia.Input; using Avalonia.Markup.Xaml; +using Stardrop.Utilities; using Stardrop.ViewModels; +using System; +using System.Net.WebSockets; namespace Stardrop.Views { public partial class NexusLogin : Window { + private NexusWebsocket? _nexusWebsocket; public NexusLogin() { InitializeComponent(); + _nexusWebsocket = new NexusWebsocket(); #if DEBUG this.AttachDevTools(); #endif @@ -18,20 +23,34 @@ public NexusLogin() public NexusLogin(MainWindowViewModel viewModel) : this() { + HandleNexusFlow(); // Handle buttons this.FindControl