Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Stardrop/Models/Nexus/Web/WebsocketResponse.cs
Original file line number Diff line number Diff line change
@@ -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; }


}
}
14 changes: 14 additions & 0 deletions Stardrop/Models/Nexus/Web/WebsocketResponseData.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
16 changes: 16 additions & 0 deletions Stardrop/Utilities/NexusConnectionResult.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}
143 changes: 143 additions & 0 deletions Stardrop/Utilities/NexusWebsocket.cs
Original file line number Diff line number Diff line change
@@ -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<NexusConnectionResult> 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<byte>(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<byte>(Array.Empty<byte>()), 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<byte>(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<WebsocketResponse>(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;
}
}
}
2 changes: 1 addition & 1 deletion Stardrop/Views/NexusLogin.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@
</StackPanel>

<StackPanel Grid.Row="2" HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Vertical" Margin="0 15 0 0">
<TextBlock Text="{i18n:Translate ui.nexus_login.labels.paste}" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="{DynamicResource ThemeForegroundBrush}"/>
<Border Margin="0 10 0 0" BorderBrush="{DynamicResource HighlightBrush}" BorderThickness="2">
<TextBox Name="apiBox" TextWrapping="Wrap" Height="50" Width="450" Background="{DynamicResource DataGridRowBackground}" Foreground="{DynamicResource ThemeForegroundHighBrush}" />
</Border>
</StackPanel>

<StackPanel Grid.Row="3" HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Vertical" Margin="0 15 0 0">
<TextBlock Text="{i18n:Translate ui.nexus_login.labels.note}" FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="{DynamicResource ThemeForegroundBrush}" Margin="0 0 0 0"/>
<TextBlock Text="{i18n:Translate ui.nexus_login.labels.share_warning_actual}" Width="400" TextWrapping="Wrap" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="{DynamicResource ThemeForegroundBrush}" Margin="0 15 0 0"/>
Expand Down
31 changes: 25 additions & 6 deletions Stardrop/Views/NexusLogin.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,55 @@
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
}

public NexusLogin(MainWindowViewModel viewModel) : this()
{
HandleNexusFlow();
// Handle buttons
this.FindControl<Button>("cancelButton").Click += delegate { this.Close(null); };
this.FindControl<Button>("exitButton").Click += delegate { this.Close(null); };
this.FindControl<Button>("goToNexusButton").Click += delegate { viewModel.OpenBrowser("https://www.nexusmods.com/users/myaccount?tab=api"); };
this.FindControl<Button>("goToNexusButton").Click += delegate { viewModel.OpenBrowser(_nexusWebsocket.ssoUrl); };

var applyButton = this.FindControl<Button>("applyButton");
applyButton.Click += ApplyButton_Click;
applyButton.IsEnabled = false;
}

// Give focus to textbox
var apiKeyBox = this.FindControl<TextBox>("apiBox");
apiKeyBox.AttachedToVisualTree += (s, e) => apiKeyBox.Focus();
apiKeyBox.KeyDown += KeyBox_KeyDown;
apiKeyBox.KeyUp += KeyBox_KeyUp;
private async void HandleNexusFlow()
{
var result = await _nexusWebsocket.ConnectAsync();

if (result.Error is not null)
{
Program.helper.Log($"Error getting API key: {result.Error}", Helper.Status.Warning);
}
else
{
Program.helper.Log($"Got API key: {result.ApiKey}", Helper.Status.Info);
var apiKeyBox = this.FindControl<TextBox>("apiBox");
apiKeyBox.Text = result.ApiKey ?? string.Empty;

var applyButton = this.FindControl<Button>("applyButton");
applyButton.IsEnabled = true;
}
}

private void ApplyChanges()
Expand Down