mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #1495 from wabbajack-tools/oauth2-support
Oauth2 support
This commit is contained in:
commit
fae922ef01
@ -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()
|
||||
|
@ -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