diff --git a/CHANGELOG.md b/CHANGELOG.md index d886272b1..6a82dc73d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ### Changelog +#### Version - 3.7.5.0 - TBD +* Fix for Wabbajack trying to work with an expired OAuth token. +* Added pre-compile login check. + #### Version - 3.7.4.1 - 11/21/2024 * Add support for the upcoming Nexus game ID change (thanks to [@JonathanFeenstra](https://github.com/JonathanFeenstra)) * Wabbajack will now put modfile Titles, Description and Version into the `.meta` file. diff --git a/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs index 1a1aa5e47..27ff83543 100644 --- a/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs +++ b/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs @@ -1,6 +1,6 @@ using System; -using System.Reactive.Disposables; using System.Reactive.Linq; +using System.Threading.Tasks; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; @@ -10,7 +10,6 @@ using ReactiveUI.Fody.Helpers; using Wabbajack.Downloaders; using Wabbajack.DTOs.Logins; -using Wabbajack.Messages; using Wabbajack.Networking.Http.Interfaces; using Wabbajack.UserIntervention; @@ -40,13 +39,12 @@ public NexusLoginManager(ILogger logger, ITokenProvider await RefreshTokenState()); ClearLogin = ReactiveCommand.CreateFromTask(async () => { _logger.LogInformation("Deleting Login information for {SiteName}", SiteName); - await _token.Delete(); - RefreshTokenState(); + await ClearLoginToken(); }, this.WhenAnyValue(v => v.HaveLogin)); Icon = BitmapFrame.Create( @@ -60,17 +58,25 @@ public NexusLoginManager(ILogger logger, ITokenProvider v.HaveLogin).Select(v => !v)); } + private async Task ClearLoginToken() + { + await _token.Delete(); + await RefreshTokenState(); + } + private void StartLogin() { var view = new BrowserWindow(_serviceProvider); - view.Closed += (sender, args) => { RefreshTokenState(); }; + view.Closed += async (sender, args) => { await RefreshTokenState(); }; var provider = _serviceProvider.GetRequiredService(); view.DataContext = provider; view.Show(); } - private void RefreshTokenState() + private async Task RefreshTokenState() { - HaveLogin = _token.HaveToken(); + var token = await _token.Get(); + + HaveLogin = _token.HaveToken() && !(token?.OAuth?.IsExpired ?? true); } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs b/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs index 7eb0ecb95..bb38aa557 100644 --- a/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs +++ b/Wabbajack.App.Wpf/View Models/Compilers/CompilerVM.cs @@ -19,9 +19,11 @@ using Wabbajack.Common; using Wabbajack.Compiler; using Wabbajack.DTOs; +using Wabbajack.DTOs.DownloadStates; using Wabbajack.DTOs.JsonConverters; using Wabbajack.Extensions; using Wabbajack.Installer; +using Wabbajack.LoginManagers; using Wabbajack.Models; using Wabbajack.Networking.WabbajackClientApi; using Wabbajack.Paths; @@ -31,8 +33,6 @@ namespace Wabbajack { - - public enum CompilerState { Configuration, @@ -40,6 +40,7 @@ public enum CompilerState Completed, Errored } + public class CompilerVM : BackNavigatingVM, ICpuStatusVM { private const string LastSavedCompilerSettings = "last-saved-compiler-settings"; @@ -49,24 +50,23 @@ public class CompilerVM : BackNavigatingVM, ICpuStatusVM private readonly ILogger _logger; private readonly ResourceMonitor _resourceMonitor; private readonly CompilerSettingsInferencer _inferencer; + private readonly IEnumerable _logins; private readonly Client _wjClient; - + [Reactive] public string StatusText { get; set; } [Reactive] public Percent StatusProgress { get; set; } - [Reactive] - public CompilerState State { get; set; } - - [Reactive] - public MO2CompilerVM SubCompilerVM { get; set; } - + [Reactive] public CompilerState State { get; set; } + + [Reactive] public MO2CompilerVM SubCompilerVM { get; set; } + // Paths public FilePickerVM ModlistLocation { get; } public FilePickerVM DownloadLocation { get; } public FilePickerVM OutputLocation { get; } - + // Modlist Settings - + [Reactive] public string ModListName { get; set; } [Reactive] public string Version { get; set; } [Reactive] public string Author { get; set; } @@ -87,26 +87,25 @@ public class CompilerVM : BackNavigatingVM, ICpuStatusVM [Reactive] public RelativePath[] NoMatchInclude { get; set; } = Array.Empty(); [Reactive] public RelativePath[] Include { get; set; } = Array.Empty(); [Reactive] public RelativePath[] Ignore { get; set; } = Array.Empty(); - + [Reactive] public string[] OtherProfiles { get; set; } = Array.Empty(); - + [Reactive] public AbsolutePath Source { get; set; } - + public AbsolutePath SettingsOutputLocation => Source.Combine(ModListName).WithExtension(Ext.CompilerSettings); - - + + public ReactiveCommand ExecuteCommand { get; } public ReactiveCommand ReInferSettingsCommand { get; set; } public LogStream LoggerProvider { get; } public ReadOnlyObservableCollection StatusList => _resourceMonitor.Tasks; - - [Reactive] - public ErrorResponse ErrorState { get; private set; } - + + [Reactive] public ErrorResponse ErrorState { get; private set; } + public CompilerVM(ILogger logger, DTOSerializer dtos, SettingsManager settingsManager, - IServiceProvider serviceProvider, LogStream loggerProvider, ResourceMonitor resourceMonitor, - CompilerSettingsInferencer inferencer, Client wjClient) : base(logger) + IServiceProvider serviceProvider, LogStream loggerProvider, ResourceMonitor resourceMonitor, + CompilerSettingsInferencer inferencer, Client wjClient, IEnumerable logins) : base(logger) { _logger = logger; _dtos = dtos; @@ -116,6 +115,7 @@ public CompilerVM(ILogger logger, DTOSerializer dtos, SettingsManage _resourceMonitor = resourceMonitor; _inferencer = inferencer; _wjClient = wjClient; + _logins = logins; StatusText = "Compiler Settings"; StatusProgress = Percent.Zero; @@ -126,7 +126,7 @@ public CompilerVM(ILogger logger, DTOSerializer dtos, SettingsManage await SaveSettingsFile(); NavigateToGlobal.Send(NavigateToGlobal.ScreenType.ModeSelectionView); }); - + SubCompilerVM = new MO2CompilerVM(this); ExecuteCommand = ReactiveCommand.CreateFromTask(async () => await StartCompilation()); @@ -152,7 +152,7 @@ public CompilerVM(ILogger logger, DTOSerializer dtos, SettingsManage PathType = FilePickerVM.PathTypeOptions.Folder, PromptTitle = "Location where the downloads for this list are stored" }; - + OutputLocation = new FilePickerVM { ExistCheckOption = FilePickerVM.CheckOptions.Off, @@ -160,13 +160,13 @@ public CompilerVM(ILogger logger, DTOSerializer dtos, SettingsManage PromptTitle = "Location where the compiled modlist will be stored" }; - ModlistLocation.Filters.AddRange(new [] + ModlistLocation.Filters.AddRange(new[] { new CommonFileDialogFilter("MO2 Modlist", "*" + Ext.Txt), new CommonFileDialogFilter("Compiler Settings File", "*" + Ext.CompilerSettings) }); - + this.WhenActivated(disposables => { State = CompilerState.Configuration; @@ -188,13 +188,12 @@ public CompilerVM(ILogger logger, DTOSerializer dtos, SettingsManage .Select(_ => Validate()) .BindToStrict(this, vm => vm.ErrorState) .DisposeWith(disposables); - + LoadLastSavedSettings().FireAndForget(); }); } - private async Task ReInferSettings() { var newSettings = await _inferencer.InferModListFromLocation( @@ -205,7 +204,7 @@ private async Task ReInferSettings() _logger.LogError("Cannot infer settings"); return; } - + Include = newSettings.Include; Ignore = newSettings.Ignore; AlwaysEnabled = newSettings.AlwaysEnabled; @@ -252,7 +251,7 @@ private async Task InferModListFromLocation(AbsolutePath path) Website = settings.ModListWebsite?.ToString() ?? ""; Readme = settings.ModListReadme?.ToString() ?? ""; IsNSFW = settings.ModlistIsNSFW; - + Source = settings.Source; DownloadLocation.TargetPath = settings.Downloads; if (settings.OutputFile.Extension == Ext.Wabbajack) @@ -280,11 +279,28 @@ private async Task StartCompilation() { try { - await SaveSettingsFile(); var token = CancellationToken.None; State = CompilerState.Compiling; + var downloadStateNexus = new Nexus(); + + var manager = _logins + .FirstOrDefault(l => l.LoginFor() == downloadStateNexus.GetType()); + if (manager == null) + { + _logger.LogError("Cannot compile, could not prepare {Name} for verifying", + downloadStateNexus.GetType().Name); + throw new Exception($"No way to prepare {downloadStateNexus}"); + } + + RxApp.MainThreadScheduler.Schedule(manager, (_, _) => + { + manager.TriggerLogin.Execute(null); + return Disposable.Empty; + }); + + var mo2Settings = GetSettings(); mo2Settings.UseGamePaths = true; if (mo2Settings.OutputFile.DirectoryExists()) @@ -326,20 +342,21 @@ private async Task StartCompilation() { _logger.LogInformation("Publishing List"); var downloadMetadata = _dtos.Deserialize( - await mo2Settings.OutputFile.WithExtension(Ext.Meta).WithExtension(Ext.Json).ReadAllTextAsync())!; - await _wjClient.PublishModlist(MachineUrl, System.Version.Parse(Version), mo2Settings.OutputFile, downloadMetadata); + await mo2Settings.OutputFile.WithExtension(Ext.Meta).WithExtension(Ext.Json) + .ReadAllTextAsync())!; + await _wjClient.PublishModlist(MachineUrl, System.Version.Parse(Version), + mo2Settings.OutputFile, downloadMetadata); } + _logger.LogInformation("Compiler Finished"); - + RxApp.MainThreadScheduler.Schedule(_logger, (_, _) => { StatusText = "Compilation Completed"; StatusProgress = Percent.Zero; State = CompilerState.Completed; - return Disposable.Empty; + return Disposable.Empty; }); - - } catch (Exception ex) { @@ -388,17 +405,17 @@ private async Task SaveSettingsFile() private async Task LoadLastSavedSettings() { var lastPath = await _settingsManager.Load(LastSavedCompilerSettings); - if (lastPath == default || !lastPath.FileExists() || lastPath.FileName.Extension != Ext.CompilerSettings) return; + if (lastPath == default || !lastPath.FileExists() || + lastPath.FileName.Extension != Ext.CompilerSettings) return; ModlistLocation.TargetPath = lastPath; } - + private CompilerSettings GetSettings() { - System.Version.TryParse(Version, out var pversion); Uri.TryCreate(Website, UriKind.Absolute, out var websiteUri); - + return new CompilerSettings { ModListName = ModListName, @@ -436,7 +453,7 @@ public void RemoveProfile(string profile) { OtherProfiles = OtherProfiles.Where(p => p != profile).ToArray(); } - + public void AddAlwaysEnabled(RelativePath path) { AlwaysEnabled = (AlwaysEnabled ?? Array.Empty()).Append(path).Distinct().ToArray(); @@ -446,7 +463,7 @@ public void RemoveAlwaysEnabled(RelativePath path) { AlwaysEnabled = AlwaysEnabled.Where(p => p != path).ToArray(); } - + public void AddNoMatchInclude(RelativePath path) { NoMatchInclude = (NoMatchInclude ?? Array.Empty()).Append(path).Distinct().ToArray(); @@ -456,7 +473,7 @@ public void RemoveNoMatchInclude(RelativePath path) { NoMatchInclude = NoMatchInclude.Where(p => p != path).ToArray(); } - + public void AddInclude(RelativePath path) { Include = (Include ?? Array.Empty()).Append(path).Distinct().ToArray(); @@ -467,7 +484,7 @@ public void RemoveInclude(RelativePath path) Include = Include.Where(p => p != path).ToArray(); } - + public void AddIgnore(RelativePath path) { Ignore = (Ignore ?? Array.Empty()).Append(path).Distinct().ToArray(); @@ -480,4 +497,4 @@ public void RemoveIgnore(RelativePath path) #endregion } -} +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs b/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs index 13c14e6f3..87a8a9374 100644 --- a/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs +++ b/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs @@ -27,7 +27,8 @@ public partial class MainWindow : MetroWindow private ObservableCollection TabVMs = new ObservableCollectionExtended(); - public MainWindow(ILogger logger, SystemParametersConstructor systemParams, LauncherUpdater updater, MainWindowVM vm) + public MainWindow(ILogger logger, SystemParametersConstructor systemParams, LauncherUpdater updater + , MainWindowVM vm) { InitializeComponent(); _mwvm = vm; @@ -122,7 +123,7 @@ public MainWindow(ILogger logger, SystemParametersConstructor system .ObserveOn(RxApp.MainThreadScheduler) .Select(v => v == InstallState.Installing ? Visibility.Collapsed : Visibility.Visible) .BindTo(this, view => view.SettingsButton.Visibility); - + } catch (Exception ex) { diff --git a/Wabbajack.Networking.NexusApi/NexusApi.cs b/Wabbajack.Networking.NexusApi/NexusApi.cs index 77d56277f..186d724aa 100644 --- a/Wabbajack.Networking.NexusApi/NexusApi.cs +++ b/Wabbajack.Networking.NexusApi/NexusApi.cs @@ -237,7 +237,7 @@ private async ValueTask AddAuthHeaders(HttpRequestMessage msg) var info = await AuthInfo.Get(); if (info!.OAuth != null) { - if (info!.OAuth.IsExpired) + if (info.OAuth.IsExpired) info = await RefreshToken(info, CancellationToken.None); return (false, info.OAuth!.AccessToken!); } @@ -270,6 +270,10 @@ private async Task RefreshToken(NexusOAuthState state, Cancella var content = new FormUrlEncodedContent(request); var response = await _client.PostAsync($"https://users.nexusmods.com/oauth/token", content, cancel); + + if (!response.IsSuccessStatusCode) + _logger.LogError("Nexus OAuth Token refresh failed: {ResponseReasonPhrase}", response.ReasonPhrase); + var responseString = await response.Content.ReadAsStringAsync(cancel); var newJwt = JsonSerializer.Deserialize(responseString); if (newJwt != null)