diff --git a/Wabbajack.App.Wpf/App.xaml.cs b/Wabbajack.App.Wpf/App.xaml.cs index d1aa71e9..970d2d02 100644 --- a/Wabbajack.App.Wpf/App.xaml.cs +++ b/Wabbajack.App.Wpf/App.xaml.cs @@ -8,6 +8,8 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using ReactiveUI; using Wabbajack.DTOs; +using Wabbajack.DTOs.Interventions; +using Wabbajack.Interventions; using Wabbajack.LoginManagers; using Wabbajack.Models; using Wabbajack.Services.OSIntegrated; @@ -46,6 +48,7 @@ namespace Wabbajack services.AddOSIntegrated(); services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); services.AddTransient(); @@ -73,6 +76,7 @@ namespace Wabbajack services.AddAllSingleton(); services.AddAllSingleton(); services.AddAllSingleton(); + services.AddSingleton(); return services; } diff --git a/Wabbajack.App.Wpf/Interventions/InteventionHandler.cs b/Wabbajack.App.Wpf/Interventions/InteventionHandler.cs new file mode 100644 index 00000000..d3748ac4 --- /dev/null +++ b/Wabbajack.App.Wpf/Interventions/InteventionHandler.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Logging; +using ReactiveUI; +using Wabbajack.DTOs.Interventions; + +namespace Wabbajack.Interventions; + +public class InteventionHandler : IUserInterventionHandler +{ + private readonly ILogger _logger; + + public InteventionHandler(ILogger logger) + { + _logger = logger; + } + public void Raise(IUserIntervention intervention) + { + // Recast these or they won't be properly handled by the message bus + if (intervention is ManualDownload md) + MessageBus.Current.SendMessage(md); + } +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/LoginManagers/LoversLabLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/LoversLabLoginManager.cs index e3b338f2..6f3a68f0 100644 --- a/Wabbajack.App.Wpf/LoginManagers/LoversLabLoginManager.cs +++ b/Wabbajack.App.Wpf/LoginManagers/LoversLabLoginManager.cs @@ -7,6 +7,7 @@ using System.Windows.Controls; 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; @@ -15,6 +16,7 @@ using Wabbajack.DTOs.Interventions; using Wabbajack.DTOs.Logins; using Wabbajack.Messages; using Wabbajack.Networking.Http.Interfaces; +using Wabbajack.UserIntervention; namespace Wabbajack.LoginManagers; @@ -23,6 +25,7 @@ public class LoversLabLoginManager : ViewModel, INeedsLogin private readonly ILogger _logger; private readonly ITokenProvider _token; private readonly IUserInterventionHandler _handler; + private readonly IServiceProvider _serviceProvider; public string SiteName { get; } = "Lovers Lab"; public ICommand TriggerLogin { get; set; } @@ -33,10 +36,11 @@ public class LoversLabLoginManager : ViewModel, INeedsLogin [Reactive] public bool HaveLogin { get; set; } - public LoversLabLoginManager(ILogger logger, ITokenProvider token) + public LoversLabLoginManager(ILogger logger, ITokenProvider token, IServiceProvider serviceProvider) { _logger = logger; _token = token; + _serviceProvider = serviceProvider; RefreshTokenState(); ClearLogin = ReactiveCommand.CreateFromTask(async () => @@ -52,8 +56,8 @@ public class LoversLabLoginManager : ViewModel, INeedsLogin TriggerLogin = ReactiveCommand.CreateFromTask(async () => { _logger.LogInformation("Logging into {SiteName}", SiteName); - await LoversLabLogin.Send(); - RefreshTokenState(); + + MessageBus.Current.SendMessage(new OpenBrowserTab(_serviceProvider.GetRequiredService())); }, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v)); } diff --git a/Wabbajack.App.Wpf/LoginManagers/VectorPlexusLoginManager.cs b/Wabbajack.App.Wpf/LoginManagers/VectorPlexusLoginManager.cs index 4c64eb41..bbc928d1 100644 --- a/Wabbajack.App.Wpf/LoginManagers/VectorPlexusLoginManager.cs +++ b/Wabbajack.App.Wpf/LoginManagers/VectorPlexusLoginManager.cs @@ -7,6 +7,7 @@ using System.Windows.Controls; 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; @@ -15,6 +16,7 @@ using Wabbajack.DTOs.Interventions; using Wabbajack.DTOs.Logins; using Wabbajack.Messages; using Wabbajack.Networking.Http.Interfaces; +using Wabbajack.UserIntervention; namespace Wabbajack.LoginManagers; @@ -23,6 +25,7 @@ public class VectorPlexusLoginManager : ViewModel, INeedsLogin private readonly ILogger _logger; private readonly ITokenProvider _token; private readonly IUserInterventionHandler _handler; + private readonly IServiceProvider _serviceProvider; public string SiteName { get; } = "Vector Plexus"; public ICommand TriggerLogin { get; set; } @@ -33,10 +36,11 @@ public class VectorPlexusLoginManager : ViewModel, INeedsLogin [Reactive] public bool HaveLogin { get; set; } - public VectorPlexusLoginManager(ILogger logger, ITokenProvider token) + public VectorPlexusLoginManager(ILogger logger, ITokenProvider token, IServiceProvider serviceProvider) { _logger = logger; _token = token; + _serviceProvider = serviceProvider; RefreshTokenState(); ClearLogin = ReactiveCommand.CreateFromTask(async () => @@ -52,8 +56,7 @@ public class VectorPlexusLoginManager : ViewModel, INeedsLogin TriggerLogin = ReactiveCommand.CreateFromTask(async () => { _logger.LogInformation("Logging into {SiteName}", SiteName); - await VectorPlexusLogin.Send(); - RefreshTokenState(); + MessageBus.Current.SendMessage(new OpenBrowserTab(_serviceProvider.GetRequiredService())); }, this.WhenAnyValue(v => v.HaveLogin).Select(v => !v)); } diff --git a/Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs b/Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs new file mode 100644 index 00000000..8e3697c0 --- /dev/null +++ b/Wabbajack.App.Wpf/UserIntervention/ManualDownloadHandler.cs @@ -0,0 +1,27 @@ +using System.Security.Policy; +using System.Threading; +using System.Threading.Tasks; +using Wabbajack.DTOs; +using Wabbajack.DTOs.DownloadStates; +using Wabbajack.DTOs.Interventions; +using Wabbajack.Paths; + +namespace Wabbajack.UserIntervention; + +public class ManualDownloadHandler : BrowserTabViewModel +{ + public ManualDownload Intervention { get; set; } + protected override async Task Run(CancellationToken token) + { + await WaitForReady(); + var archive = Intervention.Archive; + var md = Intervention.Archive.State as Manual; + + Instructions = string.IsNullOrWhiteSpace(md.Prompt) ? $"Please download {archive.Name}" : md.Prompt; + await NavigateTo(md.Url); + + var uri = await WaitForDownloadUri(token); + + Intervention.Finish(uri); + } +} \ No newline at end of file diff --git a/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs b/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs index afaec236..c170bfdf 100644 --- a/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs +++ b/Wabbajack.App.Wpf/UserIntervention/OAuth2LoginHandler.cs @@ -17,7 +17,7 @@ using Wabbajack.Services.OSIntegrated; namespace Wabbajack.UserIntervention; -public abstract class OAuth2LoginHandler : WebUserInterventionBase +public abstract class OAuth2LoginHandler : BrowserTabViewModel where TIntervention : IUserIntervention where TLoginType : OAuth2LoginState, new() { diff --git a/Wabbajack.App.Wpf/View Models/BrowserTabViewModel.cs b/Wabbajack.App.Wpf/View Models/BrowserTabViewModel.cs index dbe7214e..dc770d14 100644 --- a/Wabbajack.App.Wpf/View Models/BrowserTabViewModel.cs +++ b/Wabbajack.App.Wpf/View Models/BrowserTabViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading; @@ -9,6 +10,7 @@ using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Wpf; using ReactiveUI; using ReactiveUI.Fody.Helpers; +using Wabbajack.DTOs.Interventions; using Wabbajack.DTOs.Logins; using Wabbajack.Messages; using Wabbajack.Views; @@ -83,11 +85,38 @@ public abstract class BrowserTabViewModel : ViewModel 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; } + + public async Task WaitForDownloadUri(CancellationToken token) + { + var source = new TaskCompletionSource(); + var referer = _browser.Source; + _browser.CoreWebView2.DownloadStarting += (sender, args) => + { + try + { + + source.SetResult(new Uri(args.DownloadOperation.Uri)); + } + catch (Exception ex) + { + source.SetCanceled(); + } + + args.Handled = true; + args.Cancel = true; + }; + + var uri = await source.Task.WaitAsync(token); + var cookies = await GetCookies(uri.Host, token); + return new ManualDownload.BrowserDownloadState(uri, cookies, new[] + { + ("Referer", referer.ToString()) + }); + } } \ No newline at end of file diff --git a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs index 7212816c..4b4b8f9c 100644 --- a/Wabbajack.App.Wpf/View Models/MainWindowVM.cs +++ b/Wabbajack.App.Wpf/View Models/MainWindowVM.cs @@ -17,6 +17,7 @@ using Microsoft.Extensions.Logging; using Wabbajack.Common; using Wabbajack.Downloaders.GameFile; using Wabbajack; +using Wabbajack.DTOs.Interventions; using Wabbajack.Interventions; using Wabbajack.LoginManagers; using Wabbajack.Messages; @@ -112,6 +113,10 @@ namespace Wabbajack MessageBus.Current.Listen() .Subscribe(HandleLogin) .DisposeWith(CompositeDisposable); + + MessageBus.Current.Listen() + .Subscribe(HandleManualDownload) + .DisposeWith(CompositeDisposable); _resourceMonitor.Updates .Select(r => string.Join(", ", r.Where(r => r.Throughput > 0) @@ -178,7 +183,6 @@ namespace Wabbajack { var handler = _serviceProvider.GetRequiredService(); handler.RunWrapper(CancellationToken.None).FireAndForget(); - } private void HandleNavigateBack(NavigateBack navigateBack) @@ -186,6 +190,13 @@ namespace Wabbajack ActivePane = PreviousPanes.Last(); PreviousPanes.RemoveAt(PreviousPanes.Count - 1); } + + private void HandleManualDownload(ManualDownload manualDownload) + { + var handler = _serviceProvider.GetRequiredService(); + handler.Intervention = manualDownload; + MessageBus.Current.SendMessage(new OpenBrowserTab(handler)); + } private void HandleNavigateTo(NavigateToGlobal.ScreenType s) { diff --git a/Wabbajack.DTOs/Interventions/ManualDownload.cs b/Wabbajack.DTOs/Interventions/ManualDownload.cs new file mode 100644 index 00000000..d9d16a83 --- /dev/null +++ b/Wabbajack.DTOs/Interventions/ManualDownload.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using Wabbajack.DTOs.Logins; +using Wabbajack.Hashing.xxHash64; +using Wabbajack.Paths; + +namespace Wabbajack.DTOs.Interventions; + +public class ManualDownload : AUserIntervention +{ + public Archive Archive { get; } + public AbsolutePath OutputPath { get; } + + public ManualDownload(Archive archive, AbsolutePath outputPath) + { + Archive = archive; + OutputPath = outputPath; + } + + public record BrowserDownloadState(Uri Uri, Cookie[] Cookies, (string Key, string Value)[] Headers) + { + + } +} \ No newline at end of file diff --git a/Wabbajack.Downloaders.Manual/ManualDownloader.cs b/Wabbajack.Downloaders.Manual/ManualDownloader.cs index 7c0b7dcb..172b48a6 100644 --- a/Wabbajack.Downloaders.Manual/ManualDownloader.cs +++ b/Wabbajack.Downloaders.Manual/ManualDownloader.cs @@ -1,19 +1,57 @@ -using Wabbajack.Downloaders.Interfaces; +using Microsoft.Extensions.Logging; +using Wabbajack.Downloaders.Interfaces; using Wabbajack.DTOs; using Wabbajack.DTOs.DownloadStates; +using Wabbajack.DTOs.Interventions; using Wabbajack.DTOs.Validation; using Wabbajack.Hashing.xxHash64; using Wabbajack.Paths; +using Wabbajack.Paths.IO; using Wabbajack.RateLimiter; namespace Wabbajack.Downloaders.Manual; public class ManualDownloader : ADownloader { - public override Task Download(Archive archive, DTOs.DownloadStates.Manual state, AbsolutePath destination, IJob job, CancellationToken token) + private readonly ILogger _logger; + private readonly IUserInterventionHandler _interventionHandler; + private readonly IResource _limiter; + private readonly HttpClient _client; + + public ManualDownloader(ILogger logger, IUserInterventionHandler interventionHandler, HttpClient client) { - throw new NotImplementedException(); + _logger = logger; + _interventionHandler = interventionHandler; + _client = client; } + + public override async Task Download(Archive archive, DTOs.DownloadStates.Manual state, AbsolutePath destination, IJob job, CancellationToken token) + { + _logger.LogInformation("Starting manual download of {Url}", state.Url); + var intervention = new ManualDownload(archive, destination); + _interventionHandler.Raise(intervention); + var browserState = await intervention.Task; + + var msg = new HttpRequestMessage(HttpMethod.Get, browserState.Uri); + msg.Headers.Add("User-Agent", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Safari/537.36"); + msg.Headers.Add("Cookie", string.Join(";", browserState.Cookies.Select(c => $"{c.Name}={c.Value}"))); + + + foreach (var header in browserState.Headers) + { + msg.Headers.Add(header.Key, header.Value); + } + + using var response = await _client.SendAsync(msg, token); + if (!response.IsSuccessStatusCode) + throw new HttpRequestException(response.ReasonPhrase, null, statusCode:response.StatusCode); + + await using var strm = await response.Content.ReadAsStreamAsync(token); + await using var os = destination.Open(FileMode.Create, FileAccess.Write, FileShare.None); + return await strm.HashingCopy(os, token, job); + } + public override async Task Prepare() { diff --git a/Wabbajack.Downloaders.Manual/Wabbajack.Downloaders.Manual.csproj b/Wabbajack.Downloaders.Manual/Wabbajack.Downloaders.Manual.csproj index 222ac4dd..30490c37 100644 --- a/Wabbajack.Downloaders.Manual/Wabbajack.Downloaders.Manual.csproj +++ b/Wabbajack.Downloaders.Manual/Wabbajack.Downloaders.Manual.csproj @@ -10,4 +10,8 @@ + + + +