Completely rework CLI's DI process

This commit is contained in:
Timothy Baldridge
2022-01-26 17:24:13 -07:00
parent a55a64c7a9
commit 72f0b43928
38 changed files with 328 additions and 614 deletions

View File

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.Threading.Tasks;
@ -9,20 +10,26 @@ namespace Wabbajack.CLI;
public class CommandLineBuilder
{
private readonly IConsole _console;
private readonly IEnumerable<IVerb> _verbs;
private readonly VerbRegistrar _verbs;
private readonly IServiceProvider _serviceProvider;
public CommandLineBuilder(IEnumerable<IVerb> verbs, IConsole console, LoggingRateLimiterReporter _)
public CommandLineBuilder(VerbRegistrar verbs, IConsole console, LoggingRateLimiterReporter _, IServiceProvider serviceProvider)
{
_console = console;
_verbs = verbs;
_serviceProvider = serviceProvider;
}
public async Task<int> Run(string[] args)
{
var root = new RootCommand();
foreach (var verb in _verbs)
root.Add(verb.MakeCommand());
foreach (var verb in _verbs.Definitions)
{
var command = verb.MakeCommand();
command.Handler = AVerb.WrapHandler(verb.VerbType, _serviceProvider);
root.Add(command);
}
return await root.InvokeAsync(args);
}
}

View File

@ -14,7 +14,6 @@ using Wabbajack.DTOs.GitHub;
using Wabbajack.DTOs.Interventions;
using Wabbajack.Networking.Http;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Networking.WabbajackClientApi;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.Server.Lib;
@ -32,7 +31,7 @@ internal class Program
new TypeConverterAttribute(typeof(AbsolutePathTypeConverter)));
TypeDescriptor.AddAttributes(typeof(List),
new TypeConverterAttribute(typeof(ModListCategoryConverter)));
var host = Host.CreateDefaultBuilder(Array.Empty<string>())
.ConfigureServices((host, services) =>
{
@ -48,31 +47,48 @@ internal class Program
services.AddSingleton<Client>();
services.AddSingleton<Networking.WabbajackClientApi.Client>();
services.AddSingleton(s => new GitHubClient(new ProductHeaderValue("wabbajack")));
services.AddSingleton<VerbRegistrar>();
services.AddOSIntegrated();
services.AddServerLib();
services.AddTransient<Context>();
services.AddSingleton<IVerb, HashFile>();
services.AddSingleton<IVerb, VFSIndexFolder>();
services.AddSingleton<IVerb, Encrypt>();
services.AddSingleton<IVerb, Decrypt>();
services.AddSingleton<IVerb, ValidateLists>();
services.AddSingleton<IVerb, DownloadCef>();
services.AddSingleton<IVerb, DownloadUrl>();
services.AddSingleton<IVerb, GenerateMetricsReports>();
services.AddSingleton<IVerb, ForceHeal>();
services.AddSingleton<IVerb, MirrorFile>();
services.AddSingleton<IVerb, SteamLogin>();
services.AddSingleton<IVerb, SteamAppDumpInfo>();
services.AddSingleton<IVerb, SteamDownloadFile>();
services.AddSingleton<IVerb, UploadToNexus>();
services.AddSingleton<Encrypt>();
services.AddSingleton<HashFile>();
services.AddSingleton<DownloadCef>();
services.AddSingleton<Decrypt>();
services.AddSingleton<DownloadUrl>();
services.AddSingleton<ForceHeal>();
services.AddSingleton<MirrorFile>();
services.AddSingleton<SteamDownloadFile>();
services.AddSingleton<SteamLogin>();
services.AddSingleton<UploadToNexus>();
services.AddSingleton<ValidateLists>();
services.AddSingleton<VfsIndexFolder>();
services.AddSingleton<IUserInterventionHandler, UserInterventionHandler>();
}).Build();
var service = host.Services.GetService<CommandLineBuilder>();
var service = host.Services.GetRequiredService<CommandLineBuilder>();
var reg = host.Services.GetRequiredService<VerbRegistrar>();
reg.Register<Decrypt>(Decrypt.MakeCommand);
reg.Register<DownloadCef>(DownloadCef.MakeCommand);
reg.Register<DownloadUrl>(DownloadUrl.MakeCommand);
reg.Register<Encrypt>(Encrypt.MakeCommand);
reg.Register<HashFile>(HashFile.MakeCommand);
reg.Register<ForceHeal>(ForceHeal.MakeCommand);
reg.Register<MirrorFile>(MirrorFile.MakeCommand);
reg.Register<SteamDownloadFile>(SteamDownloadFile.MakeCommand);
reg.Register<SteamLogin>(SteamLogin.MakeCommand);
reg.Register<UploadToNexus>(UploadToNexus.MakeCommand);
reg.Register<ValidateLists>(ValidateLists.MakeCommand);
reg.Register<VfsIndexFolder>(VfsIndexFolder.MakeCommand);
return await service!.Run(args);
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.Linq.Expressions;
using System.Reflection;
using Wabbajack.CLI.Verbs;
namespace Wabbajack.CLI;
public class VerbRegistrar
{
public List<VerbDefinition> Definitions { get; }= new();
public void Register<T>(Func<Command> makeCommand)
{
Definitions.Add(new VerbDefinition(makeCommand, typeof(T)));
}
}
public record VerbDefinition(Func<Command> MakeCommand, Type VerbType)
{
}

View File

@ -0,0 +1,32 @@
using System;
using System.CommandLine.Invocation;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace Wabbajack.CLI.Verbs;
public abstract class AVerb
{
public static ICommandHandler WrapHandler(Type type, IServiceProvider provider)
{
return new WrappedHandler(type, provider);
}
protected abstract ICommandHandler GetHandler();
private class WrappedHandler : ICommandHandler
{
private readonly IServiceProvider _provider;
private readonly Type _type;
public WrappedHandler(Type type, IServiceProvider provider)
{
_provider = provider;
_type = type;
}
public Task<int> InvokeAsync(InvocationContext context)
{
var verb = (AVerb)_provider.GetRequiredService(_type);
return verb.GetHandler().InvokeAsync(context);
}
}
}

View File

@ -8,7 +8,7 @@ using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.CLI.Verbs;
public class Decrypt : IVerb
public class Decrypt : AVerb
{
private readonly ILogger<Decrypt> _logger;
@ -17,13 +17,12 @@ public class Decrypt : IVerb
_logger = logger;
}
public Command MakeCommand()
public static Command MakeCommand()
{
var command = new Command("decrypt");
command.Add(new Option<AbsolutePath>(new[] {"-o", "-output"}, "Output file path"));
command.Add(new Option<string>(new[] {"-n", "-name"}, "Name of the key to load data from"));
command.Description = "Decrypts a file from the Wabbajack encrypted storage";
command.Handler = CommandHandler.Create(Run);
return command;
}
@ -37,4 +36,9 @@ public class Decrypt : IVerb
return 0;
}
protected override ICommandHandler GetHandler()
{
return CommandHandler.Create(Run);
}
}

View File

@ -20,7 +20,7 @@ using Version = System.Version;
namespace Wabbajack.CLI.Verbs;
public class DownloadCef : IVerb
public class DownloadCef : AVerb
{
private readonly DownloadDispatcher _dispatcher;
private readonly FileExtractor.FileExtractor _fileExtractor;
@ -36,13 +36,12 @@ public class DownloadCef : IVerb
_httpClient = httpClient;
}
public Command MakeCommand()
public static Command MakeCommand()
{
var command = new Command("download-cef");
command.Add(new Option<AbsolutePath>(new[] {"-f", "-folder"}, "Path to Wabbajack"));
command.Add(new Option<bool>(new[] {"--force"}, "Force the download even if the output already exists"));
command.Description = "Downloads CEF into this folder";
command.Handler = CommandHandler.Create(Run);
return command;
}
@ -111,4 +110,9 @@ public class DownloadCef : IVerb
return 0;
}
protected override ICommandHandler GetHandler()
{
return CommandHandler.Create(Run);
}
}

View File

@ -10,7 +10,7 @@ using Wabbajack.Paths;
namespace Wabbajack.CLI.Verbs;
public class DownloadUrl : IVerb
public class DownloadUrl : AVerb
{
private readonly DownloadDispatcher _dispatcher;
private readonly ILogger<DownloadUrl> _logger;
@ -21,13 +21,12 @@ public class DownloadUrl : IVerb
_dispatcher = dispatcher;
}
public Command MakeCommand()
public static Command MakeCommand()
{
var command = new Command("download-url");
command.Add(new Option<Uri>(new[] {"-u", "-url"}, "Url to parse"));
command.Add(new Option<AbsolutePath>(new[] {"-o", "-output"}, "Output file"));
command.Description = "Downloads a file to a given output";
command.Handler = CommandHandler.Create(Run);
return command;
}
@ -45,4 +44,9 @@ public class DownloadUrl : IVerb
await _dispatcher.Download(archive, output, CancellationToken.None);
return 0;
}
protected override ICommandHandler GetHandler()
{
return CommandHandler.Create(Run);
}
}

View File

@ -8,7 +8,7 @@ using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.CLI.Verbs;
public class Encrypt : IVerb
public class Encrypt : AVerb
{
private readonly ILogger<Encrypt> _logger;
@ -17,16 +17,15 @@ public class Encrypt : IVerb
_logger = logger;
}
public Command MakeCommand()
public static Command MakeCommand()
{
var command = new Command("encrypt");
command.Add(new Option<AbsolutePath>(new[] {"-i", "-input"}, "Path to the file to enrypt"));
command.Add(new Option<string>(new[] {"-n", "-name"}, "Name of the key to store the data into"));
command.Description = "Encrypts a file and stores it in the Wabbajack encrypted storage";
command.Handler = CommandHandler.Create(Run);
return command;
}
public async Task<int> Run(AbsolutePath input, string name)
{
var data = await input.ReadAllBytesAsync();
@ -35,4 +34,9 @@ public class Encrypt : IVerb
.RelativeTo(KnownFolders.WabbajackAppLocal.Combine("encrypted")));
return 0;
}
protected override ICommandHandler GetHandler()
{
return CommandHandler.Create(Run);
}
}

View File

@ -24,7 +24,7 @@ using Wabbajack.VFS;
namespace Wabbajack.CLI.Verbs;
public class ForceHeal : IVerb
public class ForceHeal : AVerb
{
private readonly ILogger<ForceHeal> _logger;
private readonly Client _client;
@ -42,13 +42,12 @@ public class ForceHeal : IVerb
_httpClient = httpClient;
}
public Command MakeCommand()
public static Command MakeCommand()
{
var command = new Command("force-heal");
command.Add(new Option<AbsolutePath>(new[] {"-n", "-new-file"}, "New File"));
command.Add(new Option<string>(new[] {"-o", "-old-file"}, "Old File"));
command.Description = "Creates a patch from New file to Old File and uploads it";
command.Handler = CommandHandler.Create(Run);
return command;
}
@ -119,4 +118,9 @@ public class ForceHeal : IVerb
State = state!
};
}
protected override ICommandHandler GetHandler()
{
return CommandHandler.Create(Run);
}
}

View File

@ -1,68 +0,0 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.DTOs.ServerResponses;
using Wabbajack.Paths;
namespace Wabbajack.CLI.Verbs;
public class GenerateMetricsReports : IVerb
{
private readonly HttpClient _client;
private readonly DTOSerializer _dtos;
public GenerateMetricsReports(HttpClient client, DTOSerializer dtos)
{
_client = client;
_dtos = dtos;
}
public Command MakeCommand()
{
var command = new Command("generate-metrics-report");
command.Add(new Option<AbsolutePath>(new[] {"-o", "-output"}, "Output folder"));
command.Description = "Generates usage metrics and outputs a html report about them";
command.Handler = CommandHandler.Create(Run);
return command;
}
private async Task<int> Run(AbsolutePath output)
{
var subjects = await GetMetrics("one day ago", "now", "finish_install")
.Where(d => d.Action != d.Subject)
.Select(async d => d.GroupingSubject)
.ToHashSet();
var allTime = await GetMetrics("10 days ago", "now", "finish_install")
.Where(d => subjects.Contains(d.GroupingSubject))
.ToList();
var grouped = allTime.GroupBy(g => (g.Timestamp.ToString("yyyy-MM-dd"), g.GroupingSubject)).ToArray();
return 0;
}
private async IAsyncEnumerable<MetricResult> GetMetrics(string start, string end, string action)
{
await using var response = await _client.GetStreamAsync(new Uri($"https://build.wabbajack.org/metrics/report?action={action}&from={start}&end={end}"));
var sr = new StreamReader(response, leaveOpen: false);
while (true)
{
var line = await sr.ReadLineAsync();
if (line == null) break;
yield return _dtos.Deserialize<MetricResult>(line)!;
}
}
}

View File

@ -10,7 +10,7 @@ using Wabbajack.Paths.IO;
namespace Wabbajack.CLI.Verbs;
public class HashFile : IVerb
public class HashFile : AVerb
{
private readonly ILogger<HashFile> _logger;
@ -18,22 +18,25 @@ public class HashFile : IVerb
{
_logger = logger;
}
public Command MakeCommand()
public static Command MakeCommand()
{
var command = new Command("hash-file");
command.Add(new Option<AbsolutePath>(new[] {"-i", "-input"}, "Path to the file to hash"));
command.Description = "Hashes a file with Wabbajack's xxHash64 implementation";
command.Handler = CommandHandler.Create(Run);
return command;
}
protected override ICommandHandler GetHandler()
{
return CommandHandler.Create(Run);
}
public async Task<int> Run(AbsolutePath input)
{
await using var istream = input.Open(FileMode.Open, FileAccess.Read, FileShare.Read);
var hash = await istream.HashingCopy(Stream.Null, CancellationToken.None);
_logger.LogInformation($"{input} hash: {hash} {hash.ToHex()} {(long) hash}");
_logger.LogInformation("{Input} hash: {Hash} {HashAsHex} {HashAsLong}", input, hash, hash.ToHex(), (long)hash);
return 0;
}
}

View File

@ -1,8 +0,0 @@
using System.CommandLine;
namespace Wabbajack.CLI.Verbs;
public interface IVerb
{
public Command MakeCommand();
}

View File

@ -0,0 +1,44 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Wabbajack.Networking.Browser;
using Wabbajack.Paths;
using Wabbajack.RateLimiter;
namespace Wabbajack.CLI.Verbs;
public class ManualDownload
{
private readonly ILogger<ManualDownload> _logger;
private readonly Client _client;
private readonly IResource<HttpClient> _limiter;
public ManualDownload(ILogger<ManualDownload> logger, Client client, IResource<HttpClient> limiter)
{
_logger = logger;
_client = client;
_limiter = limiter;
}
public Command MakeCommand()
{
var command = new Command("manual-download");
command.Add(new Option<AbsolutePath>(new[] {"-p", "-prompt"}, "Text prompt to show the user"));
command.Add(new Option<AbsolutePath>(new[] {"-u", "-url"}, "Uri to show the user"));
command.Add(new Option<AbsolutePath>(new[] {"-o", "-outputPath"}, "Output Path for the downloaded file"));
command.Description = "Shows a browser and instructs the user to download a file, exist when the file is downloaded";
command.Handler = CommandHandler.Create(Run);
return command;
}
public async Task<int> Run(string prompt, Uri url, AbsolutePath outputPath, CancellationToken token)
{
_logger.LogInformation("Opening browser");
using var job = await _limiter.Begin($"Downloading {url}", 0, token);
await _client.ManualDownload(prompt, url, outputPath, token, job);
return 0;
}
}

View File

@ -7,7 +7,7 @@ using Wabbajack.Paths;
namespace Wabbajack.CLI.Verbs;
public class MirrorFile : IVerb
public class MirrorFile : AVerb
{
private readonly ILogger<MirrorFile> _logger;
private readonly Client _client;
@ -17,12 +17,11 @@ public class MirrorFile : IVerb
_logger = logger;
_client = wjClient;
}
public Command MakeCommand()
public static Command MakeCommand()
{
var command = new Command("mirror-file");
command.Add(new Option<AbsolutePath>(new[] {"-i", "-input"}, "File to Mirror"));
command.Description = "Mirrors a file to the Wabbajack CDN";
command.Handler = CommandHandler.Create(Run);
return command;
}
@ -34,5 +33,9 @@ public class MirrorFile : IVerb
return 0;
}
protected override ICommandHandler GetHandler()
{
return CommandHandler.Create(Run);
}
}

View File

@ -14,7 +14,7 @@ using Wabbajack.Paths;
namespace Wabbajack.CLI.Verbs;
public class SteamDownloadFile : IVerb
public class SteamDownloadFile : AVerb
{
private readonly ILogger<SteamDownloadFile> _logger;
private readonly Client _client;
@ -33,7 +33,7 @@ public class SteamDownloadFile : IVerb
_dtos = dtos;
_wjClient = wjClient;
}
public Command MakeCommand()
public static Command MakeCommand()
{
var command = new Command("steam-download-file");
command.Description = "Dumps information to the console about the given app";
@ -43,7 +43,6 @@ public class SteamDownloadFile : IVerb
command.Add(new Option<string>(new[] {"-v", "-version"}, "Version of the game to download for"));
command.Add(new Option<string>(new[] {"-f", "-file"}, "File to download (relative path)"));
command.Add(new Option<string>(new[] {"-o", "-output"}, "Output location"));
command.Handler = CommandHandler.Create(Run);
return command;
}
@ -93,4 +92,9 @@ public class SteamDownloadFile : IVerb
}
protected override ICommandHandler GetHandler()
{
return CommandHandler.Create(Run);
}
}

View File

@ -1,71 +0,0 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using SteamKit2;
using Wabbajack.DTOs;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Networking.Http.Interfaces;
using Wabbajack.Networking.Steam;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Wabbajack.CLI.Verbs;
public class SteamAppDumpInfo : IVerb
{
private readonly ILogger<SteamAppDumpInfo> _logger;
private readonly Client _client;
private readonly ITokenProvider<SteamLoginState> _token;
private readonly DepotDownloader _downloader;
private readonly DTOSerializer _dtos;
public SteamAppDumpInfo(ILogger<SteamAppDumpInfo> logger, Client steamClient, ITokenProvider<SteamLoginState> token,
DepotDownloader downloader, DTOSerializer dtos)
{
_logger = logger;
_client = steamClient;
_token = token;
_downloader = downloader;
_dtos = dtos;
}
public Command MakeCommand()
{
var command = new Command("steam-app-dump-info");
command.Description = "Dumps information to the console about the given app";
command.Add(new Option<string>(new[] {"-g", "-game", "-gameName"}, "Wabbajack game name"));
command.Handler = CommandHandler.Create(Run);
return command;
}
public async Task<int> Run(string gameName)
{
if (!GameRegistry.TryGetByFuzzyName(gameName, out var game))
{
_logger.LogError("Can't find game {GameName} in game registry", gameName);
return 1;
}
await _client.Login();
var appId = (uint) game.SteamIDs.First();
if (!await _downloader.AccountHasAccess(appId))
{
_logger.LogError("Your account does not have access to this Steam App");
return 1;
}
var appData = await _downloader.GetAppInfo((uint)game.SteamIDs.First());
Console.WriteLine("App Depots: ");
Console.WriteLine(_dtos.Serialize(appData, true));
return 0;
}
}

View File

@ -9,7 +9,7 @@ using Wabbajack.Paths;
namespace Wabbajack.CLI.Verbs;
public class SteamLogin : IVerb
public class SteamLogin : AVerb
{
private readonly ILogger<SteamLogin> _logger;
private readonly Client _client;
@ -21,13 +21,12 @@ public class SteamLogin : IVerb
_client = steamClient;
_token = token;
}
public Command MakeCommand()
public static Command MakeCommand()
{
var command = new Command("steam-login");
command.Description = "Logs into Steam via interactive prompts";
command.Add(new Option<string>(new[] {"-u", "-user"}, "Username for login"));
command.Handler = CommandHandler.Create(Run);
return command;
}
@ -54,5 +53,9 @@ public class SteamLogin : IVerb
return 0;
}
protected override ICommandHandler GetHandler()
{
return CommandHandler.Create(Run);
}
}

View File

@ -13,7 +13,7 @@ using Wabbajack.Paths.IO;
namespace Wabbajack.CLI.Verbs;
public class UploadToNexus : IVerb
public class UploadToNexus : AVerb
{
private readonly ILogger<UploadToNexus> _logger;
private readonly NexusApi _client;
@ -25,12 +25,11 @@ public class UploadToNexus : IVerb
_client = wjClient;
_dtos = dtos;
}
public Command MakeCommand()
public static Command MakeCommand()
{
var command = new Command("upload-to-nexus");
command.Add(new Option<AbsolutePath>(new[] {"-d", "-definition"}, "Definition JSON file"));
command.Description = "Uploads a file to the Nexus defined by the given .json definition file";
command.Handler = CommandHandler.Create(Run);
return command;
}
@ -43,5 +42,9 @@ public class UploadToNexus : IVerb
return 0;
}
protected override ICommandHandler GetHandler()
{
return CommandHandler.Create(Run);
}
}

View File

@ -7,22 +7,20 @@ using Wabbajack.VFS;
namespace Wabbajack.CLI.Verbs;
public class VFSIndexFolder : IVerb
public class VfsIndexFolder : AVerb
{
private readonly Context _context;
public VFSIndexFolder(Context context)
public VfsIndexFolder(Context context)
{
_context = context;
}
public Command MakeCommand()
public static Command MakeCommand()
{
var command = new Command("vfs-index");
command.Add(new Option<AbsolutePath>(new[] {"-f", "--folder"}, "Folder to index"));
command.Description = "Index and cache the contents of a folder";
command.Handler = CommandHandler.Create(Run);
return command;
}
@ -31,4 +29,9 @@ public class VFSIndexFolder : IVerb
await _context.AddRoot(folder, CancellationToken.None);
return 0;
}
protected override ICommandHandler GetHandler()
{
return CommandHandler.Create(Run);
}
}

View File

@ -35,7 +35,7 @@ using Wabbajack.Server.Lib.TokenProviders;
namespace Wabbajack.CLI.Verbs;
public class ValidateLists : IVerb
public class ValidateLists : AVerb
{
private static readonly Uri MirrorPrefix = new("https://mirror.wabbajack.org");
private readonly WriteOnlyClient _discord;
@ -70,7 +70,7 @@ public class ValidateLists : IVerb
_random = new Random();
}
public Command MakeCommand()
public static Command MakeCommand()
{
var command = new Command("validate-lists");
command.Add(new Option<List[]>(new[] {"-l", "-lists"}, "Lists of lists to validate") {IsRequired = true});
@ -84,7 +84,6 @@ public class ValidateLists : IVerb
{IsRequired = false});
command.Description = "Gets a list of modlists, validates them and exports a result list";
command.Handler = CommandHandler.Create(Run);
return command;
}
@ -514,4 +513,9 @@ public class ValidateLists : IVerb
await client.ConnectAsync(token);
return client;
}
protected override ICommandHandler GetHandler()
{
return CommandHandler.Create(Run);
}
}

View File

@ -24,6 +24,7 @@
<ItemGroup>
<ProjectReference Include="..\Wabbajack.Downloaders.Dispatcher\Wabbajack.Downloaders.Dispatcher.csproj" />
<ProjectReference Include="..\Wabbajack.Hashing.xxHash64\Wabbajack.Hashing.xxHash64.csproj" />
<ProjectReference Include="..\Wabbajack.Networking.Browser\Wabbajack.Networking.Browser.csproj" />
<ProjectReference Include="..\Wabbajack.Networking.Discord\Wabbajack.Networking.Discord.csproj" />
<ProjectReference Include="..\Wabbajack.Networking.GitHub\Wabbajack.Networking.GitHub.csproj" />
<ProjectReference Include="..\Wabbajack.Paths.IO\Wabbajack.Paths.IO.csproj" />

View File

@ -8,4 +8,5 @@ public class DownloadProgress : IMessage
public bool IsDone { get; set; }
public long BytesPerSecond { get; set; }
public long BytesCompleted { get; set; }
public long? ExpectedSize { get; set; }
}

View File

@ -1,5 +1,7 @@
using System;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
@ -10,6 +12,8 @@ using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using Splat;
using Wabbajack.DTOs.BrowserMessages;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Networking.Browser.ViewModels;
using Wabbajack.Networking.Browser.Views;
@ -52,6 +56,22 @@ namespace Wabbajack.Networking.Browser
}
base.OnFrameworkInitializationCompleted();
var dtos = Services.GetRequiredService<DTOSerializer>();
var msgProcessor = Task.Run(async () =>
{
await using var input = Console.OpenStandardInput();
while (true)
{
var msg = await JsonSerializer.DeserializeAsync<IMessage>(input);
switch (msg)
{
}
}
});
}
private void Startup(object sender, ControlledApplicationLifetimeStartupEventArgs e)

View File

@ -46,7 +46,6 @@ class CustomGlue : AvaloniaWebViewGlue
protected override void OnDownloadUpdated(CefBrowser browser, CefDownloadItem downloadItem, CefDownloadItemCallback callback)
{
downloadItem.
base.OnDownloadUpdated(browser, downloadItem, callback);
}
}

View File

@ -6,8 +6,6 @@ using Avalonia.Threading;
using CefNet;
using Microsoft.Extensions.DependencyInjection;
using Wabbajack.CLI.TypeConverters;
using Wabbajack.CLI.Verbs;
using Wabbajack.Networking.Browser.Verbs;
using Wabbajack.Networking.Browser.ViewModels;
using Wabbajack.Networking.Browser.Views;
using Wabbajack.Paths;
@ -33,11 +31,7 @@ public static class ServiceExtensions
var resources = KnownFolders.EntryPoint;
services.AddSingleton<MainWindow>();
services.AddSingleton<MainWindowViewModel>();
services.AddSingleton<IVerb, NexusLogin>();
services.AddSingleton<IVerb, LoverLabLogin>();
services.AddSingleton<IVerb, VectorPlexusLogin>();
services.AddSingleton<IVerb, ManualDownload>();
services.AddOSIntegrated();
services.AddSingleton(s => new CefSettings

View File

@ -1,136 +0,0 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using CefNet;
using Microsoft.Extensions.Logging;
using Wabbajack.CLI.Verbs;
using Wabbajack.DTOs.Logins;
using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.Networking.Browser.Verbs;
public abstract class AOAuthLoginVerb<TLoginType> : AVerb
where TLoginType : OAuth2LoginState, new()
{
private readonly EncryptedJsonTokenProvider<TLoginType> _tokenProvider;
private readonly HttpClient _httpClient;
private readonly string _namePrefix;
private readonly ILogger _logger;
public AOAuthLoginVerb(ILogger logger, string namePrefix, EncryptedJsonTokenProvider<TLoginType> tokenProvider, HttpClient client)
{
_logger = logger;
_tokenProvider = tokenProvider;
_httpClient = client;
_namePrefix = namePrefix;
}
public override Command MakeCommand()
{
var textInfo = new CultureInfo("en-US", false).TextInfo;
var command = new Command($"{_namePrefix}-login");
command.Description = $"Prompt the user to log into {textInfo.ToTitleCase(_namePrefix.Replace("-", " "))}";
command.Handler = CommandHandler.Create(Run);
return command;
}
private async Task Run(CancellationToken token)
{
var tlogin = new TLoginType();
await Browser.WaitForReady();
var handler = new AsyncSchemeHandler();
Browser.RequestContext.RegisterSchemeHandlerFactory("wabbajack", "", handler);
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 Browser.NavigateTo(new Uri(tlogin.AuthorizationEndpoint +
$"?response_type=code&client_id={tlogin.ClientID}&state={state}&scope={scopes}"));
var uri = await handler.Task.WaitAsync(token);
var cookies = await Browser.Cookies(tlogin.AuthorizationEndpoint.Host, token);
var parsed = HttpUtility.ParseQueryString(uri.Query);
if (parsed.Get("state") != state)
{
_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");
throw new Exception("Bad code result from OAuth");
}
var authCode = parsed.Get("code");
var formData = new KeyValuePair<string?, string?>[]
{
new("grant_type", "authorization_code"),
new("code", authCode),
new("client_id", tlogin.ClientID)
};
var msg = new HttpRequestMessage();
msg.Method = HttpMethod.Post;
msg.RequestUri = tlogin.TokenEndpoint;
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(";", cookies.Select(c => $"{c.Name}={c.Value}")));
msg.Content = new FormUrlEncodedContent(formData.ToList());
using var response = await _httpClient.SendAsync(msg, token);
var data = await response.Content.ReadFromJsonAsync<OAuthResultState>(cancellationToken: token);
await _tokenProvider.SetToken(new TLoginType
{
Cookies = cookies,
ResultState = data!
});
}
private class AsyncSchemeHandler : CefSchemeHandlerFactory
{
private readonly TaskCompletionSource<Uri> _tcs = new();
public Task<Uri> Task => _tcs.Task;
protected override CefResourceHandler Create(CefBrowser browser, CefFrame frame, string schemeName,
CefRequest request)
{
return new Handler(_tcs);
}
}
private class Handler : CefResourceHandler
{
private readonly TaskCompletionSource<Uri> _tcs;
public Handler(TaskCompletionSource<Uri> tcs)
{
_tcs = tcs;
}
protected override bool ProcessRequest(CefRequest request, CefCallback callback)
{
_tcs.TrySetResult(new Uri(request.Url));
return false;
}
}
}

View File

@ -1,26 +0,0 @@
using System.CommandLine;
using Avalonia.Threading;
using CefNet.Avalonia;
using ReactiveUI;
using Wabbajack.Networking.Browser;
namespace Wabbajack.CLI.Verbs;
public abstract class AVerb : IVerb
{
public abstract Command MakeCommand();
public string Instructions
{
set
{
Dispatcher.UIThread.Post(() =>
{
Program.MainWindowVM.Instructions = value;
});
}
}
public WebView Browser => Program.MainWindow.Browser;
}

View File

@ -1,8 +0,0 @@
using System.CommandLine;
namespace Wabbajack.CLI.Verbs;
public interface IVerb
{
public Command MakeCommand();
}

View File

@ -1,15 +0,0 @@
using System.Net.Http;
using JetBrains.Annotations;
using Microsoft.Extensions.Logging;
using Wabbajack.DTOs.Logins;
using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.Networking.Browser.Verbs;
public class LoverLabLogin : AOAuthLoginVerb<LoversLabLoginState>
{
public LoverLabLogin(ILogger<LoverLabLogin> logger, EncryptedJsonTokenProvider<LoversLabLoginState> tokenProvider, HttpClient client) :
base(logger, "vector-plexus", tokenProvider, client)
{
}
}

View File

@ -1,42 +0,0 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Wabbajack.CLI.Verbs;
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.Networking.Browser.Verbs;
public class ManualDownload : AVerb
{
private readonly ILogger<ManualDownload> _logger;
public ManualDownload(ILogger<ManualDownload> logger)
{
_logger = logger;
}
public override Command MakeCommand()
{
var command = new Command("manual-download");
command.Description = "Prompt the user to download a file";
command.Add(new Option<Uri>(new[] {"-u", "-url"}, "Uri"));
command.Add(new Option<AbsolutePath>);
command.Handler = CommandHandler.Create(Run);
return command;
}
public async Task<int> Run(Uri url)
{
await Browser.WaitForReady();
await Browser.NavigateTo(url);
await Task.Delay(100000);
return 0;
}
}

View File

@ -1,108 +0,0 @@
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Fizzler.Systems.HtmlAgilityPack;
using ReactiveUI;
using Wabbajack.CLI.Verbs;
using Wabbajack.DTOs.Logins;
using Wabbajack.Networking.Http.Interfaces;
namespace Wabbajack.Networking.Browser.Verbs;
public class NexusLogin : AVerb
{
private readonly ITokenProvider<NexusApiState> _tokenProvider;
public NexusLogin(ITokenProvider<NexusApiState> tokenProvider)
{
_tokenProvider = tokenProvider;
}
public override Command MakeCommand()
{
var command = new Command("nexus-login");
command.Description = "Prompt the user to log into the nexus";
command.Handler = CommandHandler.Create(Run);
return command;
}
private async Task Run(CancellationToken token)
{
token.ThrowIfCancellationRequested();
Instructions = "Please log into the Nexus";
await Browser.WaitForReady();
await Browser.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 Browser.Cookies("nexusmods.com", token);
if (cookies.Any(c => c.Name == "member_id"))
break;
token.ThrowIfCancellationRequested();
await Task.Delay(500, token);
}
Instructions = "Getting API Key...";
await Browser.NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api"));
await Browser.NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api"));
var key = "";
while (true)
{
try
{
key = (await Browser.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;
try
{
await Browser.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
}
token.ThrowIfCancellationRequested();
}
Instructions = "Success, saving information...";
await _tokenProvider.SetToken(new NexusApiState
{
Cookies = cookies,
ApiKey = key
});
}
}

View File

@ -1,14 +0,0 @@
using System.Net.Http;
using Microsoft.Extensions.Logging;
using Wabbajack.DTOs.Logins;
using Wabbajack.Services.OSIntegrated;
namespace Wabbajack.Networking.Browser.Verbs;
public class VectorPlexusLogin : AOAuthLoginVerb<VectorPlexusLoginState>
{
public VectorPlexusLogin(ILogger<LoverLabLogin> logger, EncryptedJsonTokenProvider<VectorPlexusLoginState> tokenProvider, HttpClient client) :
base(logger, "lovers-lab", tokenProvider, client)
{
}
}

View File

@ -1,44 +1,16 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.Reactive.Disposables;
using System.Text;
using System.Threading.Tasks;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.CLI.Verbs;
using Wabbajack.Common;
using ReactiveUI.Fody.Helpers;
namespace Wabbajack.Networking.Browser.ViewModels
{
public class MainWindowViewModel : ViewModelBase
{
private readonly IEnumerable<IVerb> _verbs;
public MainWindowViewModel(IEnumerable<IVerb> verbs)
public MainWindowViewModel()
{
_verbs = verbs;
this.WhenActivated(disposables =>
{
ExecuteCommand().FireAndForget();
Disposable.Empty.DisposeWith(disposables);
});
}
[Reactive]
public string Instructions { get; set; }
private async Task ExecuteCommand()
{
while (Program.MainWindow.Browser == null)
await Task.Delay(250);
var root = new RootCommand();
foreach (var verb in _verbs)
root.Add(verb.MakeCommand());
var code = await root.InvokeAsync(Program.Args);
Environment.Exit(code);
}
}
}

View File

@ -1,16 +1,8 @@
using System.Collections.Generic;
using System.CommandLine;
using System.Reactive.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Mixins;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using CefNet.Avalonia;
using ReactiveUI;
using Wabbajack.CLI.Verbs;
using Wabbajack.Common;
using Wabbajack.Networking.Browser.ViewModels;
namespace Wabbajack.Networking.Browser.Views

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>10</LangVersion>

View File

@ -0,0 +1,65 @@
using System.Diagnostics;
using System.Text.Json;
using Wabbajack.DTOs.BrowserMessages;
using Wabbajack.DTOs.JsonConverters;
using Wabbajack.Paths;
using Wabbajack.RateLimiter;
namespace Wabbajack.Networking.Browser;
public class Client
{
private readonly Configuration _config;
private readonly DTOSerializer _dtos;
public Client(Configuration config, DTOSerializer dtos)
{
_config = config;
_dtos = dtos;
}
public async Task ManualDownload(string prompt, Uri uri, AbsolutePath dest, CancellationToken token, IJob job)
{
var process = new Process()
{
StartInfo = new ProcessStartInfo()
{
FileName = _config.HostExecutable.ToString(),
RedirectStandardError = true,
RedirectStandardInput = true,
RedirectStandardOutput = true
}
};
var ptask = process.Start();
var reader = Task.Run(async () =>
{
while (!token.IsCancellationRequested)
{
var msg = await JsonSerializer.DeserializeAsync<IMessage>(process.StandardOutput.BaseStream,
_dtos.Options, token);
if (msg is DownloadProgress dp)
{
job.ReportNoWait((int) dp.BytesCompleted);
job.Size = dp.ExpectedSize;
if (dp.IsDone)
{
return;
}
}
}
}, token);
await process.StandardInput.WriteAsync(JsonSerializer.Serialize(new DTOs.BrowserMessages.ManualDownload()
{
Prompt = prompt,
Url = uri,
Path = dest
}));
await process.WaitForExitAsync(token);
}
}

View File

@ -1,7 +1,7 @@
using Wabbajack.Paths;
using Wabbajack.Paths.IO;
namespace Wabbajack.Networking.Browser.Client;
namespace Wabbajack.Networking.Browser;
public class Configuration
{

View File

@ -10,4 +10,5 @@ public interface IJob
public long Current { get; }
public string Description { get; }
public ValueTask Report(int processedSize, CancellationToken token);
public void ReportNoWait(int processedSize);
}