From e10a5476837ac2dacba2cab7019de21af6646fc9 Mon Sep 17 00:00:00 2001 From: AnotherPillow <85362273+AnotherPillow@users.noreply.github.com> Date: Thu, 17 Jul 2025 22:01:25 +0100 Subject: [PATCH 1/3] Move nexus API key obtaining to the sso system instead of manual copy --- .../Models/Nexus/Web/WebsocketResponse.cs | 16 ++ .../Models/Nexus/Web/WebsocketResponseData.cs | 14 ++ Stardrop/Utilities/NexusConnectionResult.cs | 16 ++ Stardrop/Utilities/NexusWebsocket.cs | 158 ++++++++++++++++++ Stardrop/Views/NexusLogin.axaml | 2 +- Stardrop/Views/NexusLogin.axaml.cs | 31 +++- 6 files changed, 230 insertions(+), 7 deletions(-) create mode 100644 Stardrop/Models/Nexus/Web/WebsocketResponse.cs create mode 100644 Stardrop/Models/Nexus/Web/WebsocketResponseData.cs create mode 100644 Stardrop/Utilities/NexusConnectionResult.cs create mode 100644 Stardrop/Utilities/NexusWebsocket.cs 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..cbdb8de8 --- /dev/null +++ b/Stardrop/Utilities/NexusWebsocket.cs @@ -0,0 +1,158 @@ +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 + { + //#if DEBUG + // private readonly Uri ssoWebsocketURI = new("ws://127.0.0.1"); + //#else + + //#endif + 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); + Console.WriteLine($"[nexus websocket] received data {msg}"); + + 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