Remove unneeded code, port status messages

This commit is contained in:
Timothy Baldridge 2021-12-26 15:05:28 -07:00
parent 31f32ee2f5
commit b53f98eebd
28 changed files with 299 additions and 742 deletions

View File

@ -14,10 +14,10 @@ using System.Reactive.Subjects;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib;
using Wabbajack.Lib.AuthorApi;
using Wabbajack.Lib.FileUploader;
using Wabbajack.Lib.Interventions;
namespace Wabbajack
{

View File

@ -12,6 +12,7 @@ using DynamicData;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.AuthorApi;
using Wabbajack.Lib.Extensions;
using Wabbajack.Lib.FileUploader;
using Wabbajack.Lib.GitHub;
using WebSocketSharp;

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.Interventions;
namespace Wabbajack
{

View File

@ -16,13 +16,13 @@ using ReactiveUI.Fody.Helpers;
using System.Windows.Media;
using DynamicData;
using DynamicData.Binding;
using Wabbajack.Common.StatusFeed;
using System.Reactive;
using System.Collections.Generic;
using System.Reactive.Subjects;
using System.Windows.Input;
using Microsoft.WindowsAPICodePack.Dialogs;
using Wabbajack.Common.IO;
using Wabbajack.Lib.Interventions;
namespace Wabbajack
{

View File

@ -10,8 +10,8 @@ using System.Threading.Tasks;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib;
using Wabbajack.Lib.Interventions;
using Wabbajack.Util;
namespace Wabbajack

View File

@ -16,8 +16,8 @@ using System.Windows.Input;
using System.Windows.Threading;
using Alphaleonis.Win32.Filesystem;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib;
using Wabbajack.Lib.Interventions;
namespace Wabbajack
{

View File

@ -5,6 +5,7 @@ using System.Text;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Lib;
using Wabbajack.Lib.Interventions;
namespace Wabbajack
{

View File

@ -11,9 +11,9 @@ using System.Windows.Threading;
using CefSharp;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib;
using Wabbajack.Lib.Downloaders;
using Wabbajack.Lib.Interventions;
using Wabbajack.Lib.LibCefHelpers;
using Wabbajack.Lib.NexusApi;
using Wabbajack.Lib.WebAutomation;

View File

@ -10,6 +10,7 @@ using DynamicData;
using DynamicData.Kernel;
using ReactiveUI;
using Wabbajack.Lib;
using Wabbajack.Lib.Extensions;
namespace Wabbajack
{

View File

@ -0,0 +1,234 @@
using System;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading.Tasks;
namespace Wabbajack.Lib.Extensions
{
public static class RxExt
{
/// <summary>
/// Convenience function that discards events that are null
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <returns>Source events that are not null</returns>
public static IObservable<T> NotNull<T>(this IObservable<T?> source)
where T : class
{
return source
.Where(u => u != null)
.Select(u => u!);
}
/// <summary>
/// Converts any observable to type Unit. Useful for when you care that a signal occurred,
/// but don't care about what its value is downstream.
/// </summary>
/// <returns>An observable that returns Unit anytime the source signal fires an event.</returns>
public static IObservable<Unit> Unit<T>(this IObservable<T> source)
{
return source.Select(_ => System.Reactive.Unit.Default);
}
/// <summary>
/// Convenience operator to subscribe to the source observable, only when a second "switch" observable is on.
/// When the switch is on, the source will be subscribed to, and its updates passed through.
/// When the switch is off, the subscription to the source observable will be stopped, and no signal will be published.
/// </summary>
/// <param name="source">Source observable to subscribe to if on</param>
/// <param name="filterSwitch">On/Off signal of whether to subscribe to source observable</param>
/// <returns>Observable that publishes data from source, if the switch is on.</returns>
public static IObservable<T> FlowSwitch<T>(this IObservable<T> source, IObservable<bool> filterSwitch)
{
return filterSwitch
.DistinctUntilChanged()
.Select(on =>
{
if (on)
{
return source;
}
else
{
return Observable.Empty<T>();
}
})
.Switch();
}
/// <summary>
/// Convenience operator to subscribe to the source observable, only when a second "switch" observable is on.
/// When the switch is on, the source will be subscribed to, and its updates passed through.
/// When the switch is off, the subscription to the source observable will be stopped, and no signal will be published.
/// </summary>
public static IObservable<T> FlowSwitch<T>(this IObservable<T> source, IObservable<bool> filterSwitch, T valueWhenOff)
{
return filterSwitch
.DistinctUntilChanged()
.Select(on =>
{
if (on)
{
return source;
}
else
{
return Observable.Return<T>(valueWhenOff);
}
})
.Switch();
}
/// Inspiration:
/// http://reactivex.io/documentation/operators/debounce.html
/// https://stackoverflow.com/questions/20034476/how-can-i-use-reactive-extensions-to-throttle-events-using-a-max-window-size
public static IObservable<T> Debounce<T>(this IObservable<T> source, TimeSpan interval, IScheduler? scheduler = null)
{
scheduler ??= Scheduler.Default;
return Observable.Create<T>(o =>
{
var hasValue = false;
bool throttling = false;
T? value = default;
var dueTimeDisposable = new SerialDisposable();
void internalCallback()
{
if (hasValue)
{
// We have another value that came in to fire.
// Reregister for callback
dueTimeDisposable.Disposable = scheduler!.Schedule(interval, internalCallback);
o.OnNext(value!);
value = default;
hasValue = false;
}
else
{
// Nothing to do, throttle is complete.
throttling = false;
}
}
return source.Subscribe(
onNext: (x) =>
{
if (!throttling)
{
// Fire initial value
o.OnNext(x);
// Mark that we're throttling
throttling = true;
// Register for callback when throttle is complete
dueTimeDisposable.Disposable = scheduler.Schedule(interval, internalCallback);
}
else
{
// In the middle of throttle
// Save value and return
hasValue = true;
value = x;
}
},
onError: o.OnError,
onCompleted: o.OnCompleted);
});
}
public static IObservable<Unit> SelectTask<T>(this IObservable<T> source, Func<T, Task> task)
{
return source
.SelectMany(async i =>
{
await task(i).ConfigureAwait(false);
return System.Reactive.Unit.Default;
});
}
public static IObservable<Unit> SelectTask<T>(this IObservable<T> source, Func<Task> task)
{
return source
.SelectMany(async _ =>
{
await task().ConfigureAwait(false);
return System.Reactive.Unit.Default;
});
}
public static IObservable<R> SelectTask<T, R>(this IObservable<T> source, Func<Task<R>> task)
{
return source
.SelectMany(_ => task());
}
public static IObservable<R> SelectTask<T, R>(this IObservable<T> source, Func<T, Task<R>> task)
{
return source
.SelectMany(x => task(x));
}
public static IObservable<T> DoTask<T>(this IObservable<T> source, Func<T, Task> task)
{
return source
.SelectMany(async (x) =>
{
await task(x).ConfigureAwait(false);
return x;
});
}
public static IObservable<R> WhereCastable<T, R>(this IObservable<T> source)
where R : class
where T : class
{
return source
.Select(x => x as R)
.NotNull();
}
public static IObservable<bool> Invert(this IObservable<bool> source)
{
return source.Select(x => !x);
}
public static IObservable<(T Previous, T Current)> Pairwise<T>(this IObservable<T> source)
{
T? prevStorage = default;
return source.Select(i =>
{
var prev = prevStorage;
prevStorage = i;
return (prev, i);
})!;
}
public static IObservable<T> DelayInitial<T>(this IObservable<T> source, TimeSpan delay, IScheduler scheduler)
{
return source.FlowSwitch(
Observable.Return(System.Reactive.Unit.Default)
.Delay(delay, scheduler)
.Select(_ => true)
.StartWith(false));
}
public static IObservable<T?> DisposeOld<T>(this IObservable<T?> source)
where T : class, IDisposable
{
return source
.StartWith(default(T))
.Pairwise()
.Do(x =>
{
if (x.Previous != null)
{
x.Previous.Dispose();
}
})
.Select(x => x.Current);
}
}
}

View File

@ -0,0 +1,12 @@
using System;
namespace Wabbajack.Lib.Interventions
{
public abstract class AErrorMessage : Exception, IException
{
public DateTime Timestamp { get; } = DateTime.Now;
public abstract string ShortDescription { get; }
public abstract string ExtendedDescription { get; }
Exception IException.Exception => this;
}
}

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using System.Windows.Input;
using ReactiveUI;
using Wabbajack.Common;
using Wabbajack.Lib.Interventions;
namespace Wabbajack.Lib
{
@ -17,9 +18,6 @@ namespace Wabbajack.Lib
private bool _handled;
public bool Handled { get => _handled; set => this.RaiseAndSetIfChanged(ref _handled, value); }
public int CpuID { get; } = WorkQueue.AsyncLocalQueue?.CpuId ?? WorkQueue.UnassignedCpuId;
public abstract void Cancel();
public ICommand CancelCommand { get; }

View File

@ -0,0 +1,6 @@
namespace Wabbajack.Lib.Interventions
{
public interface IError : IStatusMessage
{
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Wabbajack.Lib.Interventions
{
public interface IException : IError
{
Exception Exception { get; }
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace Wabbajack.Lib.Interventions
{
public interface IStatusMessage
{
DateTime Timestamp { get; }
string ShortDescription { get; }
string ExtendedDescription { get; }
}
}

View File

@ -1,18 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ReactiveUI;
using Wabbajack.Common.StatusFeed;
using ReactiveUI;
namespace Wabbajack.Lib
namespace Wabbajack.Lib.Interventions
{
/// <summary>
/// Defines a message that requires user interaction. The user must perform some action
/// or make a choice.
/// </summary>
public interface IUserIntervention : IStatusMessage, IReactiveObject
public interface IUserIntervention : IReactiveObject
{
/// <summary>
/// The user didn't make a choice, so this action should be aborted
@ -25,9 +19,5 @@ namespace Wabbajack.Lib
/// </summary>
bool Handled { get; }
/// <summary>
/// WorkQueue job ID that is blocking on this intervention
/// </summary>
int CpuID { get; }
}
}

View File

@ -1,71 +0,0 @@
using System;
namespace Wabbajack.Lib.NexusApi
{
public class UserStatus
{
public string email = string.Empty;
public bool is_premium;
public bool is_supporter;
public string key = string.Empty;
public string name = string.Empty;
public string profile_url = string.Empty;
public string user_id = string.Empty;
}
public class NexusFileInfo
{
public long category_id { get; set; }
public string? category_name { get; set; } = null;
public string changelog_html { get; set; } = string.Empty;
public string description { get; set; } = string.Empty;
public string external_virus_scan_url { get; set; } = string.Empty;
public long file_id { get; set; }
public string file_name { get; set; } = string.Empty;
public bool is_primary { get; set; }
public string mod_version { get; set; } = string.Empty;
public string name { get; set; } = string.Empty;
public long size { get; set; }
public long size_kb { get; set; }
public DateTime uploaded_time { get; set; }
public long uploaded_timestamp { get; set; }
public string version { get; set; } = string.Empty;
}
public class ModInfo
{
public string name { get; set; } = string.Empty;
public string summary { get; set; } = string.Empty;
public string description { get; set; } = string.Empty;
public Uri? picture_url { get; set; }
public string mod_id { get; set; } = string.Empty;
public long game_id { get; set; }
public bool allow_rating { get; set; }
public string domain_name { get; set; } = string.Empty;
public long category_id { get; set; }
public string version { get; set; } = string.Empty;
public long endorsement_count { get; set; }
public long created_timestamp { get; set; }
public DateTime created_time { get; set; }
public long updated_timestamp { get; set; }
public DateTime updated_time { get; set; }
public string author { get; set; } = string.Empty;
public string uploaded_by { get; set; } = string.Empty;
public Uri? uploaded_users_profile_url { get; set; }
public bool contains_adult_content { get; set; }
public string status { get; set; } = string.Empty;
public bool available { get; set; } = true;
}
public class MD5Response
{
public ModInfo? mod;
public NexusFileInfo? file_details;
}
public class EndorsementResponse
{
public string message = string.Empty;
public string status = string.Empty;
}
}

View File

@ -1,68 +0,0 @@
using System;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using HtmlAgilityPack;
using Wabbajack.Common;
using Wabbajack.Lib.LibCefHelpers;
using Wabbajack.Lib.WebAutomation;
namespace Wabbajack.Lib.NexusApi
{
public class HTMLInterface
{
public static async Task<PermissionValue> GetUploadPermissions(Game game, long modId)
{
HtmlDocument response;
using (var driver = await Driver.Create())
{
await driver.NavigateTo(new Uri($"https://nexusmods.com/{game.MetaData().NexusName}/mods/{modId}"));
TOP:
response = await driver.GetHtmlAsync();
if (response!.Text!.Contains("This process is automatic. Your browser will redirect to your requested content shortly."))
{
await Task.Delay(5000);
goto TOP;
}
}
var hidden = response.DocumentNode
.Descendants()
.Any(n => n.Id == $"{modId}-title" && n.InnerText == "Hidden mod");
if (hidden) return PermissionValue.Hidden;
var perm = response.DocumentNode
.Descendants()
.Where(d => d.HasClass("permissions-title") && d.InnerHtml == "Upload permission")
.SelectMany(d => d.ParentNode.ParentNode.GetClasses())
.FirstOrDefault(perm => perm.StartsWith("permission-"));
var not_found = response.DocumentNode.Descendants()
.Where(d => d.Id == $"{modId}-title")
.Select(d => d.InnerText)
.FirstOrDefault() == "Not found";
if (not_found) return PermissionValue.NotFound;
return perm switch
{
"permission-no" => PermissionValue.No,
"permission-maybe" => PermissionValue.Maybe,
"permission-yes" => PermissionValue.Yes,
_ => PermissionValue.No
};
}
public enum PermissionValue : int
{
No = 0,
Yes = 1,
Maybe = 2,
Hidden = 3,
NotFound = 4
}
}
}

View File

@ -1,20 +0,0 @@
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders;
namespace Wabbajack.Lib.NexusApi
{
public interface INexusApi
{
public Task<string> GetNexusDownloadLink(NexusDownloader.State archive);
public Task<NexusApiClient.GetModFilesResponse> GetModFiles(Game game, long modid, bool useCache = true);
public Task<NexusFileInfo> GetModFile(Game game, long modid, long fileId, bool useCache = true);
public Task<ModInfo> GetModInfo(Game game, long modId, bool useCache = true);
public Task<UserStatus> GetUserStatus();
public Task<bool> IsPremium();
public bool IsAuthenticated { get; }
public int RemainingAPICalls { get; }
}
}

View File

@ -1,400 +0,0 @@
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders;
using System.Threading;
using Wabbajack.Common.Exceptions;
using Wabbajack.Lib.LibCefHelpers;
using Wabbajack.Lib.WebAutomation;
namespace Wabbajack.Lib.NexusApi
{
public class NexusApiClient : ViewModel, INexusApi
{
private static readonly string API_KEY_CACHE_FILE = "nexus.key_cache";
/// <summary>
/// Forces the client to do manual downloading via CEF (for testing)
/// </summary>
private static bool ManualTestingMode = false;
public Http.Client HttpClient { get; } = new();
#region Authentication
public static string? ApiKey { get; set; }
public bool IsAuthenticated => ApiKey != null;
public int RemainingAPICalls => Math.Max(HourlyRemaining, DailyRemaining);
private Task<UserStatus>? _userStatus;
public Task<UserStatus> UserStatus
{
get
{
return _userStatus ??= GetUserStatus();
}
}
public async Task<bool> IsPremium()
{
return IsAuthenticated && (await UserStatus).is_premium;
}
public async Task<string> Username() => (await UserStatus).name;
private static AsyncLock _getAPIKeyLock = new AsyncLock();
private static async Task<string> GetApiKey()
{
using (await _getAPIKeyLock.WaitAsync())
{
// Clean up old location
if (File.Exists(API_KEY_CACHE_FILE))
{
File.Delete(API_KEY_CACHE_FILE);
}
try
{
return await Utils.FromEncryptedJson<string>("nexusapikey");
}
catch (Exception)
{
}
var env_key = Environment.GetEnvironmentVariable("NEXUSAPIKEY");
if (env_key != null)
{
return env_key;
}
return await RequestAndCacheAPIKey();
}
}
public static async Task<string> RequestAndCacheAPIKey()
{
var result = await Utils.Log(new RequestNexusAuthorization()).Task;
await result.ToEcryptedJson("nexusapikey");
return result;
}
public static async Task<string> SetupNexusLogin(IWebDriver browser, Action<string> updateStatus, CancellationToken cancel)
{
updateStatus("Please log into the Nexus");
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"));
Helpers.Cookie[] cookies = {};
while (true)
{
cookies = await browser.GetCookies("nexusmods.com");
if (cookies.Any(c => c.Name == "member_id"))
break;
cancel.ThrowIfCancellationRequested();
await Task.Delay(500, cancel);
}
await browser.NavigateTo(new Uri("https://www.nexusmods.com/users/myaccount?tab=api"));
updateStatus("Saving login info");
await cookies.ToEcryptedJson("nexus-cookies");
updateStatus("Looking for API Key");
var apiKey = new TaskCompletionSource<string>();
while (true)
{
var key = "";
try
{
key = await browser.EvaluateJavaScript(
"document.querySelector(\"input[value=wabbajack]\").parentElement.parentElement.querySelector(\"textarea.application-key\").innerHTML");
}
catch (Exception)
{
// ignored
}
if (!string.IsNullOrEmpty(key))
{
return key;
}
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;"
);
updateStatus("Generating API Key, Please Wait...");
}
catch (Exception)
{
// ignored
}
cancel.ThrowIfCancellationRequested();
await Task.Delay(500);
}
}
public async Task<UserStatus> GetUserStatus()
{
var url = "https://api.nexusmods.com/v1/users/validate.json";
var result = await Get<UserStatus>(url);
Utils.Log($"Logged into the nexus as {result.name}");
Utils.Log($"Nexus calls remaining: {DailyRemaining} daily, {HourlyRemaining} hourly");
return result;
}
public async Task<(int, int)> GetRemainingApiCalls()
{
var url = "https://api.nexusmods.com/v1/users/validate.json";
using var response = await HttpClient.GetAsync(url);
var result = (int.Parse(response.Headers.GetValues("X-RL-Daily-Remaining").First()),
int.Parse(response.Headers.GetValues("X-RL-Hourly-Remaining").First()));
_dailyRemaining = result.Item1;
_hourlyRemaining = result.Item2;
return result;
}
#endregion
#region Rate Tracking
private readonly object RemainingLock = new object();
private int _dailyRemaining;
public int DailyRemaining
{
get
{
lock (RemainingLock)
{
return _dailyRemaining;
}
}
protected set
{
lock (RemainingLock)
{
_dailyRemaining = value;
}
}
}
private int _hourlyRemaining;
public int HourlyRemaining
{
get
{
lock (RemainingLock)
{
return _hourlyRemaining;
}
}
protected set
{
lock (RemainingLock)
{
_hourlyRemaining = value;
}
}
}
protected virtual async Task UpdateRemaining(HttpResponseMessage response)
{
try
{
_dailyRemaining = int.Parse(response.Headers.GetValues("x-rl-daily-remaining").First());
_hourlyRemaining = int.Parse(response.Headers.GetValues("x-rl-hourly-remaining").First());
this.RaisePropertyChanged(nameof(DailyRemaining));
this.RaisePropertyChanged(nameof(HourlyRemaining));
}
catch (Exception)
{
}
}
#endregion
protected NexusApiClient(string? apiKey = null)
{
ApiKey = apiKey;
// set default headers for all requests to the Nexus API
var headers = HttpClient.Headers;
headers.Add(("User-Agent", Consts.UserAgent));
headers.Add(("apikey", ApiKey));
headers.Add(("Accept", "application/json"));
headers.Add(("Application-Name", Consts.AppName));
headers.Add(("Application-Version", $"{Assembly.GetEntryAssembly()?.GetName()?.Version ?? new Version(0, 1)}"));
}
public static async Task<NexusApiClient> Get(string? apiKey = null)
{
apiKey ??= await GetApiKey();
return new NexusApiClient(apiKey);
}
public async Task<T> Get<T>(string url, Http.Client? client = null)
{
client ??= HttpClient;
int retries = 0;
TOP:
try
{
using var response = await client.GetAsync(url);
await UpdateRemaining(response);
if (!response.IsSuccessStatusCode)
{
Utils.Log($"Nexus call failed: {response.RequestMessage!.RequestUri}");
throw new HttpException(response);
}
await using var stream = await response.Content.ReadAsStreamAsync();
return stream.FromJson<T>(genericReader:true);
}
catch (TimeoutException)
{
if (retries == Consts.MaxHTTPRetries)
throw;
Utils.Log($"Nexus call to {url} failed, retrying {retries} of {Consts.MaxHTTPRetries}");
retries++;
goto TOP;
}
catch (Exception e)
{
Utils.Log($"Nexus call failed `{url}`: " + e);
throw;
}
}
private async Task<T> GetCached<T>(string url)
{
if (BuildServerStatus.IsBuildServerDown)
return await Get<T>(url);
var builder = new UriBuilder(url)
{
Host = Consts.WabbajackBuildServerUri.Host,
Scheme = Consts.WabbajackBuildServerUri.Scheme,
Port = Consts.WabbajackBuildServerUri.Port
};
return await Get<T>(builder.ToString(), HttpClient.WithHeader((Consts.MetricsKeyHeader, await Metrics.GetMetricsKey())));
}
private static AsyncLock ManualDownloadLock = new();
public async Task<string> GetNexusDownloadLink(NexusDownloader.State archive)
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
var fileInfo = await GetModFile(archive.Game, archive.ModID, archive.FileID);
if (fileInfo.category_name == null)
throw new Exception("Mod unavailable");
if (await IsPremium() && !ManualTestingMode)
{
if (HourlyRemaining <= 0 && DailyRemaining <= 0)
{
throw new NexusAPIQuotaExceeded();
}
var url =
$"https://api.nexusmods.com/v1/games/{archive.Game.MetaData().NexusName}/mods/{archive.ModID}/files/{archive.FileID}/download_link.json";
return (await Get<List<DownloadLink>>(url)).First().URI;
}
try
{
using var _ = await ManualDownloadLock.WaitAsync();
await Task.Delay(1000);
Utils.Log($"Requesting manual download for {archive.Name} {archive.PrimaryKeyString}");
return (await Utils.Log(await ManuallyDownloadNexusFile.Create(archive)).Task).ToString();
}
catch (TaskCanceledException ex)
{
Utils.Error(ex, "Manual cancellation of download");
throw;
}
}
public class GetModFilesResponse
{
public List<NexusFileInfo> files { get; set; } = new List<NexusFileInfo>();
}
public async Task<GetModFilesResponse> GetModFiles(Game game, long modid, bool useCache = true)
{
var url = $"https://api.nexusmods.com/v1/games/{game.MetaData().NexusName}/mods/{modid}/files.json";
var result = useCache ? await GetCached<GetModFilesResponse>(url) : await Get<GetModFilesResponse>(url);
if (result.files == null)
throw new InvalidOperationException("Got Null data from the Nexus while finding mod files");
return result;
}
public async Task<NexusFileInfo> GetModFile(Game game, long modId, long fileId, bool useCache = true)
{
var url = $"https://api.nexusmods.com/v1/games/{game.MetaData().NexusName}/mods/{modId}/files/{fileId}.json";
var result = useCache ? await GetCached<NexusFileInfo>(url) : await Get<NexusFileInfo>(url);
return result;
}
public async Task<List<MD5Response>> GetModInfoFromMD5(Game game, string md5Hash)
{
var url = $"https://api.nexusmods.com/v1/games/{game.MetaData().NexusName}/mods/md5_search/{md5Hash}.json";
return await Get<List<MD5Response>>(url);
}
public async Task<ModInfo> GetModInfo(Game game, long modId, bool useCache = true)
{
var url = $"https://api.nexusmods.com/v1/games/{game.MetaData().NexusName}/mods/{modId}.json";
if (useCache)
{
try
{
return await GetCached<ModInfo>(url);
}
catch (HttpException)
{
return await Get<ModInfo>(url);
}
}
return await Get<ModInfo>(url);
}
private class DownloadLink
{
public string URI { get; set; } = string.Empty;
}
public static Uri ManualDownloadUrl(NexusDownloader.State state)
{
return new Uri($"https://www.nexusmods.com/{state.Game.MetaData().NexusName}/mods/{state.ModID}?tab=files");
}
}
}

View File

@ -1,30 +0,0 @@
using System.Text.RegularExpressions;
using Wabbajack.Common;
namespace Wabbajack.Lib.NexusApi
{
public sealed class NexusApiUtils
{
public static string ConvertGameName(string gameName)
{
if (Regex.IsMatch(gameName, @"^[^a-z\s]+\.[^a-z\s]+$"))
return gameName;
return GameRegistry.GetByMO2ArchiveName(gameName)?.NexusName ?? gameName.ToLower();
}
public static string GetModURL(Game game, string argModId)
{
return $"https://nexusmods.com/{game.MetaData().NexusName}/mods/{argModId}";
}
public static string FixupSummary(string? argSummary)
{
if (argSummary == null)
return "";
return argSummary.Replace("&#39;", "'")
.Replace("<br/>", "\n\n")
.Replace("<br />", "\n\n")
.Replace("&#33;", "!");
}
}
}

View File

@ -1,80 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.ServiceModel.Syndication;
using System.Threading.Tasks;
using System.Xml;
using Wabbajack.Common;
namespace Wabbajack.Lib.NexusApi
{
public class NexusUpdatesFeeds
{
public static async Task<List<UpdateRecord>> GetUpdates()
{
var updated = GetFeed(new Uri("https://www.nexusmods.com/rss/updatedtoday"));
var newToday = GetFeed(new Uri("https://www.nexusmods.com/rss/newtoday"));
var sorted = (await updated).Concat(await newToday).OrderByDescending(f => f.TimeStamp);
var deduped = sorted.GroupBy(g => (g.Game, g.ModId)).Select(g => g.First()).ToList();
return deduped;
}
private static bool TryParseGameUrl(SyndicationLink link, out Game game, out long modId)
{
var parts = link.Uri.AbsolutePath.Split('/', StringSplitOptions.RemoveEmptyEntries);
if (!GameRegistry.TryGetByFuzzyName(parts[0], out var foundGame))
{
game = Game.Oblivion;
modId = 0;
return false;
}
if (long.TryParse(parts[2], out modId))
{
game = foundGame.Game;
return true;
}
game = Game.Oblivion;
modId = 0;
return false;
}
private static async Task<IEnumerable<UpdateRecord>> GetFeed(Uri uri)
{
var client = new Wabbajack.Lib.Http.Client();
var data = await client.GetStringAsync(uri);
var reader = XmlReader.Create(new StringReader(data));
var results = SyndicationFeed.Load(reader);
return results.Items
.Select(itm =>
{
if (TryParseGameUrl(itm.Links.First(), out var game, out var modId))
{
return new UpdateRecord
{
TimeStamp = itm.PublishDate.UtcDateTime,
Game = game,
ModId = modId
};
}
return null;
})
.NotNull();
}
public class UpdateRecord
{
public Game Game { get; set; }
public long ModId { get; set; }
public DateTime TimeStamp { get; set; }
}
}
}

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wabbajack.Common;
namespace Wabbajack.Lib.NexusApi
{
public class RequestNexusAuthorization : AUserIntervention
{
public override string ShortDescription => "Getting User's Nexus API Key";
public override string ExtendedDescription { get; } = string.Empty;
private readonly TaskCompletionSource<string> _source = new TaskCompletionSource<string>();
public Task<string> Task => _source.Task;
public void Resume(string apikey)
{
Handled = true;
_source.SetResult(apikey);
}
public override void Cancel()
{
Handled = true;
_source.TrySetCanceled();
}
}
}

View File

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Paths;
namespace Wabbajack.Lib
{

View File

@ -1,6 +1,6 @@
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib.Interventions;
namespace Wabbajack.Lib
{

View File

@ -1,26 +1,25 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders;
using Wabbajack.DTOs.DownloadStates;
namespace Wabbajack.Lib
{
public class ManuallyDownloadFile : AUserIntervention
{
public ManualDownloader.State State { get; }
public Manual State { get; }
public override string ShortDescription { get; } = string.Empty;
public override string ExtendedDescription { get; } = string.Empty;
private TaskCompletionSource<(Uri, Wabbajack.Lib.Http.Client)> _tcs = new TaskCompletionSource<(Uri, Wabbajack.Lib.Http.Client)>();
public Task<(Uri, Wabbajack.Lib.Http.Client)> Task => _tcs.Task;
private readonly TaskCompletionSource<(Uri, HttpResponseMessage)> _tcs = new ();
public Task<(Uri, HttpResponseMessage)> Task => _tcs.Task;
private ManuallyDownloadFile(ManualDownloader.State state)
private ManuallyDownloadFile(Manual state)
{
State = state;
}
public static async Task<ManuallyDownloadFile> Create(ManualDownloader.State state)
public static async Task<ManuallyDownloadFile> Create(Manual state)
{
var result = new ManuallyDownloadFile(state);
return result;
@ -30,7 +29,7 @@ namespace Wabbajack.Lib
_tcs.SetCanceled();
}
public void Resume(Uri s, Wabbajack.Lib.Http.Client client)
public void Resume(Uri s, HttpResponseMessage client)
{
_tcs.SetResult((s, client));
}

View File

@ -1,26 +1,24 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Wabbajack.Common;
using Wabbajack.Lib.Downloaders;
using Wabbajack.DTOs.DownloadStates;
namespace Wabbajack.Lib
{
public class ManuallyDownloadNexusFile : AUserIntervention
{
public NexusDownloader.State State { get; }
public Nexus State { get; }
public override string ShortDescription { get; } = string.Empty;
public override string ExtendedDescription { get; } = string.Empty;
private TaskCompletionSource<Uri> _tcs = new TaskCompletionSource<Uri>();
public Task<Uri> Task => _tcs.Task;
private ManuallyDownloadNexusFile(NexusDownloader.State state)
private ManuallyDownloadNexusFile(Nexus state)
{
State = state;
}
public static async Task<ManuallyDownloadNexusFile> Create(NexusDownloader.State state)
public static async Task<ManuallyDownloadNexusFile> Create(Nexus state)
{
var result = new ManuallyDownloadNexusFile(state);
return result;

View File

@ -1,4 +1,4 @@
using Wabbajack.Common.StatusFeed;
using Wabbajack.Lib.Interventions;
namespace Wabbajack.Lib
{