mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Merge pull request #1169 from qhy040404/feat/qr
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<!-- https://learn.microsoft.com/en-us/windows/apps/design/style/segoe-fluent-icons-font -->
|
||||
<x:String x:Key="FontIconContentAdd"></x:String>
|
||||
<x:String x:Key="FontIconContentSetting"></x:String>
|
||||
<x:String x:Key="FontIconContentRefresh"></x:String>
|
||||
@@ -12,6 +13,7 @@
|
||||
<x:String x:Key="FontIconContentBulletedList"></x:String>
|
||||
<x:String x:Key="FontIconContentCheckList"></x:String>
|
||||
<x:String x:Key="FontIconContentWebsite"></x:String>
|
||||
<x:String x:Key="FontIconContentQRCode"></x:String>
|
||||
<x:String x:Key="FontIconContentHomeGroup"></x:String>
|
||||
<x:String x:Key="FontIconContentAsteriskBadge12"></x:String>
|
||||
<x:String x:Key="FontIconContentZipFolder"></x:String>
|
||||
|
||||
@@ -46,7 +46,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd(ApplicationJson);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.CNVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -62,7 +62,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_id", "bll8iq97cem8");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.CNVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "2");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_name", string.Empty);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "2.16.0");
|
||||
@@ -81,7 +81,7 @@ internal static partial class IocHttpClientConfiguration
|
||||
client.DefaultRequestHeaders.Add("x-rpc-app_version", SaltConstants.OSVersion);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-client_type", "5");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-language", "zh-cn");
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId);
|
||||
client.DefaultRequestHeaders.Add("x-rpc-device_id", HoyolabOptions.DeviceId36);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
61
src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs
Normal file
61
src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
internal static class Uuid
|
||||
{
|
||||
public static Guid NewV5(string name, Guid namespaceId)
|
||||
{
|
||||
Span<byte> namespaceBuffer = stackalloc byte[16];
|
||||
Verify.Operation(namespaceId.TryWriteBytes(namespaceBuffer), "Failed to copy namespace guid bytes");
|
||||
Span<byte> nameBytes = Encoding.UTF8.GetBytes(name);
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
ReverseEndianness(namespaceBuffer);
|
||||
}
|
||||
|
||||
Span<byte> data = stackalloc byte[namespaceBuffer.Length + nameBytes.Length];
|
||||
namespaceBuffer.CopyTo(data);
|
||||
nameBytes.CopyTo(data[namespaceBuffer.Length..]);
|
||||
|
||||
Span<byte> temp = stackalloc byte[20];
|
||||
Verify.Operation(SHA1.TryHashData(data, temp, out _), "Failed to compute SHA1 hash of UUID");
|
||||
|
||||
Span<byte> hash = temp[..16];
|
||||
|
||||
if (BitConverter.IsLittleEndian)
|
||||
{
|
||||
ReverseEndianness(hash);
|
||||
}
|
||||
|
||||
hash[8] &= 0x3F;
|
||||
hash[8] |= 0x80;
|
||||
|
||||
int versionIndex = BitConverter.IsLittleEndian ? 7 : 6;
|
||||
|
||||
hash[versionIndex] &= 0x0F;
|
||||
hash[versionIndex] |= 0x50;
|
||||
|
||||
return new(hash);
|
||||
}
|
||||
|
||||
private static void ReverseEndianness(in Span<byte> guidByte)
|
||||
{
|
||||
ExchangeBytes(guidByte, 0, 3);
|
||||
ExchangeBytes(guidByte, 1, 2);
|
||||
ExchangeBytes(guidByte, 4, 5);
|
||||
ExchangeBytes(guidByte, 6, 7);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void ExchangeBytes(in Span<byte> guid, int left, int right)
|
||||
{
|
||||
(guid[right], guid[left]) = (guid[left], guid[right]);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace Snap.Hutao.Extension;
|
||||
|
||||
internal static partial class EnumerableExtension
|
||||
{
|
||||
public static bool TryGetValue(this NameValueCollection collection, string name, [NotNullWhen(true)] out string? value)
|
||||
public static bool TryGetSingleValue(this NameValueCollection collection, string name, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
if (collection.AllKeys.Contains(name))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Factory.QrCode;
|
||||
|
||||
internal interface IQRCodeFactory
|
||||
{
|
||||
byte[] Create(string source);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using QRCoder;
|
||||
|
||||
namespace Snap.Hutao.Factory.QrCode;
|
||||
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IQRCodeFactory))]
|
||||
internal class QRCodeFactory : IQRCodeFactory
|
||||
{
|
||||
public byte[] Create(string source)
|
||||
{
|
||||
using (QRCodeGenerator generator = new())
|
||||
{
|
||||
using (QRCodeData data = generator.CreateQrCode(source, QRCodeGenerator.ECCLevel.Q))
|
||||
{
|
||||
using (BitmapByteQRCode code = new(data))
|
||||
{
|
||||
return code.GetGraphic(10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1265,6 +1265,9 @@
|
||||
<data name="ViewDialogLaunchGamePackageConvertTitle" xml:space="preserve">
|
||||
<value>正在转换客户端</value>
|
||||
</data>
|
||||
<data name="ViewDialogQRCodeTitle" xml:space="preserve">
|
||||
<value>使用米游社扫描二维码</value>
|
||||
</data>
|
||||
<data name="ViewDialogSettingDeleteUserDataContent" xml:space="preserve">
|
||||
<value>该操作是不可逆的,所有用户登录状态会丢失</value>
|
||||
</data>
|
||||
@@ -2624,6 +2627,9 @@
|
||||
<data name="ViewUserCookieOperationLoginMihoyoUserAction" xml:space="preserve">
|
||||
<value>网页登录</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationLoginQRCodeAction" xml:space="preserve">
|
||||
<value>扫码登录</value>
|
||||
</data>
|
||||
<data name="ViewUserCookieOperationManualInputAction" xml:space="preserve">
|
||||
<value>手动输入</value>
|
||||
</data>
|
||||
|
||||
@@ -30,7 +30,7 @@ internal sealed partial class GachaLogQueryManualInputProvider : IGachaLogQueryP
|
||||
{
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(queryString);
|
||||
|
||||
if (query.TryGetValue("auth_appid", out string? appId) && appId is "webview_gacha")
|
||||
if (query.TryGetSingleValue("auth_appid", out string? appId) && appId is "webview_gacha")
|
||||
{
|
||||
string? queryLanguageCode = query["lang"];
|
||||
if (metadataOptions.LanguageCodeFitsCurrentLocale(queryLanguageCode))
|
||||
|
||||
@@ -74,7 +74,7 @@ internal sealed partial class UserFingerprintService : IUserFingerprintService
|
||||
SeedTime = $"{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}",
|
||||
ExtFields = JsonSerializer.Serialize(extendProperties),
|
||||
AppName = "bbs_cn",
|
||||
BbsDeviceId = HoyolabOptions.DeviceId,
|
||||
BbsDeviceId = HoyolabOptions.DeviceId36,
|
||||
DeviceFp = string.IsNullOrEmpty(user.Fingerprint) ? Core.Random.GetLowerHexString(13) : user.Fingerprint,
|
||||
};
|
||||
|
||||
|
||||
@@ -174,6 +174,7 @@
|
||||
<None Remove="View\Dialog\LaunchGameAccountNameDialog.xaml" />
|
||||
<None Remove="View\Dialog\LaunchGamePackageConvertDialog.xaml" />
|
||||
<None Remove="View\Dialog\UserDialog.xaml" />
|
||||
<None Remove="View\Dialog\UserQRCodeDialog.xaml" />
|
||||
<None Remove="View\Guide\GuideView.xaml" />
|
||||
<None Remove="View\InfoBarView.xaml" />
|
||||
<None Remove="View\MainView.xaml" />
|
||||
@@ -302,6 +303,7 @@
|
||||
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.8.8" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.2428" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.4.231115000" />
|
||||
<PackageReference Include="QRCoder" Version="1.4.3" />
|
||||
<PackageReference Include="Snap.Discord.GameSDK" Version="1.5.0" />
|
||||
<PackageReference Include="Snap.Hutao.SourceGeneration" Version="1.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@@ -330,6 +332,11 @@
|
||||
<ItemGroup>
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Dialog\UserQRCodeDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="View\Card\Primitive\HorizontalCard.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
|
||||
21
src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml
Normal file
21
src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml
Normal file
@@ -0,0 +1,21 @@
|
||||
<ContentDialog
|
||||
x:Class="Snap.Hutao.View.Dialog.UserQRCodeDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
Title="{shcm:ResourceString Name=ViewDialogQRCodeTitle}"
|
||||
CloseButtonCommand="{x:Bind CancelCommand}"
|
||||
CloseButtonText="{shcm:ResourceString Name=ContentDialogCancelCloseButtonText}"
|
||||
DefaultButton="Close"
|
||||
Style="{StaticResource DefaultContentDialogStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel>
|
||||
<Image
|
||||
Width="320"
|
||||
Height="320"
|
||||
Source="{x:Bind QRCodeSource}"/>
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
157
src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs
Normal file
157
src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Snap.Hutao.Factory.QrCode;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
|
||||
using Snap.Hutao.Web.Hoyolab.Passport;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Collections.Specialized;
|
||||
using System.IO;
|
||||
using System.Web;
|
||||
|
||||
namespace Snap.Hutao.View.Dialog;
|
||||
|
||||
[DependencyProperty("QRCodeSource", typeof(ImageSource))]
|
||||
internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable
|
||||
{
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly PandaClient pandaClient;
|
||||
private readonly IQRCodeFactory qrCodeFactory;
|
||||
|
||||
private readonly CancellationTokenSource userManualCancellationTokenSource = new();
|
||||
private bool disposed;
|
||||
|
||||
public UserQRCodeDialog(IServiceProvider serviceProvider)
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
taskContext = serviceProvider.GetRequiredService<ITaskContext>();
|
||||
pandaClient = serviceProvider.GetRequiredService<PandaClient>();
|
||||
qrCodeFactory = serviceProvider.GetRequiredService<IQRCodeFactory>();
|
||||
}
|
||||
|
||||
~UserQRCodeDialog()
|
||||
{
|
||||
Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!disposed)
|
||||
{
|
||||
userManualCancellationTokenSource.Dispose();
|
||||
disposed = true;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async ValueTask<ValueResult<bool, UidGameToken>> GetUidGameTokenAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await GetUidGameTokenCoreAsync().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
userManualCancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Command("CancelCommand")]
|
||||
private void Cancel()
|
||||
{
|
||||
userManualCancellationTokenSource.Cancel();
|
||||
}
|
||||
|
||||
private async ValueTask<ValueResult<bool, UidGameToken>> GetUidGameTokenCoreAsync()
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
await ShowAsync();
|
||||
|
||||
while (!userManualCancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
CancellationToken token = userManualCancellationTokenSource.Token;
|
||||
string ticket = await FetchQRCodeAndSetImageAsync(token).ConfigureAwait(false);
|
||||
UidGameToken? uidGameToken = await WaitQueryQRCodeConfirmAsync(ticket, token).ConfigureAwait(false);
|
||||
|
||||
if (uidGameToken is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Hide();
|
||||
return new(true, uidGameToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new(false, default!);
|
||||
}
|
||||
|
||||
private async ValueTask<string> FetchQRCodeAndSetImageAsync(CancellationToken token)
|
||||
{
|
||||
Response<UrlWrapper> fetchResponse = await pandaClient.QRCodeFetchAsync(token).ConfigureAwait(false);
|
||||
if (!fetchResponse.IsOk())
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string url = fetchResponse.Data.Url;
|
||||
string ticket = GetTicketFromUrl(fetchResponse.Data.Url);
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
BitmapImage bitmap = new();
|
||||
await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.Create(url)).AsRandomAccessStream());
|
||||
QRCodeSource = bitmap;
|
||||
|
||||
return ticket;
|
||||
|
||||
static string GetTicketFromUrl(in ReadOnlySpan<char> urlSpan)
|
||||
{
|
||||
ReadOnlySpan<char> querySpan = urlSpan[urlSpan.IndexOf('?')..];
|
||||
NameValueCollection queryCollection = HttpUtility.ParseQueryString(querySpan.ToString());
|
||||
if (queryCollection.TryGetSingleValue("ticket", out string? ticket))
|
||||
{
|
||||
return ticket;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<UidGameToken?> WaitQueryQRCodeConfirmAsync(string ticket, CancellationToken token)
|
||||
{
|
||||
using (PeriodicTimer timer = new(new(0, 0, 3)))
|
||||
{
|
||||
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
|
||||
{
|
||||
Response<GameLoginResult> query = await pandaClient.QRCodeQueryAsync(ticket, token).ConfigureAwait(false);
|
||||
|
||||
if (query is { ReturnCode: 0, Data: { Stat: "Confirmed", Payload.Proto: "Account" } })
|
||||
{
|
||||
UidGameToken? uidGameToken = JsonSerializer.Deserialize<UidGameToken>(query.Data.Payload.Raw);
|
||||
ArgumentNullException.ThrowIfNull(uidGameToken);
|
||||
return uidGameToken;
|
||||
}
|
||||
else if (query.ReturnCode == (int)KnownReturnCode.QrCodeExpired)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -154,6 +154,13 @@
|
||||
Command="{Binding LoginMihoyoUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentWebsite}}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/>
|
||||
<AppBarButton
|
||||
Width="{StaticResource LargeAppBarButtonWidth}"
|
||||
MaxWidth="{StaticResource LargeAppBarButtonWidth}"
|
||||
Margin="2,-4"
|
||||
Command="{Binding LoginByQRCodeCommand}"
|
||||
Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentQRCode}}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginQRCodeAction}"/>
|
||||
<AppBarButton
|
||||
Width="{StaticResource LargeAppBarButtonWidth}"
|
||||
MaxWidth="{StaticResource LargeAppBarButtonWidth}"
|
||||
|
||||
@@ -15,6 +15,8 @@ using Snap.Hutao.Service.User;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using Snap.Hutao.View.Page;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Passport;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Text;
|
||||
using Windows.System;
|
||||
@@ -173,6 +175,32 @@ internal sealed partial class UserViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
[Command("LoginByQRCodeCommand")]
|
||||
private async Task LoginByQRCode()
|
||||
{
|
||||
UserQRCodeDialog dialog = await contentDialogFactory.CreateInstanceAsync<UserQRCodeDialog>().ConfigureAwait(false);
|
||||
(bool isOk, UidGameToken? token) = await dialog.GetUidGameTokenAsync().ConfigureAwait(false);
|
||||
|
||||
if (!isOk)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Response<LoginResult> sTokenResponse = await serviceProvider
|
||||
.GetRequiredService<PassportClient2>()
|
||||
.GetSTokenByGameTokenAsync(token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (sTokenResponse.IsOk())
|
||||
{
|
||||
Cookie stokenV2 = Cookie.FromLoginResult(sTokenResponse.Data);
|
||||
|
||||
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(stokenV2, false).ConfigureAwait(false);
|
||||
|
||||
await HandleUserOptionResultAsync(optionResult, uid).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
[Command("RemoveUserCommand")]
|
||||
private async Task RemoveUserAsync(User? user)
|
||||
{
|
||||
|
||||
@@ -40,6 +40,7 @@ internal static class ApiEndpoints
|
||||
{
|
||||
return $"{ApiTakumiAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ApiTaKumiBindingApi
|
||||
@@ -282,6 +283,14 @@ internal static class ApiEndpoints
|
||||
public const string AnnContent = $"{Hk4eApiAnnouncementApi}/getAnnContent?{AnnouncementQuery}";
|
||||
#endregion
|
||||
|
||||
#region Hk4eSdk
|
||||
|
||||
public const string QrCodeFetch = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/fetch";
|
||||
|
||||
public const string QrCodeQuery = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/query";
|
||||
|
||||
#endregion
|
||||
|
||||
#region PassportApi | PassportApiV4
|
||||
|
||||
/// <summary>
|
||||
@@ -294,6 +303,11 @@ internal static class ApiEndpoints
|
||||
/// </summary>
|
||||
public const string AccountGetLTokenBySToken = $"{PassportApiAuthApi}/getLTokenBySToken";
|
||||
|
||||
/// <summary>
|
||||
/// 通过GameToken获取V2SToken
|
||||
/// </summary>
|
||||
public const string AccountGetSTokenByGameToken = $"{PassportApi}/account/ma-cn-session/app/getTokenByGameToken";
|
||||
|
||||
/// <summary>
|
||||
/// 获取V2SToken
|
||||
/// </summary>
|
||||
@@ -382,6 +396,8 @@ internal static class ApiEndpoints
|
||||
private const string Hk4eApi = "https://hk4e-api.mihoyo.com";
|
||||
private const string Hk4eApiAnnouncementApi = $"{Hk4eApi}/common/hk4e_cn/announcement/api";
|
||||
|
||||
private const string Hk4eSdk = "https://hk4e-sdk.mihoyo.com";
|
||||
|
||||
private const string PassportApi = "https://passport-api.mihoyo.com";
|
||||
private const string PassportApiAuthApi = $"{PassportApi}/account/auth/api";
|
||||
private const string PassportApiV4 = "https://passport-api-v4.mihoyo.com";
|
||||
|
||||
@@ -233,7 +233,7 @@ internal class MiHoYoJSBridge
|
||||
// Skip x-rpc-lifecycle_id
|
||||
{ "x-rpc-app_id", "bll8iq97cem8" },
|
||||
{ "x-rpc-client_type", "5" },
|
||||
{ "x-rpc-device_id", HoyolabOptions.DeviceId },
|
||||
{ "x-rpc-device_id", HoyolabOptions.DeviceId36 },
|
||||
{ "x-rpc-app_version", userAndUid.IsOversea ? SaltConstants.OSVersion : SaltConstants.CNVersion },
|
||||
{ "x-rpc-sdk_version", "2.16.0" },
|
||||
};
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
|
||||
|
||||
[HighQuality]
|
||||
internal sealed class GameLoginRequest
|
||||
{
|
||||
[JsonPropertyName("app_id")]
|
||||
public int AppId { get; set; }
|
||||
|
||||
[JsonPropertyName("device")]
|
||||
public string Device { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("ticket")]
|
||||
public string? Ticket { get; set; }
|
||||
|
||||
public static GameLoginRequest Create(int appId, string device, string? ticket = null)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
AppId = appId,
|
||||
Device = device,
|
||||
Ticket = ticket,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
|
||||
|
||||
/// <summary>
|
||||
/// 扫码登录结果
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed class GameLoginResult
|
||||
{
|
||||
[JsonPropertyName("stat")]
|
||||
public string Stat { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("payload")]
|
||||
public GameLoginResultPayload Payload { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
|
||||
|
||||
[HighQuality]
|
||||
internal sealed partial class GameLoginResultPayload
|
||||
{
|
||||
[JsonPropertyName("proto")]
|
||||
public string Proto { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("raw")]
|
||||
public string Raw { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("ext")]
|
||||
public string Ext { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
|
||||
|
||||
[ConstructorGenerated(ResolveHttpClient = true)]
|
||||
[HttpClient(HttpClientConfiguration.XRpc2)]
|
||||
internal sealed partial class PandaClient
|
||||
{
|
||||
private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory;
|
||||
private readonly ILogger<PandaClient> logger;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
public async ValueTask<Response<UrlWrapper>> QRCodeFetchAsync(CancellationToken token = default)
|
||||
{
|
||||
GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40);
|
||||
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.QrCodeFetch)
|
||||
.PostJson(options);
|
||||
|
||||
Response<UrlWrapper>? resp = await builder
|
||||
.TryCatchSendAsync<Response<UrlWrapper>>(httpClient, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
public async ValueTask<Response<GameLoginResult>> QRCodeQueryAsync(string ticket, CancellationToken token = default)
|
||||
{
|
||||
GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40, ticket);
|
||||
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.QrCodeQuery)
|
||||
.PostJson(options);
|
||||
|
||||
Response<GameLoginResult>? resp = await builder
|
||||
.TryCatchSendAsync<Response<GameLoginResult>>(httpClient, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
|
||||
|
||||
internal sealed class UrlWrapper
|
||||
{
|
||||
[JsonPropertyName("url")]
|
||||
public string Url { get; set; } = default!;
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Options;
|
||||
using Snap.Hutao.Web.Hoyolab.DataSigning;
|
||||
using System.Collections.Frozen;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
/// <summary>
|
||||
/// 米游社选项
|
||||
/// </summary>
|
||||
internal sealed class HoyolabOptions : IOptions<HoyolabOptions>
|
||||
internal static class HoyolabOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 米游社请求UA
|
||||
@@ -35,7 +35,12 @@ internal sealed class HoyolabOptions : IOptions<HoyolabOptions>
|
||||
/// <summary>
|
||||
/// 米游社设备Id
|
||||
/// </summary>
|
||||
public static string DeviceId { get; } = Guid.NewGuid().ToString();
|
||||
public static string DeviceId36 { get; } = Guid.NewGuid().ToString();
|
||||
|
||||
/// <summary>
|
||||
/// 扫码登录设备Id
|
||||
/// </summary>
|
||||
public static string DeviceId40 { get; } = GenerateDeviceId40();
|
||||
|
||||
/// <summary>
|
||||
/// 盐
|
||||
@@ -56,6 +61,16 @@ internal sealed class HoyolabOptions : IOptions<HoyolabOptions>
|
||||
[SaltType.OSX6] = "okr4obncj8bw5a65hbnn5oo6ixjc3l9w",
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public HoyolabOptions Value { get => this; }
|
||||
[SuppressMessage("", "CA1308")]
|
||||
private static string GenerateDeviceId40()
|
||||
{
|
||||
Guid uuid = Core.Uuid.NewV5(DeviceId36, new("9450ea74-be9c-35c0-9568-f97407856768"));
|
||||
|
||||
Span<byte> uuidSpan = stackalloc byte[16];
|
||||
Span<byte> hash = stackalloc byte[20];
|
||||
|
||||
Verify.Operation(uuid.TryWriteBytes(uuidSpan), "Failed to write UUID bytes");
|
||||
Verify.Operation(SHA1.TryHashData(uuidSpan, hash, out _), "Failed to write SHA1 hash");
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Passport;
|
||||
|
||||
internal sealed class AccountIdGameToken
|
||||
{
|
||||
[JsonPropertyName("account_id")]
|
||||
public int AccountId { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("game_token")]
|
||||
public string GameToken { get; set; } = default!;
|
||||
}
|
||||
@@ -5,9 +5,11 @@ using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Web.Hoyolab.Annotation;
|
||||
using Snap.Hutao.Web.Hoyolab.DataSigning;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Globalization;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Passport;
|
||||
@@ -68,6 +70,25 @@ internal sealed partial class PassportClient2
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
public async ValueTask<Response<LoginResult>> GetSTokenByGameTokenAsync(UidGameToken account, CancellationToken token = default)
|
||||
{
|
||||
AccountIdGameToken data = new()
|
||||
{
|
||||
AccountId = int.Parse(account.Uid, CultureInfo.InvariantCulture),
|
||||
GameToken = account.GameToken,
|
||||
};
|
||||
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(ApiEndpoints.AccountGetSTokenByGameToken)
|
||||
.PostJson(data);
|
||||
|
||||
Response<LoginResult>? resp = await builder
|
||||
.TryCatchSendAsync<Response<LoginResult>>(httpClient, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
private class Timestamp
|
||||
{
|
||||
[JsonPropertyName("t")]
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Passport;
|
||||
|
||||
[HighQuality]
|
||||
internal sealed class UidGameToken
|
||||
{
|
||||
[JsonPropertyName("uid")]
|
||||
public string Uid { get; set; } = default!;
|
||||
|
||||
[JsonPropertyName("token")]
|
||||
public string GameToken { get; set; } = default!;
|
||||
}
|
||||
@@ -84,6 +84,11 @@ internal enum KnownReturnCode
|
||||
/// </summary>
|
||||
AppIdError = -109,
|
||||
|
||||
/// <summary>
|
||||
/// 二维码已过期
|
||||
/// </summary>
|
||||
QrCodeExpired = -106,
|
||||
|
||||
/// <summary>
|
||||
/// 验证密钥过期
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user