Woot, we can log into the Nexus through 3.0 code

This commit is contained in:
Timothy Baldridge 2022-01-03 15:46:28 -07:00
parent 149dd9b07e
commit 9f87d91918
22 changed files with 355 additions and 51 deletions

View File

@ -15,6 +15,7 @@ using Wabbajack.DTOs;
using Wabbajack.LoginManagers;
using Wabbajack.Models;
using Wabbajack.Services.OSIntegrated;
using Wabbajack.UserIntervention;
using Wabbajack.Util;
namespace Wabbajack
@ -47,6 +48,9 @@ namespace Wabbajack
RxApp.MainThreadScheduler = new DispatcherScheduler(Dispatcher.CurrentDispatcher);
services.AddOSIntegrated();
services.AddSingleton<CefService>();
services.AddTransient<MainWindow>();
services.AddTransient<MainWindowVM>();
services.AddSingleton<SystemParametersConstructor>();
@ -60,6 +64,10 @@ namespace Wabbajack
services.AddTransient<ModeSelectionVM>();
services.AddTransient<ModListGalleryVM>();
services.AddTransient<InstallerVM>();
services.AddTransient<WebBrowserVM>();
services.AddTransient<NexusLoginHandler>();
// Login Managers
services.AddAllSingleton<INeedsLogin, NexusLoginManager>();

View File

@ -1,5 +1,6 @@
using System;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack;
@ -9,6 +10,7 @@ public static class Consts
public static Uri WabbajackBuildServerUri => new("https://build.wabbajack.org");
public static Version CurrentMinimumWabbajackVersion { get; set; } = Version.Parse("2.3.0.0");
public static bool UseNetworkWorkaroundMode { get; set; } = false;
public static AbsolutePath CefCacheLocation { get; } = KnownFolders.WabbajackAppLocal.Combine("Cef");
public static byte SettingsVersion = 0;

View File

@ -2,10 +2,12 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Interventions;
namespace Wabbajack
@ -18,6 +20,12 @@ namespace Wabbajack
private bool _handled;
public bool Handled { get => _handled; set => this.RaiseAndSetIfChanged(ref _handled, value); }
public CancellationToken Token { get; }
public void SetException(Exception exception)
{
throw new NotImplementedException();
}
public abstract void Cancel();
public ICommand CancelCommand { get; }

View File

@ -1,23 +0,0 @@
using ReactiveUI;
namespace Wabbajack.Interventions
{
/// <summary>
/// Defines a message that requires user interaction. The user must perform some action
/// or make a choice.
/// </summary>
public interface IUserIntervention : IReactiveObject
{
/// <summary>
/// The user didn't make a choice, so this action should be aborted
/// </summary>
void Cancel();
/// <summary>
/// Whether the interaction has been handled and no longer needs attention
/// Note: This needs to be Reactive so that users can monitor its status
/// </summary>
bool Handled { get; }
}
}

View File

@ -11,7 +11,8 @@ public class NavigateToGlobal
Installer,
Settings,
Compiler,
ModListContents
ModListContents,
WebBrowser
}
public ScreenType Screen { get; }

View File

@ -1,16 +1,28 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using ReactiveUI;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Networking.Http.Interfaces;
namespace Wabbajack.Messages;
public class NexusLogin
public class NexusLogin : IUserIntervention
{
private TaskCompletionSource CompletionSource { get; }
private readonly CancellationTokenSource _source;
public TaskCompletionSource CompletionSource { get; }
public CancellationToken Token => _source.Token;
public void SetException(Exception exception)
{
CompletionSource.SetException(exception);
_source.Cancel();
}
public NexusLogin()
{
CompletionSource = new TaskCompletionSource();
_source = new CancellationTokenSource();
}
public static Task Send()
@ -19,4 +31,12 @@ public class NexusLogin
MessageBus.Current.SendMessage(msg);
return msg.CompletionSource.Task;
}
public void Cancel()
{
_source.Cancel();
CompletionSource.TrySetCanceled();
}
public bool Handled => CompletionSource.Task.IsCompleted;
}

View File

@ -0,0 +1,70 @@
using System;
using System.Threading.Tasks;
using CefSharp;
using CefSharp.Wpf;
using Microsoft.Extensions.Logging;
namespace Wabbajack.Models;
public class CefService
{
private readonly ILogger<CefService> _logger;
private bool Inited { get; set; } = false;
public Func<IBrowser, IFrame, string, IRequest, IResourceHandler>? SchemeHandler { get; set; }
public CefService(ILogger<CefService> logger)
{
_logger = logger;
Inited = false;
Init();
}
public IWebBrowser CreateBrowser()
{
return new ChromiumWebBrowser();
}
private void Init()
{
if (Inited || Cef.IsInitialized) return;
Inited = true;
var settings = new CefSettings
{
CachePath = Consts.CefCacheLocation.ToString(),
JavascriptFlags = "--noexpose_wasm"
};
settings.RegisterScheme(new CefCustomScheme()
{
SchemeName = "wabbajack",
SchemeHandlerFactory = new SchemeHandlerFactor(_logger, this)
});
_logger.LogInformation("Initializing Cef");
if (!Cef.Initialize(settings))
{
_logger.LogError("Cannot initialize CEF");
}
}
private class SchemeHandlerFactor : ISchemeHandlerFactory
{
private readonly ILogger _logger;
private readonly CefService _service;
internal SchemeHandlerFactor(ILogger logger, CefService service)
{
_logger = logger;
_service = service;
}
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request)
{
_logger.LogInformation("Scheme handler Got: {Scheme} : {Url}", schemeName, request.Url);
if (_service.SchemeHandler != null && schemeName == "wabbajack")
{
return _service.SchemeHandler!(browser, frame, schemeName, request);
}
return new ResourceHandler();
}
}
}

View File

@ -103,7 +103,7 @@ public class LoggerProvider : ILoggerProvider
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
Debug.WriteLine(formatter(state, exception));
Debug.WriteLine($"{logLevel} - {formatter(state, exception)}");
_provider._messages.OnNext(new LogMessage<TState>(DateTime.UtcNow, _provider.NextMessageId(), logLevel,
eventId, state, exception, formatter));
}

View File

@ -0,0 +1,111 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using Wabbajack.DTOs.Logins;
using Wabbajack.LibCefHelpers;
using Wabbajack.Messages;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Services.OSIntegrated.TokenProviders;
namespace Wabbajack.UserIntervention;
public class NexusLoginHandler : WebUserInterventionBase
{
private readonly ITokenProvider<NexusApiState> _provider;
public NexusLoginHandler(ILogger<NexusLoginHandler> logger, WebBrowserVM browserVM, ITokenProvider<NexusApiState> provider) : base(logger, browserVM)
{
_provider = provider;
}
public async Task Begin()
{
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"));
Helpers.Cookie[] cookies = {};
while (true)
{
cookies = await Driver.GetCookies("nexusmods.com");
if (cookies.Any(c => c.Name == "member_id"))
break;
Message.Token.ThrowIfCancellationRequested();
await Task.Delay(500, Message.Token);
}
await NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api"));
UpdateStatus("Looking for API Key");
var key = "";
while (true)
{
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 _provider.SetToken(new NexusApiState()
{
ApiKey = key,
Cookies = cookies.Select(c => new Cookie()
{
Domain = c.Domain,
Name = c.Name,
Path = c.Path,
Value = c.Value
}).ToArray()
});
((NexusLogin)Message).CompletionSource.SetResult();
Messages.NavigateTo.Send(PrevPane);
}
catch (Exception ex)
{
Logger.LogError(ex, "While logging into Nexus Mods");
Message.SetException(ex);
Messages.NavigateTo.Send(PrevPane);
}
}
}

View File

@ -0,0 +1,42 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Interventions;
using Wabbajack.WebAutomation;
namespace Wabbajack.UserIntervention;
public class WebUserInterventionBase
{
protected readonly WebBrowserVM Browser;
protected readonly ILogger Logger;
protected IUserIntervention Message;
protected ViewModel PrevPane;
protected IWebDriver Driver;
public WebUserInterventionBase(ILogger logger, WebBrowserVM browser)
{
Logger = logger;
Browser = browser;
Driver = new CefSharpWrapper(logger, browser.Browser);
}
public void Configure(ViewModel prevPane, IUserIntervention message)
{
Message = message;
PrevPane = prevPane;
}
protected void UpdateStatus(string status)
{
Browser.Instructions = status;
}
protected async Task NavigateTo(Uri uri)
{
await Driver.NavigateTo(uri, Message.Token);
}
}

View File

@ -12,6 +12,7 @@ using System.Reactive.Disposables;
using System.Reactive.Linq;
using DynamicData.Binding;
using ReactiveUI.Fody.Helpers;
using Wabbajack.DTOs.Interventions;
namespace Wabbajack
{

View File

@ -1,13 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ReactiveUI;
using Wabbajack.Common;
using System.Threading.Tasks;
using Wabbajack.Installer;
using Wabbajack;
using Wabbajack.Interventions;
using Wabbajack.DTOs.Interventions;
namespace Wabbajack
{

View File

@ -13,6 +13,7 @@ using Wabbajack.Common;
using Wabbajack.Installer;
using Wabbajack;
using Wabbajack.DTOs;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Interventions;
using Wabbajack.Util;
@ -194,7 +195,7 @@ namespace Wabbajack
*/
return true;
}
public IUserIntervention InterventionConverter(IUserIntervention intervention)
{
switch (intervention)

View File

@ -2,6 +2,7 @@
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
@ -16,10 +17,12 @@ using Wabbajack.Common;
using Wabbajack.Downloaders.GameFile;
using Wabbajack;
using Wabbajack.Interventions;
using Wabbajack.LoginManagers;
using Wabbajack.Messages;
using Wabbajack.Models;
using Wabbajack.Networking.WabbajackClientApi;
using Wabbajack.Paths;
using Wabbajack.UserIntervention;
using Wabbajack.View_Models;
namespace Wabbajack
@ -44,12 +47,16 @@ namespace Wabbajack
public readonly Lazy<SettingsVM> SettingsPane;
public readonly ModListGalleryVM Gallery;
public readonly ModeSelectionVM ModeSelectionVM;
public readonly WebBrowserVM WebBrowserVM;
public readonly Lazy<ModListContentsVM> ModListContentsVM;
public readonly UserInterventionHandlers UserInterventionHandlers;
private readonly Client _wjClient;
private readonly ILogger<MainWindowVM> _logger;
private readonly ResourceMonitor _resourceMonitor;
private List<ViewModel> PreviousPanes = new();
private readonly IServiceProvider _serviceProvider;
public ICommand CopyVersionCommand { get; }
public ICommand ShowLoginManagerVM { get; }
public ICommand OpenSettingsCommand { get; }
@ -64,11 +71,12 @@ namespace Wabbajack
public MainWindowVM(ILogger<MainWindowVM> logger, MainSettings settings, Client wjClient,
IServiceProvider serviceProvider, ModeSelectionVM modeSelectionVM, ModListGalleryVM modListGalleryVM, ResourceMonitor resourceMonitor,
InstallerVM installer)
InstallerVM installer, WebBrowserVM webBrowserVM)
{
_logger = logger;
_wjClient = wjClient;
_resourceMonitor = resourceMonitor;
_serviceProvider = serviceProvider;
ConverterRegistration.Register();
Settings = settings;
Installer = installer;
@ -76,12 +84,25 @@ namespace Wabbajack
SettingsPane = new Lazy<SettingsVM>(() => new SettingsVM(serviceProvider.GetRequiredService<ILogger<SettingsVM>>(), this, serviceProvider));
Gallery = modListGalleryVM;
ModeSelectionVM = modeSelectionVM;
WebBrowserVM = webBrowserVM;
ModListContentsVM = new Lazy<ModListContentsVM>(() => new ModListContentsVM(serviceProvider.GetRequiredService<ILogger<ModListContentsVM>>(), this));
UserInterventionHandlers = new UserInterventionHandlers(serviceProvider.GetRequiredService<ILogger<UserInterventionHandlers>>(), this);
MessageBus.Current.Listen<NavigateToGlobal>()
.Subscribe(m => HandleNavigateTo(m.Screen))
.DisposeWith(CompositeDisposable);
MessageBus.Current.Listen<NavigateTo>()
.Subscribe(m => HandleNavigateTo(m.ViewModel))
.DisposeWith(CompositeDisposable);
MessageBus.Current.Listen<NavigateBack>()
.Subscribe(HandleNavigateBack)
.DisposeWith(CompositeDisposable);
MessageBus.Current.Listen<NexusLogin>()
.Subscribe(m => HandleNexusLogin(m))
.DisposeWith(CompositeDisposable);
_resourceMonitor.Updates
.Select(r => string.Join(", ", r.Where(r => r.Throughput > 0)
@ -169,6 +190,24 @@ namespace Wabbajack
execute: () => NavigateToGlobal.Send(NavigateToGlobal.ScreenType.Settings));
}
private void HandleNavigateTo(ViewModel objViewModel)
{
ActivePane = objViewModel;
}
private void HandleNexusLogin(NexusLogin nexusLogin)
{
var handler = _serviceProvider.GetRequiredService<NexusLoginHandler>();
handler.Configure(ActivePane, nexusLogin);
handler.Begin().FireAndForget();
}
private void HandleNavigateBack(NavigateBack navigateBack)
{
ActivePane = PreviousPanes.Last();
PreviousPanes.RemoveAt(PreviousPanes.Count - 1);
}
private void HandleNavigateTo(NavigateToGlobal.ScreenType s)
{
ActivePane = s switch
@ -181,6 +220,7 @@ namespace Wabbajack
};
}
private static bool IsStartingFromModlist(out AbsolutePath modlistPath)
{
/* TODO

View File

@ -2,9 +2,11 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Interventions;
namespace Wabbajack
@ -16,6 +18,11 @@ namespace Wabbajack
public MO2InstallerVM Installer { get; }
public bool Handled => ((IUserIntervention)Source).Handled;
public CancellationToken Token { get; }
public void SetException(Exception exception)
{
throw new NotImplementedException();
}
public int CpuID => 0;

View File

@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Interventions;
using Wabbajack.Messages;
@ -23,12 +24,13 @@ namespace Wabbajack
MainWindow = mvm;
}
private async Task WrapBrowserJob(IUserIntervention intervention, Func<WebBrowserVM, CancellationTokenSource, Task> toDo)
private async Task WrapBrowserJob(IUserIntervention intervention, WebBrowserVM vm, Func<WebBrowserVM, CancellationTokenSource, Task> toDo)
{
var wait = await _browserLock.WaitAsync();
var cancel = new CancellationTokenSource();
var oldPane = MainWindow.ActivePane;
using var vm = await WebBrowserVM.GetNew(_logger);
// TODO: FIX using var vm = await WebBrowserVM.GetNew(_logger);
NavigateTo.Send(vm);
vm.BackCommand = ReactiveCommand.Create(() =>
{

View File

@ -13,19 +13,22 @@ using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack;
using Wabbajack.LibCefHelpers;
using Wabbajack.Messages;
using Wabbajack.Models;
using Wabbajack.WebAutomation;
namespace Wabbajack
{
public class WebBrowserVM : ViewModel, IBackNavigatingVM, IDisposable
{
private readonly ILogger _logger;
private readonly ILogger<WebBrowserVM> _logger;
private readonly CefService _cefService;
[Reactive]
public string Instructions { get; set; }
public IWebBrowser Browser { get; } = new ChromiumWebBrowser();
public CefSharpWrapper Driver => new(_logger, Browser);
public IWebBrowser Browser { get; }
public CefSharpWrapper Driver { get; set; }
[Reactive]
public ViewModel NavigateBackTarget { get; set; }
@ -36,17 +39,17 @@ namespace Wabbajack
public Subject<bool> IsBackEnabledSubject { get; } = new Subject<bool>();
public IObservable<bool> IsBackEnabled { get; }
private WebBrowserVM(ILogger logger, string url = "http://www.wabbajack.org")
public WebBrowserVM(ILogger<WebBrowserVM> logger, CefService cefService)
{
// CefService is required so that Cef is initalized
_logger = logger;
IsBackEnabled = IsBackEnabledSubject.StartWith(true);
_cefService = cefService;
Instructions = "Wabbajack Web Browser";
}
BackCommand = ReactiveCommand.Create(NavigateBack.Send);
Browser = cefService.CreateBrowser();
Driver = new CefSharpWrapper(_logger, Browser);
public static async Task<WebBrowserVM> GetNew(ILogger logger, string url = "http://www.wabbajack.org")
{
// Make sure libraries are extracted first
return new WebBrowserVM(logger, url);
}
public override void Dispose()

View File

@ -35,6 +35,7 @@ namespace Wabbajack.WebAutomation
_browser.LoadingStateChanged -= handler;
tcs.SetResult(true);
};
_browser.LoadingStateChanged += handler;
_browser.Load(uri.ToString());
token?.Register(() => tcs.TrySetCanceled());

View File

@ -14,6 +14,7 @@ namespace Wabbajack.WebAutomation
Task NavigateTo(Uri uri, CancellationToken? token = null);
Task<string> EvaluateJavaScript(string text);
Task<Helpers.Cookie[]> GetCookies(string domainPrefix);
public Action<Uri>? DownloadHandler { get; set; }
public Action<Uri>? DownloadHandler { get; set; }
public Task WaitForInitialized();
}
}

View File

@ -102,6 +102,11 @@ public class ForceHeal : IVerb
var ini = meta.LoadIniFile();
var state = await _downloadDispatcher.ResolveArchive(ini["General"].ToDictionary(d => d.KeyName, d => d.Value));
if (state == null)
{
_logger.LogError("Cannot resolve state from meta for {File}", file);
throw new Exception($"Cannot resolve state from meta for {file}");
}
_logger.LogInformation("Hashing {File}", file.FileName);
var hash = await _fileHashCache.FileHashCachedAsync(file, CancellationToken.None);

View File

@ -1,3 +1,6 @@
using System;
using System.Threading;
namespace Wabbajack.DTOs.Interventions;
/// <summary>
@ -15,4 +18,11 @@ public interface IUserIntervention
/// Whether the interaction has been handled and no longer needs attention
/// </summary>
bool Handled { get; }
/// <summary>
/// Token that can be used to trigger cancellation when Cancel() is called.
/// </summary>
public CancellationToken Token { get; }
void SetException(Exception exception);
}

View File

@ -8,4 +8,5 @@ public abstract class GithubAuthTokenProvider : ITokenProvider<string>
public abstract ValueTask<string> Get();
public abstract ValueTask SetToken(string val);
public abstract ValueTask<bool> Delete();
public abstract bool HaveToken();
}