mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge branch 'master' into image-hashing
This commit is contained in:
commit
d94aa06ab8
@ -148,7 +148,7 @@ Reading all the previous section you might wonder if Wabbajack is able to detect
|
||||
|
||||
This basically means `original + patch = final` and we only include `patch` in the `.wabbajack` file which, by itself, is just gibberish and completely useless without the original file. This allows us to distribute arbitrary changes without violating copyrights as we do not copy copyrighted material. Instead, we copy instructions on how to modify the copyrighted material.
|
||||
|
||||
You don't even have to tell Wabbajack that a specific file was modified, that you be way too much work. Instead Wabbajack will figure out which file got modified and create a binary patch. The modified file can be anything from some modified settings file to a patched plugin or optimized mesh/texture.
|
||||
You don't even have to tell Wabbajack that a specific file was modified, that would be way too much work. Instead Wabbajack will figure out which file got modified and create a binary patch. The modified file can be anything from some modified settings file to a patched plugin or optimized mesh/texture.
|
||||
|
||||
#### BSA Decomposition
|
||||
|
||||
|
@ -35,7 +35,7 @@ namespace Wabbajack.Lib.Downloaders
|
||||
typeof(MegaDownloader.State),
|
||||
typeof(ModDBDownloader.State),
|
||||
typeof(NexusDownloader.State),
|
||||
typeof(VectorPlexusDownloader.State),
|
||||
typeof(VectorPlexusOAuthDownloader.State),
|
||||
typeof(DeadlyStreamDownloader.State),
|
||||
typeof(TESAllianceDownloader.State),
|
||||
typeof(TESAllDownloader.State),
|
||||
|
301
Wabbajack.Lib/Downloaders/AbstractIPS4OAuthDownloader.cs
Normal file
301
Wabbajack.Lib/Downloaders/AbstractIPS4OAuthDownloader.cs
Normal file
@ -0,0 +1,301 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using Newtonsoft.Json;
|
||||
using ReactiveUI;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Common.Serialization.Json;
|
||||
using Wabbajack.Lib.Downloaders.DTOs;
|
||||
using Wabbajack.Lib.Http;
|
||||
using Wabbajack.Lib.Validation;
|
||||
|
||||
namespace Wabbajack.Lib.Downloaders
|
||||
{
|
||||
public abstract class AbstractIPS4OAuthDownloader<TDownloader, TState> : INeedsLogin, IDownloader, IWaitForWindowDownloader
|
||||
where TState : AbstractIPS4OAuthDownloader<TDownloader, TState>.State, new()
|
||||
where TDownloader : IDownloader
|
||||
{
|
||||
public AbstractIPS4OAuthDownloader(string clientID, Uri authEndpoint, Uri tokenEndpoint, IEnumerable<string> scopes, string encryptedKeyName)
|
||||
{
|
||||
ClientID = clientID;
|
||||
AuthorizationEndpoint = authEndpoint;
|
||||
TokenEndpoint = tokenEndpoint;
|
||||
EncryptedKeyName = encryptedKeyName;
|
||||
|
||||
TriggerLogin = ReactiveCommand.CreateFromTask(
|
||||
execute: () => Utils.CatchAndLog(async () => await Utils.Log(new RequestOAuthLogin(ClientID, authEndpoint, tokenEndpoint, SiteName, scopes, EncryptedKeyName)).Task),
|
||||
canExecute: IsLoggedIn.Select(b => !b).ObserveOnGuiThread());
|
||||
ClearLogin = ReactiveCommand.CreateFromTask(
|
||||
execute: () => Utils.CatchAndLog(async () => await Utils.DeleteEncryptedJson(EncryptedKeyName)),
|
||||
canExecute: IsLoggedIn.ObserveOnGuiThread());
|
||||
|
||||
}
|
||||
|
||||
public string EncryptedKeyName { get; }
|
||||
public Uri TokenEndpoint { get; }
|
||||
public Uri AuthorizationEndpoint { get; }
|
||||
public string ClientID { get; }
|
||||
public ReactiveCommand<Unit, Unit> TriggerLogin { get; }
|
||||
public ReactiveCommand<Unit, Unit> ClearLogin { get; }
|
||||
public IObservable<bool> IsLoggedIn => Utils.HaveEncryptedJsonObservable(EncryptedKeyName);
|
||||
public abstract string SiteName { get; }
|
||||
public IObservable<string>? MetaInfo { get; }
|
||||
public abstract Uri SiteURL { get; }
|
||||
public virtual Uri? IconUri { get; }
|
||||
public Client? AuthedClient { get; set; }
|
||||
|
||||
private bool _isPrepared = false;
|
||||
public async Task<AbstractDownloadState?> GetDownloaderState(dynamic archiveINI, bool quickMode = false)
|
||||
{
|
||||
if (archiveINI.General.ips4Site == SiteName && archiveINI.General.ips4Mod != null && archiveINI.General.ips4File != null)
|
||||
{
|
||||
if (!long.TryParse(archiveINI.General.ips4Mod, out long parsedMod))
|
||||
return null;
|
||||
var state = new TState {IPS4Mod = parsedMod, IPS4File = archiveINI.General.ips4File};
|
||||
|
||||
if (!quickMode)
|
||||
{
|
||||
var downloads = await GetDownloads(state.IPS4Mod);
|
||||
state.IPS4Url = downloads.Url ?? "";
|
||||
}
|
||||
|
||||
return state;
|
||||
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<IPS4OAuthFilesResponse.Root> GetDownloads(long modID)
|
||||
{
|
||||
var responseString = await (await GetAuthedClient())!.GetStringAsync(SiteURL+ $"api/downloads/files/{modID}") ;
|
||||
return responseString.FromJsonString<IPS4OAuthFilesResponse.Root>();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public async Task Prepare()
|
||||
{
|
||||
|
||||
if (_isPrepared) return;
|
||||
AuthedClient = (await GetAuthedClient()) ?? throw new Exception($"Not logged into {SiteName}");
|
||||
_isPrepared = true;
|
||||
}
|
||||
|
||||
|
||||
private async Task<Http.Client?> GetAuthedClient()
|
||||
{
|
||||
if (!Utils.HaveEncryptedJson(EncryptedKeyName))
|
||||
return null;
|
||||
|
||||
var data = await Utils.FromEncryptedJson<OAuthResultState>(EncryptedKeyName);
|
||||
await data.Refresh();
|
||||
var client = new Http.Client();
|
||||
client.Headers.Add(("Authorization", $"Bearer {data.AccessToken}"));
|
||||
return client;
|
||||
}
|
||||
|
||||
public async Task WaitForNextRequestWindow()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
public abstract class State : AbstractDownloadState, IMetaState
|
||||
{
|
||||
public long IPS4Mod { get; set; }
|
||||
public string IPS4File { get; set; } = "";
|
||||
public string IPS4Url { get; set; } = "";
|
||||
|
||||
public override object[] PrimaryKey => new object[] {IPS4Mod, IPS4File};
|
||||
|
||||
public override bool IsWhitelisted(ServerWhitelist whitelist)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override async Task<bool> Download(Archive a, AbsolutePath destination)
|
||||
{
|
||||
var downloads = await TypedDownloader.GetDownloads(IPS4Mod);
|
||||
var fileEntry = downloads.Files.First(f => f.Name == IPS4File);
|
||||
if (a.Size != 0 && fileEntry.Size != a.Size)
|
||||
throw new Exception(
|
||||
$"File {IPS4File} on mod {IPS4Mod} on {TypedDownloader.SiteName} appears to be re-uploaded with the same name");
|
||||
|
||||
var state = new HTTPDownloader.State(fileEntry.Url!) {Client = TypedDownloader.AuthedClient!};
|
||||
if (a.Size == 0) a.Size = fileEntry.Size!.Value;
|
||||
return await state.Download(a, destination);
|
||||
}
|
||||
|
||||
private static AbstractIPS4OAuthDownloader<TDownloader, TState> TypedDownloader => (AbstractIPS4OAuthDownloader<TDownloader, TState>)(object)DownloadDispatcher.GetInstance<TDownloader>();
|
||||
|
||||
public override async Task<bool> Verify(Archive archive, CancellationToken? token = null)
|
||||
{
|
||||
var downloads = await DownloadDispatcher.GetInstance<VectorPlexusOAuthDownloader>().GetDownloads(IPS4Mod);
|
||||
var fileEntry = downloads.Files.FirstOrDefault(f => f.Name == IPS4File);
|
||||
if (fileEntry == null) return false;
|
||||
return archive.Size == 0 || fileEntry.Size == archive.Size;
|
||||
}
|
||||
|
||||
public override string? GetManifestURL(Archive a)
|
||||
{
|
||||
return IPS4Url;
|
||||
}
|
||||
|
||||
public override string[] GetMetaIni()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
"[General]",
|
||||
$"ips4Site={TypedDownloader.SiteName}",
|
||||
$"ips4Mod={IPS4Mod}",
|
||||
$"ips4File={IPS4File}"
|
||||
};
|
||||
}
|
||||
|
||||
public Uri URL => new(IPS4Url);
|
||||
public string? Name { get; set; }
|
||||
public string? Author { get; set; }
|
||||
public string? Version { get; set; }
|
||||
public Uri? ImageURL { get; set; }
|
||||
public bool IsNSFW { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public async Task<bool> LoadMetaData()
|
||||
{
|
||||
var data = await TypedDownloader.GetDownloads(IPS4Mod);
|
||||
Name = data.Title;
|
||||
Author = data.Author?.Name;
|
||||
Version = data.Version;
|
||||
ImageURL = data.PrimaryScreenshot.Url != null ? new Uri(data.PrimaryScreenshot.Url) : null;
|
||||
IsNSFW = true;
|
||||
Description = "";
|
||||
IPS4Url = data.Url!;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonName("OAuthResultState")]
|
||||
public class OAuthResultState
|
||||
{
|
||||
[JsonProperty("access_token")]
|
||||
public string AccessToken { get; set; } = "";
|
||||
|
||||
[JsonProperty("token_type")]
|
||||
public string TokenType { get; set; } = "";
|
||||
|
||||
[JsonProperty("expires_in")]
|
||||
public long ExpiresIn { get; set; }
|
||||
|
||||
[JsonProperty("refresh_token")]
|
||||
public string RefreshToken { get; set; } = "";
|
||||
|
||||
[JsonProperty("scope")]
|
||||
public string Scope { get; set; } = "";
|
||||
|
||||
[JsonProperty("authorization_code")]
|
||||
public string AuthorizationCode { get; set; } = "";
|
||||
|
||||
[JsonProperty("token_endpoint")]
|
||||
public Uri? TokenEndpoint { get; set; }
|
||||
|
||||
[JsonProperty("expires_at")]
|
||||
public DateTime ExpiresAt { get; set; }
|
||||
|
||||
[JsonProperty("client_id")]
|
||||
public string ClientID { get; set; } = "";
|
||||
|
||||
internal void FillInData(string authCode, RequestOAuthLogin oa)
|
||||
{
|
||||
AuthorizationCode = authCode;
|
||||
TokenEndpoint = oa.TokenEndpoint;
|
||||
ExpiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(ExpiresIn);
|
||||
ClientID = oa.ClientID;
|
||||
}
|
||||
|
||||
|
||||
public async Task<bool> Refresh()
|
||||
{
|
||||
if (ExpiresAt > DateTime.UtcNow + TimeSpan.FromHours(6))
|
||||
return true;
|
||||
|
||||
var client = new Http.Client();
|
||||
var formData = new KeyValuePair<string?, string?>[]
|
||||
{
|
||||
new ("grant_type", "refresh_token"),
|
||||
new ("refresh_token", RefreshToken),
|
||||
new ("client_id", ClientID)
|
||||
};
|
||||
using var response = await client.PostAsync(TokenEndpoint!.ToString(), new FormUrlEncodedContent(formData.ToList()));
|
||||
var responseData = (await response.Content.ReadAsStringAsync()).FromJsonString<OAuthResultState>();
|
||||
|
||||
AccessToken = responseData.AccessToken;
|
||||
ExpiresIn = responseData.ExpiresIn;
|
||||
ExpiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(ExpiresIn);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public class RequestOAuthLogin : AUserIntervention
|
||||
{
|
||||
public string ClientID { get; }
|
||||
public Uri AuthorizationEndpoint { get; }
|
||||
public Uri TokenEndpoint { get; }
|
||||
public string SiteName { get; }
|
||||
|
||||
public string[] Scopes { get; }
|
||||
|
||||
public RequestOAuthLogin(string clientID, Uri authEndpoint, Uri tokenEndpoint, string siteName, IEnumerable<string> scopes, string key)
|
||||
{
|
||||
ClientID = clientID;
|
||||
AuthorizationEndpoint = authEndpoint;
|
||||
TokenEndpoint = tokenEndpoint;
|
||||
SiteName = siteName;
|
||||
Scopes = scopes.ToArray();
|
||||
EncryptedDataKey = key;
|
||||
}
|
||||
|
||||
public string EncryptedDataKey { get; set; }
|
||||
|
||||
public override string ShortDescription => $"Getting {SiteName} Login";
|
||||
public override string ExtendedDescription { get; } = string.Empty;
|
||||
|
||||
private readonly TaskCompletionSource<OAuthResultState> _source = new ();
|
||||
public Task<OAuthResultState> Task => _source.Task;
|
||||
|
||||
public async Task Resume(string authCode)
|
||||
{
|
||||
Handled = true;
|
||||
|
||||
var client = new Http.Client();
|
||||
var formData = new KeyValuePair<string?, string?>[]
|
||||
{
|
||||
new ("grant_type", "authorization_code"),
|
||||
new ("code", authCode),
|
||||
new ("client_id", ClientID)
|
||||
};
|
||||
using var response = await client.PostAsync(TokenEndpoint.ToString(), new FormUrlEncodedContent(formData.ToList()));
|
||||
var responseData = (await response.Content.ReadAsStringAsync()).FromJsonString<OAuthResultState>();
|
||||
responseData.FillInData(authCode, this);
|
||||
|
||||
await responseData.ToEcryptedJson(EncryptedDataKey);
|
||||
_source.SetResult(new OAuthResultState());
|
||||
}
|
||||
|
||||
public override void Cancel()
|
||||
{
|
||||
Handled = true;
|
||||
_source.TrySetCanceled();
|
||||
}
|
||||
}
|
||||
}
|
295
Wabbajack.Lib/Downloaders/DTOs/IPS4OAuthFilesResponse.cs
Normal file
295
Wabbajack.Lib/Downloaders/DTOs/IPS4OAuthFilesResponse.cs
Normal file
@ -0,0 +1,295 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Wabbajack.Lib.Downloaders.DTOs
|
||||
{
|
||||
public class IPS4OAuthFilesResponse
|
||||
{
|
||||
|
||||
// Root myDeserializedClass = JsonConvert.DeserializeObject<Root>(myJsonResponse);
|
||||
public class Category
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
[JsonProperty("class")]
|
||||
public string? Class { get; set; }
|
||||
}
|
||||
|
||||
public class PrimaryGroup
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonProperty("formattedName")]
|
||||
public string? FormattedName { get; set; }
|
||||
}
|
||||
|
||||
public class Author
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonProperty("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
[JsonProperty("formattedName")]
|
||||
public string? FormattedName { get; set; }
|
||||
|
||||
[JsonProperty("primaryGroup")]
|
||||
public PrimaryGroup? PrimaryGroup { get; set; }
|
||||
|
||||
[JsonProperty("joined")]
|
||||
public DateTime Joined { get; set; }
|
||||
|
||||
[JsonProperty("reputationPoints")]
|
||||
public int ReputationPoints { get; set; }
|
||||
|
||||
[JsonProperty("photoUrl")]
|
||||
public string? PhotoUrl { get; set; }
|
||||
|
||||
[JsonProperty("photoUrlIsDefault")]
|
||||
public bool PhotoUrlIsDefault { get; set; }
|
||||
|
||||
[JsonProperty("coverPhotoUrl")]
|
||||
public string? CoverPhotoUrl { get; set; }
|
||||
|
||||
[JsonProperty("profileUrl")]
|
||||
public string? ProfileUrl { get; set; }
|
||||
|
||||
[JsonProperty("posts")]
|
||||
public int Posts { get; set; }
|
||||
|
||||
[JsonProperty("lastActivity")]
|
||||
public DateTime LastActivity { get; set; }
|
||||
|
||||
[JsonProperty("lastVisit")]
|
||||
public DateTime LastVisit { get; set; }
|
||||
|
||||
[JsonProperty("lastPost")]
|
||||
public DateTime LastPost { get; set; }
|
||||
|
||||
[JsonProperty("profileViews")]
|
||||
public int ProfileViews { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class File
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
[JsonProperty("size")]
|
||||
public long? Size { get; set; }
|
||||
}
|
||||
|
||||
public class Screenshot
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
[JsonProperty("size")]
|
||||
public int Size { get; set; }
|
||||
}
|
||||
|
||||
public class PrimaryScreenshot
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
[JsonProperty("size")]
|
||||
public string? Size { get; set; }
|
||||
}
|
||||
|
||||
public class Forum
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")]
|
||||
public string? Name { get; set; }
|
||||
|
||||
[JsonProperty("topics")]
|
||||
public int Topics { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
|
||||
public class FirstPost
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("item_id")]
|
||||
public int ItemId { get; set; }
|
||||
|
||||
[JsonProperty("author")]
|
||||
public Author? Author { get; set; }
|
||||
|
||||
[JsonProperty("date")]
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
[JsonProperty("content")]
|
||||
public string? Content { get; set; }
|
||||
|
||||
[JsonProperty("hidden")]
|
||||
public bool Hidden { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
|
||||
public class LastPost
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("item_id")]
|
||||
public int ItemId { get; set; }
|
||||
|
||||
[JsonProperty("author")]
|
||||
public Author? Author { get; set; }
|
||||
|
||||
[JsonProperty("date")]
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
[JsonProperty("content")]
|
||||
public string? Content { get; set; }
|
||||
|
||||
[JsonProperty("hidden")]
|
||||
public bool Hidden { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string? Url { get; set; }
|
||||
}
|
||||
|
||||
public class Topic
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
[JsonProperty("forum")]
|
||||
public Forum? Forum { get; set; }
|
||||
|
||||
[JsonProperty("posts")]
|
||||
public int Posts { get; set; }
|
||||
|
||||
[JsonProperty("views")]
|
||||
public int Views { get; set; }
|
||||
|
||||
[JsonProperty("locked")]
|
||||
public bool Locked { get; set; }
|
||||
|
||||
[JsonProperty("hidden")]
|
||||
public bool Hidden { get; set; }
|
||||
|
||||
[JsonProperty("pinned")]
|
||||
public bool Pinned { get; set; }
|
||||
|
||||
[JsonProperty("featured")]
|
||||
public bool Featured { get; set; }
|
||||
|
||||
[JsonProperty("archived")]
|
||||
public bool Archived { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
[JsonProperty("rating")]
|
||||
public double Rating { get; set; }
|
||||
}
|
||||
|
||||
public class Root
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonProperty("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
[JsonProperty("category")]
|
||||
public Category? Category { get; set; }
|
||||
|
||||
[JsonProperty("author")]
|
||||
public Author? Author { get; set; }
|
||||
|
||||
[JsonProperty("date")]
|
||||
public DateTime Date { get; set; }
|
||||
|
||||
[JsonProperty("description")]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[JsonProperty("version")]
|
||||
public string? Version { get; set; }
|
||||
|
||||
[JsonProperty("changelog")]
|
||||
public string? Changelog { get; set; }
|
||||
|
||||
[JsonProperty("files")] public List<File> Files { get; set; } = new();
|
||||
|
||||
[JsonProperty("screenshots")] public List<Screenshot> Screenshots { get; set; } = new();
|
||||
|
||||
[JsonProperty("primaryScreenshot")]
|
||||
public PrimaryScreenshot PrimaryScreenshot { get; set; } = new();
|
||||
|
||||
[JsonProperty("downloads")]
|
||||
public int Downloads { get; set; }
|
||||
|
||||
[JsonProperty("comments")]
|
||||
public int Comments { get; set; }
|
||||
|
||||
[JsonProperty("reviews")]
|
||||
public int Reviews { get; set; }
|
||||
|
||||
[JsonProperty("views")]
|
||||
public int Views { get; set; }
|
||||
|
||||
[JsonProperty("tags")] public List<string?> Tags { get; set; } = new ();
|
||||
|
||||
[JsonProperty("locked")]
|
||||
public bool Locked { get; set; }
|
||||
|
||||
[JsonProperty("hidden")]
|
||||
public bool Hidden { get; set; }
|
||||
|
||||
[JsonProperty("pinned")]
|
||||
public bool Pinned { get; set; }
|
||||
|
||||
[JsonProperty("featured")]
|
||||
public bool Featured { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
[JsonProperty("topic")] public Topic Topic { get; set; } = new();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ namespace Wabbajack.Lib.Downloaders
|
||||
new NexusDownloader(),
|
||||
new MediaFireDownloader(),
|
||||
new LoversLabDownloader(),
|
||||
new VectorPlexusDownloader(),
|
||||
new VectorPlexusOAuthDownloader(),
|
||||
new DeadlyStreamDownloader(),
|
||||
new TESAllianceDownloader(),
|
||||
new TESAllDownloader(),
|
||||
|
@ -1,13 +1,16 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using HtmlAgilityPack;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Common.Serialization.Json;
|
||||
using Wabbajack.Lib.Validation;
|
||||
|
||||
namespace Wabbajack.Lib.Downloaders
|
||||
{
|
||||
public class VectorPlexusDownloader : AbstractIPS4Downloader<VectorPlexusDownloader, VectorPlexusDownloader.State>
|
||||
public class VectorPlexusOAuthDownloader : AbstractIPS4OAuthDownloader<VectorPlexusOAuthDownloader, VectorPlexusOAuthDownloader.State>
|
||||
{
|
||||
#region INeedsDownload
|
||||
public override string SiteName => "Vector Plexus";
|
||||
@ -15,57 +18,21 @@ namespace Wabbajack.Lib.Downloaders
|
||||
public override Uri IconUri => new Uri("https://www.vectorplexus.com/favicon.ico");
|
||||
#endregion
|
||||
|
||||
public VectorPlexusDownloader() : base(new Uri("https://vectorplexus.com/login"),
|
||||
"vectorplexus", "vectorplexus.com")
|
||||
public VectorPlexusOAuthDownloader() : base("45c6d3c9867903a7daa6ded0a38cedf8",
|
||||
new Uri("https://vectorplexus.com/oauth/authorize/"),
|
||||
new Uri("https://vectorplexus.com/oauth/token/"),
|
||||
new []{"profile", "get_downloads"},
|
||||
"vector-plexus-oauth2")
|
||||
{
|
||||
}
|
||||
|
||||
[JsonName("VectorPlexusDownloader")]
|
||||
public class State : State<VectorPlexusDownloader>
|
||||
|
||||
|
||||
[JsonName("VectorPlexusOAuthDownloader+State")]
|
||||
public class State : AbstractIPS4OAuthDownloader<VectorPlexusOAuthDownloader, VectorPlexusOAuthDownloader.State>.State
|
||||
{
|
||||
public override async Task<bool> LoadMetaData()
|
||||
public override IDownloader GetDownloader()
|
||||
{
|
||||
var html = await Downloader.AuthedClient.GetStringAsync(URL);
|
||||
var doc = new HtmlDocument();
|
||||
doc.LoadHtml(html);
|
||||
var node = doc.DocumentNode;
|
||||
|
||||
Name = HttpUtility.HtmlDecode(node
|
||||
.SelectNodes(
|
||||
"//h1[@class='ipsType_pageTitle ipsContained_container']/span[@class='ipsType_break ipsContained']")
|
||||
?.First().InnerHtml);
|
||||
|
||||
Author = HttpUtility.HtmlDecode(node
|
||||
.SelectNodes(
|
||||
"//div[@class='ipsBox_alt']/div[@class='ipsPhotoPanel ipsPhotoPanel_tiny ipsClearfix ipsSpacer_bottom']/div/p[@class='ipsType_reset ipsType_large ipsType_blendLinks']/a")
|
||||
?.First().InnerHtml);
|
||||
|
||||
Version = HttpUtility.HtmlDecode(node
|
||||
.SelectNodes("//section/h2[@class='ipsType_sectionHead']/span[@data-role='versionTitle']")
|
||||
?
|
||||
.First().InnerHtml);
|
||||
|
||||
var url = HttpUtility.HtmlDecode(node
|
||||
.SelectNodes(
|
||||
"//div[@class='ipsBox ipsSpacer_top ipsSpacer_double']/section/div[@class='ipsPad ipsAreaBackground']/div[@class='ipsCarousel ipsClearfix']/div[@class='ipsCarousel_inner']/ul[@class='cDownloadsCarousel ipsClearfix']/li[@class='ipsCarousel_item ipsAreaBackground_reset ipsPad_half']/span[@class='ipsThumb ipsThumb_medium ipsThumb_bg ipsCursor_pointer']")
|
||||
?.First().GetAttributeValue("data-fullurl", "none"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
ImageURL = new Uri(url);
|
||||
return true;
|
||||
}
|
||||
|
||||
url = HttpUtility.HtmlDecode(node
|
||||
.SelectNodes(
|
||||
"//article[@class='ipsColumn ipsColumn_fluid']/div[@class='ipsPad']/section/div[@class='ipsType_richText ipsContained ipsType_break']/p/a/img[@class='ipsImage ipsImage_thumbnailed']")
|
||||
?.First().GetAttributeValue("src", ""));
|
||||
if (!string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
ImageURL = new Uri(url);
|
||||
}
|
||||
|
||||
return true;
|
||||
return DownloadDispatcher.GetInstance<VectorPlexusOAuthDownloader>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +85,8 @@ namespace Wabbajack.Lib.LibCefHelpers
|
||||
public string Path { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public static Func<IBrowser, IFrame, string, IRequest, IResourceHandler>? SchemeHandler { get; set; }
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
if (Inited || Cef.IsInitialized) return;
|
||||
@ -92,9 +94,28 @@ namespace Wabbajack.Lib.LibCefHelpers
|
||||
CefSettings settings = new CefSettings();
|
||||
settings.CachePath = Consts.CefCacheLocation.ToString();
|
||||
settings.JavascriptFlags = "--noexpose_wasm";
|
||||
settings.RegisterScheme(new CefCustomScheme()
|
||||
{
|
||||
SchemeName = "wabbajack",
|
||||
SchemeHandlerFactory = new SchemeHandlerFactor()
|
||||
});
|
||||
|
||||
|
||||
Cef.Initialize(settings);
|
||||
}
|
||||
|
||||
private class SchemeHandlerFactor : ISchemeHandlerFactory
|
||||
{
|
||||
public IResourceHandler Create(IBrowser browser, IFrame frame, string schemeName, IRequest request)
|
||||
{
|
||||
if (SchemeHandler != null && schemeName == "wabbajack")
|
||||
{
|
||||
return SchemeHandler!(browser, frame, schemeName, request);
|
||||
}
|
||||
return new ResourceHandler();
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Inited { get; set; }
|
||||
|
||||
public static void ClearCookies()
|
||||
|
@ -362,6 +362,8 @@ namespace Wabbajack.Server.Services
|
||||
return (archive, ArchiveStatus.Valid);
|
||||
case MediaFireDownloader.State _:
|
||||
return (archive, ArchiveStatus.Valid);
|
||||
case LoversLabDownloader.State _ :
|
||||
return (archive, ArchiveStatus.Valid);
|
||||
default:
|
||||
{
|
||||
if (data.ArchiveStatus.TryGetValue((archive.State.PrimaryKeyString, archive.Hash),
|
||||
|
@ -396,13 +396,15 @@ namespace Wabbajack.Test
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
[Fact]
|
||||
public async Task VectorPlexusDownload()
|
||||
{
|
||||
await DownloadDispatcher.GetInstance<VectorPlexusDownloader>().Prepare();
|
||||
await DownloadDispatcher.GetInstance<VectorPlexusOAuthDownloader>().Prepare();
|
||||
var ini = @"[General]
|
||||
directURL=https://vectorplexus.com/files/file/290-wabbajack-test-file";
|
||||
ips4Site=Vector Plexus
|
||||
ips4Mod=290
|
||||
ips4File=WABBAJACK_TEST_FILE.zip";
|
||||
|
||||
var state = (AbstractDownloadState)await DownloadDispatcher.ResolveArchive(ini.LoadIniString());
|
||||
|
||||
@ -414,13 +416,25 @@ namespace Wabbajack.Test
|
||||
|
||||
Assert.True(converted.IsWhitelisted(new ServerWhitelist { AllowedPrefixes = new List<string>() }));
|
||||
|
||||
await converted.Download(new Archive(state: null!) { Name = "Vector Plexus Test.zip" }, filename.Path);
|
||||
var archive = new Archive(state: null!) {Name = "Vector Plexus Test.zip"};
|
||||
await converted.Download(archive, filename.Path);
|
||||
|
||||
Assert.Equal(Hash.FromBase64("eSIyd+KOG3s="), await filename.Path.FileHashAsync());
|
||||
|
||||
Assert.Equal("Cheese for Everyone!", await filename.Path.ReadAllTextAsync());
|
||||
Assert.True(converted is VectorPlexusOAuthDownloader.State);
|
||||
|
||||
}*/
|
||||
var st = (VectorPlexusOAuthDownloader.State)converted;
|
||||
Assert.True(await st.LoadMetaData());
|
||||
Assert.Equal("halgari", st.Author);
|
||||
Assert.Equal("Wabbajack Test File", st.Name);
|
||||
Assert.True(st.IsNSFW);
|
||||
Assert.Equal("1.0.0", st.Version);
|
||||
Assert.Equal("https://vectorplexus.com/files/file/290-wabbajack-test-file/", st.GetManifestURL(archive));
|
||||
Assert.True(st.ImageURL != null);
|
||||
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task YandexDownloader()
|
||||
|
@ -5,6 +5,7 @@ using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using CefSharp;
|
||||
@ -16,6 +17,7 @@ using Wabbajack.Lib.Downloaders;
|
||||
using Wabbajack.Lib.LibCefHelpers;
|
||||
using Wabbajack.Lib.NexusApi;
|
||||
using Wabbajack.Lib.WebAutomation;
|
||||
using WebSocketSharp;
|
||||
|
||||
namespace Wabbajack
|
||||
{
|
||||
@ -83,6 +85,14 @@ namespace Wabbajack
|
||||
var data = await c.Downloader.GetAndCacheCookies(new CefSharpWrapper(vm.Browser), m => vm.Instructions = m, cancel.Token);
|
||||
c.Resume(data);
|
||||
});
|
||||
break;
|
||||
case RequestOAuthLogin oa:
|
||||
await WrapBrowserJob(oa, async (vm, cancel) =>
|
||||
{
|
||||
await OAuthLogin(oa, vm, cancel);
|
||||
});
|
||||
|
||||
|
||||
break;
|
||||
case CriticalFailureIntervention c:
|
||||
MessageBox.Show(c.ExtendedDescription, c.ShortDescription, MessageBoxButton.OK,
|
||||
@ -97,6 +107,44 @@ namespace Wabbajack
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OAuthLogin(RequestOAuthLogin oa, WebBrowserVM vm, CancellationTokenSource cancel)
|
||||
{
|
||||
await vm.Driver.WaitForInitialized();
|
||||
vm.Instructions = "Please log in and allow Wabbajack to access your account";
|
||||
|
||||
var wrapper = new CefSharpWrapper(vm.Browser);
|
||||
var scopes = string.Join(" ", oa.Scopes);
|
||||
var state = Guid.NewGuid().ToString();
|
||||
await wrapper.NavigateTo(new Uri(oa.AuthorizationEndpoint + $"?response_type=code&client_id={oa.ClientID}&state={state}&scope={scopes}"));
|
||||
|
||||
Helpers.SchemeHandler = (browser, frame, _, request) =>
|
||||
{
|
||||
var req = new Uri(request.Url);
|
||||
var parsed = HttpUtility.ParseQueryString(req.Query);
|
||||
if (parsed.Contains("state"))
|
||||
{
|
||||
if (parsed.Get("state") != state)
|
||||
{
|
||||
Utils.Log("Bad OAuth state, state, this shouldn't happen");
|
||||
oa.Cancel();
|
||||
return new ResourceHandler();
|
||||
}
|
||||
}
|
||||
if (parsed.Contains("code"))
|
||||
{
|
||||
oa.Resume(parsed.Get("code"));
|
||||
}
|
||||
else
|
||||
{
|
||||
oa.Cancel();
|
||||
}
|
||||
return new ResourceHandler();
|
||||
};
|
||||
|
||||
while (!oa.Task.IsCanceled && !oa.Task.IsCompleted && !cancel.IsCancellationRequested)
|
||||
await Task.Delay(250);
|
||||
}
|
||||
|
||||
private async Task HandleManualDownload(WebBrowserVM vm, CancellationTokenSource cancel, ManuallyDownloadFile manuallyDownloadFile)
|
||||
{
|
||||
var browser = new CefSharpWrapper(vm.Browser);
|
||||
|
Loading…
Reference in New Issue
Block a user