diff --git a/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs index bb7e242d..82a96445 100644 --- a/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs +++ b/Wabbajack.App.Wpf/LoginManagers/NexusLoginManager.cs @@ -1,7 +1,10 @@ +using System; +using System.Reactive.Disposables; using System.Reactive.Linq; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ReactiveUI; using ReactiveUI.Fody.Helpers; @@ -9,6 +12,7 @@ using Wabbajack.DTOs.Interventions; using Wabbajack.DTOs.Logins; using Wabbajack.Messages; using Wabbajack.Networking.Http.Interfaces; +using Wabbajack.UserIntervention; namespace Wabbajack.LoginManagers; @@ -17,6 +21,7 @@ public class NexusLoginManager : ViewModel, INeedsLogin private readonly ILogger _logger; private readonly ITokenProvider _token; private readonly IUserInterventionHandler _handler; + private readonly IServiceProvider _serviceProvider; public string SiteName { get; } = "Nexus Mods"; public ICommand TriggerLogin { get; set; } @@ -27,10 +32,11 @@ public class NexusLoginManager : ViewModel, INeedsLogin [Reactive] public bool HaveLogin { get; set; } - public NexusLoginManager(ILogger logger, ITokenProvider token) + public NexusLoginManager(ILogger logger, ITokenProvider token, IServiceProvider serviceProvider) { _logger = logger; _token = token; + _serviceProvider = serviceProvider; RefreshTokenState(); ClearLogin = ReactiveCommand.CreateFromTask(async () => @@ -45,10 +51,13 @@ public class NexusLoginManager : ViewModel, INeedsLogin TriggerLogin = ReactiveCommand.CreateFromTask(async () => { - _logger.LogInformation("Logging into {SiteName}", SiteName); - await NexusLogin.Send(); - RefreshTokenState(); + _logger.LogInformation("Logging into {SiteName}", SiteName); + MessageBus.Current.SendMessage(new OpenBrowserTab(_serviceProvider.GetRequiredService())); }, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v)); + + MessageBus.Current.Listen() + .Subscribe(x => RefreshTokenState()) + .DisposeWith(CompositeDisposable); } private void RefreshTokenState() diff --git a/Wabbajack.App.Wpf/Messages/CloseBrowserTab.cs b/Wabbajack.App.Wpf/Messages/CloseBrowserTab.cs new file mode 100644 index 00000000..b44f14b0 --- /dev/null +++ b/Wabbajack.App.Wpf/Messages/CloseBrowserTab.cs @@ -0,0 +1,11 @@ +namespace Wabbajack.Messages; + +public class CloseBrowserTab +{ + public BrowserTabViewModel ViewModel { get; init; } + + public CloseBrowserTab(BrowserTabViewModel viewModel) + { + ViewModel = viewModel; + } +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/UserIntervention/LoversLabLoginHandler.cs b/Wabbajack.App.Wpf/UserIntervention/LoversLabLoginHandler.cs index 6b000c08..53f0810a 100644 --- a/Wabbajack.App.Wpf/UserIntervention/LoversLabLoginHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/LoversLabLoginHandler.cs @@ -1,16 +1,17 @@ using System.Net.Http; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Wabbajack.DTOs.Logins; using Wabbajack.Models; using Wabbajack.Networking.Http.Interfaces; +using Wabbajack.Services.OSIntegrated; namespace Wabbajack.UserIntervention; public class LoversLabLoginHandler : OAuth2LoginHandler { - public LoversLabLoginHandler(ILogger logger, HttpClient client, ITokenProvider tokenProvider, - WebBrowserVM browser, CefService service) - : base(logger, client, tokenProvider, browser, service) + public LoversLabLoginHandler(ILogger logger, HttpClient httpClient, EncryptedJsonTokenProvider tokenProvider) + : base(logger, httpClient, tokenProvider) { } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/UserIntervention/NexusLoginHandler.cs b/Wabbajack.App.Wpf/UserIntervention/NexusLoginHandler.cs index 2fba1999..f610dbb0 100644 --- a/Wabbajack.App.Wpf/UserIntervention/NexusLoginHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/NexusLoginHandler.cs @@ -1,104 +1,98 @@ using System; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using Fizzler.Systems.HtmlAgilityPack; using Microsoft.Extensions.Logging; using Wabbajack.DTOs.Logins; using Wabbajack.Messages; using Wabbajack.Models; using Wabbajack.Networking.Http.Interfaces; +using Wabbajack.Services.OSIntegrated; namespace Wabbajack.UserIntervention; -public class NexusLoginHandler : WebUserInterventionBase +public class NexusLoginHandler : BrowserTabViewModel { - private readonly ITokenProvider _provider; + private readonly EncryptedJsonTokenProvider _tokenProvider; - public NexusLoginHandler(ILogger logger, WebBrowserVM browserVM, ITokenProvider provider, CefService service) - : base(logger, browserVM, service) + public NexusLoginHandler(EncryptedJsonTokenProvider tokenProvider) { - _provider = provider; + HeaderText = "Nexus Login"; + _tokenProvider = tokenProvider; } - public override async Task Begin() + + protected override async Task Run(CancellationToken token) { - try - { - Messages.NavigateTo.Send(Browser); - UpdateStatus("Please log into the Nexus"); - await Driver.WaitForInitialized(); - - await NavigateTo(new Uri("https://users.nexusmods.com/auth/continue?client_id=nexus&redirect_uri=https://www.nexusmods.com/oauth/callback&response_type=code&referrer=//www.nexusmods.com")); + token.ThrowIfCancellationRequested(); - Cookie[] cookies = {}; - while (true) + Instructions = "Please log into the Nexus"; + + await NavigateTo(new Uri( + "https://users.nexusmods.com/auth/continue?client_id=nexus&redirect_uri=https://www.nexusmods.com/oauth/callback&response_type=code&referrer=//www.nexusmods.com")); + + + Cookie[] cookies = { }; + while (true) + { + cookies = await GetCookies("nexusmods.com", token); + if (cookies.Any(c => c.Name == "member_id")) + break; + + token.ThrowIfCancellationRequested(); + await Task.Delay(500, token); + } + + Instructions = "Getting API Key..."; + + await NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api")); + + var key = ""; + + while (true) + { + try { - cookies = await Driver.GetCookies("nexusmods.com"); - if (cookies.Any(c => c.Name == "member_id")) - break; - Message.Token.ThrowIfCancellationRequested(); - await Task.Delay(500, Message.Token); + key = (await GetDom(token)) + .DocumentNode + .QuerySelectorAll("input[value=wabbajack]") + .SelectMany(p => p.ParentNode.ParentNode.QuerySelectorAll("textarea.application-key")) + .Select(node => node.InnerHtml) + .FirstOrDefault() ?? ""; + } + catch (Exception) + { + // ignored } + if (!string.IsNullOrEmpty(key)) + break; - await NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api")); - - UpdateStatus("Looking for API Key"); - - var key = ""; - - while (true) + try { - try - { - key = await Driver.EvaluateJavaScript( - "document.querySelector(\"input[value=wabbajack]\").parentElement.parentElement.querySelector(\"textarea.application-key\").innerHTML"); - } - catch (Exception) - { - // ignored - } - - if (!string.IsNullOrEmpty(key)) - { - break; - } - - try - { - await Driver.EvaluateJavaScript( - "var found = document.querySelector(\"input[value=wabbajack]\").parentElement.parentElement.querySelector(\"form button[type=submit]\");" + - "found.onclick= function() {return true;};" + - "found.class = \" \"; " + - "found.click();" + - "found.remove(); found = undefined;" - ); - UpdateStatus("Generating API Key, Please Wait..."); - - - } - catch (Exception) - { - // ignored - } - - Message.Token.ThrowIfCancellationRequested(); - await Task.Delay(500, Message.Token); + await EvaluateJavaScript( + "var found = document.querySelector(\"input[value=wabbajack]\").parentElement.parentElement.querySelector(\"form button[type=submit]\");" + + "found.onclick= function() {return true;};" + + "found.class = \" \"; " + + "found.click();" + + "found.remove(); found = undefined;" + ); + Instructions = "Generating API Key, Please Wait..."; + } + catch (Exception) + { + // ignored } - - await _provider.SetToken(new NexusApiState() - { - ApiKey = key, - Cookies = cookies - }); - - ((NexusLogin)Message).CompletionSource.SetResult(); - Messages.NavigateTo.Send(PrevPane); + token.ThrowIfCancellationRequested(); + await Task.Delay(500, token); } - catch (Exception ex) + + Instructions = "Success, saving information..."; + await _tokenProvider.SetToken(new NexusApiState { - Logger.LogError(ex, "While logging into Nexus Mods"); - Message.SetException(ex); - Messages.NavigateTo.Send(PrevPane); - } + Cookies = cookies, + ApiKey = key + }); } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs b/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs index 97e66c4f..afaec236 100644 --- a/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs @@ -22,45 +22,57 @@ public abstract class OAuth2LoginHandler : WebUserInt where TLoginType : OAuth2LoginState, new() { private readonly HttpClient _httpClient; - private readonly ITokenProvider _tokenProvider; + private readonly EncryptedJsonTokenProvider _tokenProvider; + private readonly ILogger _logger; public OAuth2LoginHandler(ILogger logger, HttpClient httpClient, - ITokenProvider tokenProvider, WebBrowserVM browserVM, CefService service) : base(logger, browserVM, service) + EncryptedJsonTokenProvider tokenProvider) { + var tlogin = new TLoginType(); + HeaderText = $"{tlogin.SiteName} Login"; + _logger = logger; _httpClient = httpClient; _tokenProvider = tokenProvider; } - public override async Task Begin() + protected override async Task Run(CancellationToken token) { - Messages.NavigateTo.Send(Browser); var tlogin = new TLoginType(); - await Driver.WaitForInitialized(); - - using var handler = Driver.WithSchemeHandler(uri => uri.Scheme == "wabbajack"); - - UpdateStatus($"Please log in and allow Wabbajack to access your {tlogin.SiteName} account"); + var tcs = new TaskCompletionSource(); + await WaitForReady(); + Browser!.Browser.CoreWebView2.Settings.UserAgent = "Wabbajack"; + Browser!.Browser.NavigationStarting += (sender, args) => + { + var uri = new Uri(args.Uri); + if (uri.Scheme == "wabbajack") + { + tcs.TrySetResult(uri); + } + }; + + Instructions = $"Please log in and allow Wabbajack to access your {tlogin.SiteName} account"; var scopes = string.Join(" ", tlogin.Scopes); var state = Guid.NewGuid().ToString(); - await NavigateTo(new Uri(tlogin.AuthorizationEndpoint + $"?response_type=code&client_id={tlogin.ClientID}&state={state}&scope={scopes}")); + await NavigateTo(new Uri(tlogin.AuthorizationEndpoint + + $"?response_type=code&client_id={tlogin.ClientID}&state={state}&scope={scopes}")); - var uri = await handler.Task.WaitAsync(Message.Token); + var uri = await tcs.Task.WaitAsync(token); - var cookies = await Driver.GetCookies(tlogin.AuthorizationEndpoint.Host); + var cookies = await GetCookies(tlogin.AuthorizationEndpoint.Host, token); var parsed = HttpUtility.ParseQueryString(uri.Query); if (parsed.Get("state") != state) { - Logger.LogCritical("Bad OAuth state, this shouldn't happen"); + _logger.LogCritical("Bad OAuth state, this shouldn't happen"); throw new Exception("Bad OAuth State"); } if (parsed.Get("code") == null) { - Logger.LogCritical("Bad code result from OAuth"); + _logger.LogCritical("Bad code result from OAuth"); throw new Exception("Bad code result from OAuth"); } @@ -81,8 +93,8 @@ public abstract class OAuth2LoginHandler : WebUserInt msg.Headers.Add("Cookie", string.Join(";", cookies.Select(c => $"{c.Name}={c.Value}"))); msg.Content = new FormUrlEncodedContent(formData.ToList()); - using var response = await _httpClient.SendAsync(msg, Message.Token); - var data = await response.Content.ReadFromJsonAsync(cancellationToken: Message.Token); + using var response = await _httpClient.SendAsync(msg, token); + var data = await response.Content.ReadFromJsonAsync(cancellationToken: token); await _tokenProvider.SetToken(new TLoginType { @@ -90,6 +102,5 @@ public abstract class OAuth2LoginHandler : WebUserInt ResultState = data! }); - Messages.NavigateTo.Send(PrevPane); } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/UserIntervention/VectorPlexusLoginHandler.cs b/Wabbajack.App.Wpf/UserIntervention/VectorPlexusLoginHandler.cs index c1edadc6..a06f3ca9 100644 --- a/Wabbajack.App.Wpf/UserIntervention/VectorPlexusLoginHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/VectorPlexusLoginHandler.cs @@ -1,15 +1,16 @@ using System.Net.Http; using Microsoft.Extensions.Logging; +using Wabbajack.DTOs.Logins; using Wabbajack.Models; using Wabbajack.Networking.Http.Interfaces; +using Wabbajack.Services.OSIntegrated; namespace Wabbajack.UserIntervention; public class VectorPlexusLoginHandler : OAuth2LoginHandler { - public VectorPlexusLoginHandler(ILogger logger, HttpClient client, ITokenProvider tokenProvider, - WebBrowserVM browser, CefService service) - : base(logger, client, tokenProvider, browser, service) + public VectorPlexusLoginHandler(ILogger logger, HttpClient httpClient, EncryptedJsonTokenProvider tokenProvider) + : base(logger, httpClient, tokenProvider) { } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/UserIntervention/WebUserInterventionBase.cs b/Wabbajack.App.Wpf/UserIntervention/WebUserInterventionBase.cs index 8918fa08..bf60aa82 100644 --- a/Wabbajack.App.Wpf/UserIntervention/WebUserInterventionBase.cs +++ b/Wabbajack.App.Wpf/UserIntervention/WebUserInterventionBase.cs @@ -1,46 +1,99 @@ using System; +using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using System.Web; +using HtmlAgilityPack; using Microsoft.Extensions.Logging; +using Microsoft.Web.WebView2.Core; +using Microsoft.Web.WebView2.Wpf; +using ReactiveUI.Fody.Helpers; using Wabbajack.DTOs.Interventions; +using Wabbajack.DTOs.Logins; using Wabbajack.Interventions; using Wabbajack.Models; +using Wabbajack.Views; using Wabbajack.WebAutomation; namespace Wabbajack.UserIntervention; -public abstract class WebUserInterventionBase +public abstract class WebUserInterventionBase : ViewModel where T : IUserIntervention { - protected readonly WebBrowserVM Browser; - protected readonly ILogger Logger; - protected T Message; - protected ViewModel PrevPane; - protected IWebDriver Driver; + [Reactive] + public string HeaderText { get; set; } + + [Reactive] + public string Instructions { get; set; } - protected WebUserInterventionBase(ILogger logger, WebBrowserVM browser, CefService service) + public BrowserView? Browser { get; set; } + + private WebView2 _browser => Browser!.Browser; + + public async Task RunWrapper(CancellationToken token) { - Logger = logger; - Browser = browser; - //Driver = new CefSharpWrapper(logger, browser.Browser, service); + await Run(token); } - public void Configure(ViewModel prevPane, T message) + protected abstract Task Run(CancellationToken token); + + protected async Task WaitForReady() { - Message = message; - PrevPane = prevPane; + while (Browser?.Browser.CoreWebView2 == null) + { + await Task.Delay(250); + } } - protected void UpdateStatus(string status) + public async Task NavigateTo(Uri uri) { - Browser.Instructions = status; + var tcs = new TaskCompletionSource(); + + void Completed(object? o, CoreWebView2NavigationCompletedEventArgs a) + { + if (a.IsSuccess) + { + tcs.TrySetResult(); + } + else + { + tcs.TrySetException(new Exception($"Navigation error to {uri}")); + } + } + + _browser.NavigationCompleted += Completed; + _browser.Source = uri; + await tcs.Task; + _browser.NavigationCompleted -= Completed; + } + + public async Task GetCookies(string domainEnding, CancellationToken token) + { + var cookies = (await _browser.CoreWebView2.CookieManager.GetCookiesAsync("")) + .Where(c => c.Domain.EndsWith(domainEnding)); + return cookies.Select(c => new Cookie + { + Domain = c.Domain, + Name = c.Name, + Path = c.Path, + Value = c.Value + }).ToArray(); } - protected async Task NavigateTo(Uri uri) + public async Task EvaluateJavaScript(string js) { - await Driver.NavigateTo(uri, Message.Token); + return await _browser.ExecuteScriptAsync(js); } - public abstract Task Begin(); + public async Task GetDom(CancellationToken token) + { + var v = HttpUtility.UrlDecode("\u003D"); + var source = await EvaluateJavaScript("document.body.outerHTML"); + var decoded = JsonSerializer.Deserialize(source); + var doc = new HtmlDocument(); + doc.LoadHtml(decoded); + return doc; + } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/View Models/BrowserTabViewModel.cs b/Wabbajack.App.Wpf/View Models/BrowserTabViewModel.cs index aa8b964c..dbe7214e 100644 --- a/Wabbajack.App.Wpf/View Models/BrowserTabViewModel.cs +++ b/Wabbajack.App.Wpf/View Models/BrowserTabViewModel.cs @@ -7,8 +7,10 @@ using System.Web; using HtmlAgilityPack; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Wpf; +using ReactiveUI; using ReactiveUI.Fody.Helpers; using Wabbajack.DTOs.Logins; +using Wabbajack.Messages; using Wabbajack.Views; namespace Wabbajack; @@ -26,6 +28,7 @@ public abstract class BrowserTabViewModel : ViewModel public async Task RunWrapper(CancellationToken token) { await Run(token); + MessageBus.Current.SendMessage(new CloseBrowserTab(this)); } protected abstract Task Run(CancellationToken token); diff --git a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs index 0877b79b..7212816c 100644 --- a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs +++ b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; @@ -164,22 +165,20 @@ namespace Wabbajack private void HandleLogin(NexusLogin nexusLogin) { var handler = _serviceProvider.GetRequiredService(); - handler.Configure(ActivePane, nexusLogin); - handler.Begin().FireAndForget(); + handler.RunWrapper(CancellationToken.None).FireAndForget(); } private void HandleLogin(LoversLabLogin loversLabLogin) { var handler = _serviceProvider.GetRequiredService(); - handler.Configure(ActivePane, loversLabLogin); - handler.Begin().FireAndForget(); + handler.RunWrapper(CancellationToken.None).FireAndForget(); } private void HandleLogin(VectorPlexusLogin vectorPlexusLogin) { var handler = _serviceProvider.GetRequiredService(); - handler.Configure(ActivePane, vectorPlexusLogin); - handler.Begin().FireAndForget(); + handler.RunWrapper(CancellationToken.None).FireAndForget(); + } private void HandleNavigateBack(NavigateBack navigateBack) diff --git a/Wabbajack.App.Wpf/Views/BrowserTabView.xaml.cs b/Wabbajack.App.Wpf/Views/BrowserTabView.xaml.cs index 64a59d6e..c81a27de 100644 --- a/Wabbajack.App.Wpf/Views/BrowserTabView.xaml.cs +++ b/Wabbajack.App.Wpf/Views/BrowserTabView.xaml.cs @@ -44,7 +44,8 @@ public partial class BrowserTabView : IDisposable private void ClickClose(object sender, RoutedEventArgs e) { var tc = (TabControl) this.Parent; - tc.Items.Remove(this); + if (tc.Items.Contains(this)) + tc.Items.Remove(this); this.Dispose(); } } diff --git a/Wabbajack.App.Wpf/Views/MainWindow.xaml b/Wabbajack.App.Wpf/Views/MainWindow.xaml index 617beef5..1bd48b9d 100644 --- a/Wabbajack.App.Wpf/Views/MainWindow.xaml +++ b/Wabbajack.App.Wpf/Views/MainWindow.xaml @@ -37,7 +37,10 @@ + WABBAJACK 3.0.0 + + diff --git a/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs b/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs index c49b23bc..3de72b1a 100644 --- a/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs +++ b/Wabbajack.App.Wpf/Views/MainWindow.xaml.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using System.Windows; @@ -51,6 +52,12 @@ namespace Wabbajack TaskbarItemInfo.ProgressState = u.State; }); + MessageBus.Current.Listen() + .Subscribe(OnOpenBrowserTab); + + MessageBus.Current.Listen() + .Subscribe(OnCloseBrowserTab); + _logger.LogInformation("Wabbajack Build - {Sha}",ThisAssembly.Git.Sha); _logger.LogInformation("Running in {EntryPoint}", KnownFolders.EntryPoint); @@ -97,6 +104,9 @@ namespace Wabbajack { this.Topmost = false; }; + + ((MainWindowVM) DataContext).WhenAnyValue(vm => vm.OpenSettingsCommand) + .BindTo(this, view => view.SettingsButton.Command); } catch (Exception ex) { @@ -196,5 +206,15 @@ namespace Wabbajack Tabs.Items.Add(tab); Tabs.SelectedItem = tab; } + + private void OnCloseBrowserTab(CloseBrowserTab msg) + { + foreach (var tab in Tabs.Items.OfType()) + { + if (tab.DataContext != msg.ViewModel) continue; + Tabs.Items.Remove(tab); + break; + } + } } } diff --git a/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj b/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj index 92f2d87b..887cd6dc 100644 --- a/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj +++ b/Wabbajack.App.Wpf/Wabbajack.App.Wpf.csproj @@ -63,6 +63,7 @@ NU1701 + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -76,7 +77,7 @@ - +