mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Remove unneeded code, port status messages
This commit is contained in:
parent
31f32ee2f5
commit
b53f98eebd
@ -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
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -6,6 +6,7 @@ using System.Threading.Tasks;
|
||||
using ReactiveUI;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib;
|
||||
using Wabbajack.Lib.Interventions;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -5,6 +5,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Lib;
|
||||
using Wabbajack.Lib.Interventions;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -10,6 +10,7 @@ using DynamicData;
|
||||
using DynamicData.Kernel;
|
||||
using ReactiveUI;
|
||||
using Wabbajack.Lib;
|
||||
using Wabbajack.Lib.Extensions;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
|
234
Wabbajack.Lib/Extensions/RxExt.cs
Normal file
234
Wabbajack.Lib/Extensions/RxExt.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
12
Wabbajack.Lib/Interventions/AErrorMessage.cs
Normal file
12
Wabbajack.Lib/Interventions/AErrorMessage.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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; }
|
||||
|
||||
|
6
Wabbajack.Lib/Interventions/IError.cs
Normal file
6
Wabbajack.Lib/Interventions/IError.cs
Normal file
@ -0,0 +1,6 @@
|
||||
namespace Wabbajack.Lib.Interventions
|
||||
{
|
||||
public interface IError : IStatusMessage
|
||||
{
|
||||
}
|
||||
}
|
9
Wabbajack.Lib/Interventions/IException.cs
Normal file
9
Wabbajack.Lib/Interventions/IException.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Wabbajack.Lib.Interventions
|
||||
{
|
||||
public interface IException : IError
|
||||
{
|
||||
Exception Exception { get; }
|
||||
}
|
||||
}
|
11
Wabbajack.Lib/Interventions/IStatusMessage.cs
Normal file
11
Wabbajack.Lib/Interventions/IStatusMessage.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace Wabbajack.Lib.Interventions
|
||||
{
|
||||
public interface IStatusMessage
|
||||
{
|
||||
DateTime Timestamp { get; }
|
||||
string ShortDescription { get; }
|
||||
string ExtendedDescription { get; }
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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("'", "'")
|
||||
.Replace("<br/>", "\n\n")
|
||||
.Replace("<br />", "\n\n")
|
||||
.Replace("!", "!");
|
||||
}
|
||||
}
|
||||
}
|
@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
using System.Threading.Tasks;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Common.StatusFeed;
|
||||
using Wabbajack.Lib.Interventions;
|
||||
|
||||
namespace Wabbajack.Lib
|
||||
{
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Wabbajack.Common.StatusFeed;
|
||||
using Wabbajack.Lib.Interventions;
|
||||
|
||||
namespace Wabbajack.Lib
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user