wabbajack/Wabbajack.Lib/NexusApi/NexusApi.cs

395 lines
13 KiB
C#
Raw Normal View History

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;
2019-12-07 03:50:50 +00:00
using System.Threading;
2020-05-22 20:56:58 +00:00
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";
2021-02-04 03:48:30 +00:00
/// <summary>
/// Forces the client to do manual downloading via CEF (for testing)
/// </summary>
private static bool ManualTestingMode = true;
2021-01-09 21:46:46 +00:00
public Http.Client HttpClient { get; } = new();
#region Authentication
public static string? ApiKey { get; set; }
2019-11-19 05:10:07 +00:00
public bool IsAuthenticated => ApiKey != null;
2020-11-05 21:30:00 +00:00
public int RemainingAPICalls => Math.Max(HourlyRemaining, DailyRemaining);
2020-04-10 01:29:53 +00:00
private Task<UserStatus>? _userStatus;
2019-12-07 03:50:50 +00:00
public Task<UserStatus> UserStatus
{
get
{
2021-01-06 13:02:12 +00:00
return _userStatus ??= GetUserStatus();
}
}
2019-12-07 03:50:50 +00:00
public async Task<bool> IsPremium()
{
return IsAuthenticated && (await UserStatus).is_premium;
}
2019-12-07 03:50:50 +00:00
public async Task<string> Username() => (await UserStatus).name;
private static AsyncLock _getAPIKeyLock = new AsyncLock();
2019-12-07 03:50:50 +00:00
private static async Task<string> GetApiKey()
{
using (await _getAPIKeyLock.WaitAsync())
{
2019-11-16 04:02:37 +00:00
// Clean up old location
if (File.Exists(API_KEY_CACHE_FILE))
{
2019-11-16 04:02:37 +00:00
File.Delete(API_KEY_CACHE_FILE);
}
try
2019-11-16 04:02:37 +00:00
{
return await Utils.FromEncryptedJson<string>("nexusapikey");
2019-11-16 04:02:37 +00:00
}
catch (Exception)
2019-11-16 04:02:37 +00:00
{
}
var env_key = Environment.GetEnvironmentVariable("NEXUSAPIKEY");
if (env_key != null)
{
return env_key;
}
2020-11-15 06:20:05 +00:00
2019-12-20 20:51:10 +00:00
return await RequestAndCacheAPIKey();
}
}
2019-12-20 20:51:10 +00:00
public static async Task<string> RequestAndCacheAPIKey()
{
var result = await Utils.Log(new RequestNexusAuthorization()).Task;
await result.ToEcryptedJson("nexusapikey");
2019-12-20 20:51:10 +00:00
return result;
}
public static async Task<string> SetupNexusLogin(IWebDriver browser, Action<string> updateStatus, CancellationToken cancel)
{
2020-01-13 21:11:07 +00:00
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);
}
}
2019-12-07 02:45:13 +00:00
public async Task<UserStatus> GetUserStatus()
{
var url = "https://api.nexusmods.com/v1/users/validate.json";
2020-11-15 06:20:05 +00:00
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;
}
2020-05-14 11:53:51 +00:00
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()),
2020-05-14 11:53:51 +00:00
int.Parse(response.Headers.GetValues("X-RL-Hourly-Remaining").First()));
_dailyRemaining = result.Item1;
_hourlyRemaining = result.Item2;
return result;
2020-05-14 11:53:51 +00:00
}
#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
{
2020-11-05 21:30:00 +00:00
_dailyRemaining = int.Parse(response.Headers.GetValues("x-rl-daily-remaining").First());
_hourlyRemaining = int.Parse(response.Headers.GetValues("x-rl-hourly-remaining").First());
2019-10-14 20:40:49 +00:00
this.RaisePropertyChanged(nameof(DailyRemaining));
this.RaisePropertyChanged(nameof(HourlyRemaining));
}
2019-10-14 20:38:19 +00:00
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)}"));
}
2020-04-10 01:29:53 +00:00
public static async Task<NexusApiClient> Get(string? apiKey = null)
2019-12-07 03:50:50 +00:00
{
2020-04-10 01:29:53 +00:00
apiKey ??= await GetApiKey();
2019-12-07 03:50:50 +00:00
return new NexusApiClient(apiKey);
}
2021-01-09 21:46:46 +00:00
public async Task<T> Get<T>(string url, Http.Client? client = null)
{
2020-06-16 22:21:01 +00:00
client ??= HttpClient;
2020-01-03 23:02:48 +00:00
int retries = 0;
TOP:
try
{
2020-06-16 22:21:01 +00:00
using var response = await client.GetAsync(url);
await UpdateRemaining(response);
2020-01-03 23:02:48 +00:00
if (!response.IsSuccessStatusCode)
{
2021-01-01 00:06:56 +00:00
Utils.Log($"Nexus call failed: {response.RequestMessage!.RequestUri}");
2020-05-22 20:56:58 +00:00
throw new HttpException(response);
}
2020-01-03 23:02:48 +00:00
await using var stream = await response.Content.ReadAsStreamAsync();
2020-04-11 03:16:10 +00:00
return stream.FromJson<T>(genericReader:true);
2020-01-03 23:02:48 +00:00
}
catch (TimeoutException)
{
2020-01-03 23:02:48 +00:00
if (retries == Consts.MaxHTTPRetries)
throw;
Utils.Log($"Nexus call to {url} failed, retrying {retries} of {Consts.MaxHTTPRetries}");
retries++;
goto TOP;
}
catch (Exception e)
{
2021-01-09 21:46:46 +00:00
Utils.Log($"Nexus call failed `{url}`: " + e);
throw;
}
}
2019-12-07 02:45:13 +00:00
private async Task<T> GetCached<T>(string url)
2019-10-14 21:04:12 +00:00
{
2020-07-14 12:15:01 +00:00
if (BuildServerStatus.IsBuildServerDown)
2019-12-07 02:45:13 +00:00
return await Get<T>(url);
2019-10-14 21:04:12 +00:00
2020-07-14 12:15:01 +00:00
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())));
2019-10-14 21:04:12 +00:00
}
2021-02-04 03:48:30 +00:00
private static AsyncLock ManualDownloadLock = new();
public async Task<string> GetNexusDownloadLink(NexusDownloader.State archive)
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
var info = await GetModInfo(archive.Game, archive.ModID);
2021-02-04 03:48:30 +00:00
var fileInfo = await GetModFiles(archive.Game, archive.ModID);
if (!info.available || !fileInfo.files.Any(f => f.file_id == archive.FileID && f.category_name != null))
throw new Exception("Mod unavailable");
2021-02-04 03:48:30 +00:00
if (await IsPremium() && !ManualTestingMode)
2020-11-05 21:30:00 +00:00
{
if (HourlyRemaining <= 0 && DailyRemaining <= 0)
{
throw new NexusAPIQuotaExceeded();
}
2020-11-05 21:30:00 +00:00
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;
2020-02-06 05:30:31 +00:00
}
try
{
2021-02-04 03:48:30 +00:00
using var _ = await ManualDownloadLock.WaitAsync();
await Task.Delay(1000);
Utils.Log($"Requesting manual download for {archive.Name} {archive.PrimaryKeyString}");
2020-02-06 05:30:31 +00:00
return (await Utils.Log(await ManuallyDownloadNexusFile.Create(archive)).Task).ToString();
}
catch (TaskCanceledException ex)
{
Utils.Error(ex, "Manual cancellation of download");
throw;
}
}
2020-04-10 01:29:53 +00:00
public class GetModFilesResponse
{
2020-04-10 01:29:53 +00:00
public List<NexusFileInfo> files { get; set; } = new List<NexusFileInfo>();
}
2020-04-11 03:16:10 +00:00
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";
2020-04-11 03:16:10 +00:00
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;
}
2019-12-07 02:45:13 +00:00
public async Task<List<MD5Response>> GetModInfoFromMD5(Game game, string md5Hash)
2019-11-04 14:33:17 +00:00
{
var url = $"https://api.nexusmods.com/v1/games/{game.MetaData().NexusName}/mods/md5_search/{md5Hash}.json";
2019-12-07 02:45:13 +00:00
return await Get<List<MD5Response>>(url);
2019-11-04 14:33:17 +00:00
}
2020-04-11 03:16:10 +00:00
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";
2020-04-11 03:16:10 +00:00
if (useCache)
{
2020-07-14 12:15:01 +00:00
try
{
return await GetCached<ModInfo>(url);
}
catch (HttpException)
{
return await Get<ModInfo>(url);
}
2020-04-11 03:16:10 +00:00
}
return await Get<ModInfo>(url);
}
private class DownloadLink
{
2020-04-10 01:29:53 +00:00
public string URI { get; set; } = string.Empty;
}
2019-11-05 00:27:46 +00:00
2020-02-06 05:30:31 +00:00
public static Uri ManualDownloadUrl(NexusDownloader.State state)
{
return new Uri($"https://www.nexusmods.com/{state.Game.MetaData().NexusName}/mods/{state.ModID}?tab=files");
2020-02-06 05:30:31 +00:00
}
}
}