From a8d4dc84a1b50f01e2245281c655a9c2080480ef Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Mon, 11 Dec 2023 14:31:34 +0800 Subject: [PATCH 1/5] impl #870 --- .../Snap.Hutao/Control/Theme/Glyph.xaml | 1 + src/Snap.Hutao/Snap.Hutao/Core/Random.cs | 5 + .../Factory/QrCode/IQrCodeFactory.cs | 9 ++ .../Factory/QrCode/QrCodeFactory.cs | 25 ++++ .../Snap.Hutao/Resource/Localization/SH.resx | 6 + src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 7 ++ .../Snap.Hutao/View/Dialog/QrCodeDialog.xaml | 21 ++++ .../View/Dialog/QrCodeDialog.xaml.cs | 107 ++++++++++++++++++ src/Snap.Hutao/Snap.Hutao/View/UserView.xaml | 7 ++ .../ViewModel/User/UserViewModel.cs | 28 +++++ src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs | 23 +++- .../Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs | 14 +++ .../Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs | 63 +++++++++++ .../Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs | 11 ++ .../Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs | 20 ++++ .../Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs | 14 +++ .../Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs | 24 ++++ .../QrCode/QrCodeQueryPayload.Constant.cs | 13 +++ .../Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs | 17 +++ .../Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs | 14 +++ .../Takumi/Account/GameTokenWrapper.cs | 13 +++ .../Takumi/Account/SessionAppClient.cs | 48 ++++++++ .../Web/Response/KnownReturnCode.cs | 5 + 23 files changed, 494 insertions(+), 1 deletion(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml index 555f142e..d658fc29 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml @@ -17,4 +17,5 @@ + \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Random.cs b/src/Snap.Hutao/Snap.Hutao/Core/Random.cs index 74afc366..f1427404 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Random.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Random.cs @@ -19,4 +19,9 @@ internal static class Random { return new(System.Random.Shared.GetItems("0123456789abcdefghijklmnopqrstuvwxyz".AsSpan(), length)); } + + public static string GetLetterAndNumberString(int length) + { + return new(System.Random.Shared.GetItems("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".AsSpan(), length)); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs new file mode 100644 index 00000000..3381dc01 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs @@ -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[] CreateByteArr(string source); +} diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs new file mode 100644 index 00000000..b16d0d29 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs @@ -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[] CreateByteArr(string source) + { + using (QRCodeGenerator generator = new()) + { + using (QRCodeData data = generator.CreateQrCode(source, QRCodeGenerator.ECCLevel.Q)) + { + using (BitmapByteQRCode code = new(data)) + { + return code.GetGraphic(10); + } + } + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 2ba84fb3..42907257 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -1265,6 +1265,9 @@ 正在转换客户端 + + 使用米游社扫描二维码 + 该操作是不可逆的,所有用户登录状态会丢失 @@ -2618,6 +2621,9 @@ 网页登录 + + 扫码登录 + 手动输入 diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index a1ed520d..7f248f3e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -173,6 +173,7 @@ + @@ -301,6 +302,7 @@ + all @@ -329,6 +331,11 @@ + + + MSBuild:Compile + + MSBuild:Compile diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml new file mode 100644 index 00000000..bc8e44cb --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml @@ -0,0 +1,21 @@ + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs new file mode 100644 index 00000000..e14bf962 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs @@ -0,0 +1,107 @@ +// 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.QrCode; +using Snap.Hutao.Web.Response; +using System.IO; +using System.Text.RegularExpressions; + +namespace Snap.Hutao.View.Dialog; + +/// +/// 扫描二维码对话框 +/// +[HighQuality] +internal sealed partial class QrCodeDialog : ContentDialog +{ + private readonly ITaskContext taskContext; + private readonly QrCodeClient qrCodeClient; + private readonly IQrCodeFactory qrCodeFactory; + + private QrCodeAccount? account; + + /// + /// 构造一个新的扫描二维码对话框 + /// + /// 服务提供器 + public QrCodeDialog(IServiceProvider serviceProvider) + { + InitializeComponent(); + + taskContext = serviceProvider.GetRequiredService(); + qrCodeClient = serviceProvider.GetRequiredService(); + qrCodeFactory = serviceProvider.GetRequiredService(); + + Initialize().SafeForget(); + } + + /// + /// 获取登录的用户 + /// + /// QrCodeAccount + public async ValueTask> GetAccountAsync() + { + await taskContext.SwitchToMainThreadAsync(); + ContentDialogResult result = await ShowAsync(); + return new(account is not null, account!); + } + + private async ValueTask Initialize() + { + Response fetch = await qrCodeClient.PostQrCodeFetchAsync().ConfigureAwait(false); + if (fetch.IsOk()) + { + string url = Regex.Unescape(fetch.Data.Url); + string ticket = url.ToUri().Query.Split('&').Last().Split('=').Last(); + + await taskContext.SwitchToMainThreadAsync(); + BitmapImage bitmap = new(); + await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.CreateByteArr(url)).AsRandomAccessStream()); + + ImageView.Source = bitmap; + if (bitmap is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 }) + { + VisualStateManager.GoToState(this, "Loaded", true); + } + + await taskContext.SwitchToBackgroundAsync(); + + using (PeriodicTimer timer = new(TimeSpan.FromSeconds(3))) + { + while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) + { + Response query = await qrCodeClient.PostQrCodeQueryAsync(ticket).ConfigureAwait(false); + if (query.IsOk(false)) + { + switch (query.Data.Stat) + { + case QrCodeQueryStatus.INIT: + case QrCodeQueryStatus.SCANNED: + break; // @switch + case QrCodeQueryStatus.CONFIRMED: + if (query.Data.Payload.Proto == QrCodeQueryPayload.ACCOUNT) + { + account = JsonSerializer.Deserialize(query.Data.Payload.Raw); + await taskContext.SwitchToMainThreadAsync(); + Hide(); + return; + } + + break; // @switch + } + } + else if (query.ReturnCode == (int)KnownReturnCode.QrCodeExpired) + { + Initialize().SafeForget(); + break; // @while + } + } + } + } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml index 0fbae96e..fe59a7b2 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml @@ -154,6 +154,13 @@ Command="{Binding LoginMihoyoUserCommand}" Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentWebsite}}" Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/> + ? users; @@ -173,6 +178,29 @@ internal sealed partial class UserViewModel : ObservableObject } } + [Command("LoginQrCodeCommand")] + private async Task LoginQrCode() + { + // ContentDialog must be created by main thread. + await taskContext.SwitchToMainThreadAsync(); + + QrCodeDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + ValueResult result = await dialog.GetAccountAsync().ConfigureAwait(false); + + if (result.TryGetValue(out QrCodeAccount account)) + { + Response gameTokenResp = await sessionAppClient.PostSTokenByGameTokenAsync(account).ConfigureAwait(false); + + if (gameTokenResp.IsOk()) + { + Cookie stokenV2 = Cookie.FromLoginResult(gameTokenResp.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) { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs index 64ffdee8..e19db0fc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs @@ -16,6 +16,15 @@ namespace Snap.Hutao.Web; [SuppressMessage("", "SA1124")] internal static class ApiEndpoints { + #region ApiTakumiAccountSessionApp + + /// + /// 通过 GameToken 获取 SToken (V2) + /// + public const string STokenByGameToken = $"{ApiTakumiAccountSessionApp}/getTokenByGameToken"; + + #endregion + #region ApiTakumiAuthApi /// @@ -40,6 +49,7 @@ internal static class ApiEndpoints { return $"{ApiTakumiAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3"; } + #endregion #region ApiTaKumiBindingApi @@ -357,9 +367,18 @@ internal static class ApiEndpoints // https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api/content?key=eYd89JmJ&language=zh-cn&launcher_id=18 #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 Hosts | Queries private const string ApiTakumi = "https://api-takumi.mihoyo.com"; private const string ApiTakumiAuthApi = $"{ApiTakumi}/auth/api"; + private const string ApiTakumiAccountSessionApp = $"{ApiTakumi}/account/ma-cn-session/app"; private const string ApiTaKumiBindingApi = $"{ApiTakumi}/binding/api"; private const string ApiTakumiCardApi = $"{ApiTakumiRecord}/game_record/app/card/api"; @@ -382,6 +401,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"; @@ -402,4 +423,4 @@ internal static class ApiEndpoints private const string AnnouncementQuery = "game=hk4e&game_biz=hk4e_cn&lang=zh-cn&bundle_id=hk4e_cn&platform=pc®ion=cn_gf01&level=55&uid=100000000"; #endregion -} \ No newline at end of file +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs new file mode 100644 index 00000000..9c48cd45 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +internal sealed class QrCodeAccount +{ + [JsonPropertyName("uid")] + public string Stuid { get; set; } = default!; + + [JsonPropertyName("token")] + public string GameToken { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs new file mode 100644 index 00000000..7c5d5020 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs @@ -0,0 +1,63 @@ +// 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.QrCode; + +[HighQuality] +[ConstructorGenerated(ResolveHttpClient = true)] +[HttpClient(HttpClientConfiguration.Default)] +internal sealed partial class QrCodeClient +{ + private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; + private readonly ILogger logger; + private readonly HttpClient httpClient; + + private readonly string device = Core.Random.GetLetterAndNumberString(64); + + /// + /// 异步获取扫码链接 + /// + /// 取消令牌 + /// login url + public async ValueTask> PostQrCodeFetchAsync(CancellationToken token = default) + { + QrCodeFetchOptions options = new(4, device); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeFetch) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + + /// + /// 异步获取扫码状态 + /// + /// 扫码链接中的ticket + /// 取消令牌/param> + /// 扫码状态 + public async ValueTask> PostQrCodeQueryAsync(string ticket, CancellationToken token = default) + { + QrCodeQueryOptions options = new(4, device, ticket); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeQuery) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs new file mode 100644 index 00000000..bac70add --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs @@ -0,0 +1,11 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +internal sealed class QrCodeFetch +{ + [JsonPropertyName("url")] + public string Url { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs new file mode 100644 index 00000000..df689141 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +internal sealed class QrCodeFetchOptions +{ + public QrCodeFetchOptions(int appId, string device) + { + AppId = appId; + Device = device; + } + + [JsonPropertyName("app_id")] + public int AppId { get; set; } + + [JsonPropertyName("device")] + public string Device { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs new file mode 100644 index 00000000..3091ec4e --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +internal sealed class QrCodeQuery +{ + [JsonPropertyName("stat")] + public string Stat { get; set; } = default!; + + [JsonPropertyName("payload")] + public QrCodeQueryPayload Payload { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs new file mode 100644 index 00000000..9f2806d8 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +internal sealed class QrCodeQueryOptions +{ + public QrCodeQueryOptions(int appId, string device, string ticket) + { + AppId = appId; + Device = device; + Ticket = ticket; + } + + [JsonPropertyName("app_id")] + public int AppId { get; set; } + + [JsonPropertyName("device")] + public string Device { get; set; } = default!; + + [JsonPropertyName("ticket")] + public string Ticket { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs new file mode 100644 index 00000000..d71edfd4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs @@ -0,0 +1,13 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +/// +/// Proto 常量 +/// +internal sealed partial class QrCodeQueryPayload +{ + public const string ACCOUNT = "Account"; + public const string RAW = "Raw"; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs new file mode 100644 index 00000000..e66b64ba --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs @@ -0,0 +1,17 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +[HighQuality] +internal sealed partial class QrCodeQueryPayload +{ + [JsonPropertyName("proto")] + public string Proto { get; set; } = default!; + + [JsonPropertyName("raw")] + public string Raw { get; set; } = default!; + + [JsonPropertyName("ext")] + public string Ext { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs new file mode 100644 index 00000000..ce0e9813 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +/// +/// 扫码状态 +/// +internal sealed partial class QrCodeQueryStatus +{ + public const string INIT = "Init"; + public const string SCANNED = "Scanned"; + public const string CONFIRMED = "Confirmed"; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs new file mode 100644 index 00000000..1b62d7cb --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs @@ -0,0 +1,13 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Takumi.Account; + +internal sealed class GameTokenWrapper +{ + [JsonPropertyName("account_id")] + public int Stuid { get; set; } = default!; + + [JsonPropertyName("game_token")] + public string GameToken { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs new file mode 100644 index 00000000..6cd5b0bf --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs @@ -0,0 +1,48 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; +using Snap.Hutao.Web.Hoyolab.Passport; +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.Takumi.Account; + +[HighQuality] +[ConstructorGenerated(ResolveHttpClient = true)] +[HttpClient(HttpClientConfiguration.XRpc2)] +internal sealed partial class SessionAppClient +{ + private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; + private readonly ILogger logger; + private readonly HttpClient httpClient; + + /// + /// 通过 GameToken 获取 SToken (V2) + /// + /// 扫码获得的账户信息 + /// 取消令牌 + /// 登录结果 + public async ValueTask> PostSTokenByGameTokenAsync(QrCodeAccount account, CancellationToken token = default) + { + GameTokenWrapper wrapper = new() + { + Stuid = int.Parse(account.Stuid, CultureInfo.CurrentCulture), + GameToken = account.GameToken, + }; + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.STokenByGameToken) + .PostJson(wrapper); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs index b5cc3540..d1f701b1 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs @@ -138,4 +138,9 @@ internal enum KnownReturnCode /// 实时便笺 /// CODE10104 = 10104, + + /// + /// 二维码已过期 + /// + QrCodeExpired = -106, } From 2fb6cd344174861410cce4f54def1bb6f0868452 Mon Sep 17 00:00:00 2001 From: qhy040404 Date: Mon, 11 Dec 2023 18:47:41 +0800 Subject: [PATCH 2/5] code style (?) --- .../Factory/QrCode/IQrCodeFactory.cs | 2 +- .../Factory/QrCode/QrCodeFactory.cs | 2 +- .../Snap.Hutao/View/Dialog/QrCodeDialog.xaml | 1 - .../View/Dialog/QrCodeDialog.xaml.cs | 71 ++++++++++--------- src/Snap.Hutao/Snap.Hutao/View/UserView.xaml | 2 +- .../ViewModel/User/UserViewModel.cs | 9 ++- src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs | 15 ++-- .../Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs | 63 ---------------- .../Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs | 20 ------ .../Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs | 14 ---- .../Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs | 24 ------- .../Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs | 14 ---- .../Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs | 5 ++ .../Passport/GameLoginRequestOptions.cs | 26 +++++++ .../GameLoginRequestResult.cs} | 7 +- .../Web/Hoyolab/Passport/GameLoginResult.cs | 21 ++++++ .../Passport/GameLoginResultOptions.cs | 30 ++++++++ .../GameLoginResultPayload.ProtoTypes.cs} | 4 +- .../GameLoginResultPayload.cs} | 5 +- .../Hoyolab/Passport/GameLoginResultStatus.cs | 14 ++++ .../Web/Hoyolab/Passport/PassportClient2.cs | 41 +++++++++++ .../UidGameToken.cs} | 6 +- .../Takumi/Account/SessionAppClient.cs | 7 +- .../Web/Response/KnownReturnCode.cs | 10 +-- 24 files changed, 209 insertions(+), 204 deletions(-) delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Hk4e/QrCode/QrCodeFetch.cs => Passport/GameLoginRequestResult.cs} (56%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Hk4e/QrCode/QrCodeQueryPayload.Constant.cs => Passport/GameLoginResultPayload.ProtoTypes.cs} (76%) rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Hk4e/QrCode/QrCodeQueryPayload.cs => Passport/GameLoginResultPayload.cs} (77%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Hk4e/QrCode/QrCodeAccount.cs => Passport/UidGameToken.cs} (63%) diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs index 3381dc01..0ab732fa 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs @@ -5,5 +5,5 @@ namespace Snap.Hutao.Factory.QrCode; internal interface IQrCodeFactory { - byte[] CreateByteArr(string source); + byte[] CreateQrCodeByteArray(string source); } diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs index b16d0d29..4ee567fc 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs @@ -9,7 +9,7 @@ namespace Snap.Hutao.Factory.QrCode; [Injection(InjectAs.Singleton, typeof(IQrCodeFactory))] internal class QrCodeFactory : IQrCodeFactory { - public byte[] CreateByteArr(string source) + public byte[] CreateQrCodeByteArray(string source) { using (QRCodeGenerator generator = new()) { diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml index bc8e44cb..177a814c 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml @@ -5,7 +5,6 @@ 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" - xmlns:shvd="using:Snap.Hutao.View.Dialog" Title="{shcm:ResourceString Name=ViewDialogQrCodeTitle}" CloseButtonText="{shcm:ResourceString Name=ContentDialogCancelCloseButtonText}" DefaultButton="Close" diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs index e14bf962..235f766b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs @@ -3,10 +3,10 @@ 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.QrCode; +using Snap.Hutao.Web.Hoyolab.Passport; using Snap.Hutao.Web.Response; using System.IO; using System.Text.RegularExpressions; @@ -20,10 +20,10 @@ namespace Snap.Hutao.View.Dialog; internal sealed partial class QrCodeDialog : ContentDialog { private readonly ITaskContext taskContext; - private readonly QrCodeClient qrCodeClient; + private readonly PassportClient2 passportClient2; private readonly IQrCodeFactory qrCodeFactory; - private QrCodeAccount? account; + private UidGameToken? account; /// /// 构造一个新的扫描二维码对话框 @@ -34,26 +34,27 @@ internal sealed partial class QrCodeDialog : ContentDialog InitializeComponent(); taskContext = serviceProvider.GetRequiredService(); - qrCodeClient = serviceProvider.GetRequiredService(); + passportClient2 = serviceProvider.GetRequiredService(); qrCodeFactory = serviceProvider.GetRequiredService(); - Initialize().SafeForget(); + FetchQrCodeAsync().SafeForget(); } /// /// 获取登录的用户 /// /// QrCodeAccount - public async ValueTask> GetAccountAsync() + [SuppressMessage("", "SH007")] + public async ValueTask> GetAccountAsync() { await taskContext.SwitchToMainThreadAsync(); ContentDialogResult result = await ShowAsync(); return new(account is not null, account!); } - private async ValueTask Initialize() + private async ValueTask FetchQrCodeAsync() { - Response fetch = await qrCodeClient.PostQrCodeFetchAsync().ConfigureAwait(false); + Response fetch = await passportClient2.PostQrCodeFetchAsync().ConfigureAwait(false); if (fetch.IsOk()) { string url = Regex.Unescape(fetch.Data.Url); @@ -61,7 +62,7 @@ internal sealed partial class QrCodeDialog : ContentDialog await taskContext.SwitchToMainThreadAsync(); BitmapImage bitmap = new(); - await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.CreateByteArr(url)).AsRandomAccessStream()); + await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.CreateQrCodeByteArray(url)).AsRandomAccessStream()); ImageView.Source = bitmap; if (bitmap is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 }) @@ -70,37 +71,41 @@ internal sealed partial class QrCodeDialog : ContentDialog } await taskContext.SwitchToBackgroundAsync(); + await CheckStatusAsync(ticket).ConfigureAwait(false); + } + } - using (PeriodicTimer timer = new(TimeSpan.FromSeconds(3))) + private async ValueTask CheckStatusAsync(string ticket) + { + using (PeriodicTimer timer = new(TimeSpan.FromSeconds(3))) + { + while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) { - while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) + Response query = await passportClient2.PostQrCodeQueryAsync(ticket).ConfigureAwait(false); + if (query.IsOk(false)) { - Response query = await qrCodeClient.PostQrCodeQueryAsync(ticket).ConfigureAwait(false); - if (query.IsOk(false)) + switch (query.Data.Stat) { - switch (query.Data.Stat) - { - case QrCodeQueryStatus.INIT: - case QrCodeQueryStatus.SCANNED: - break; // @switch - case QrCodeQueryStatus.CONFIRMED: - if (query.Data.Payload.Proto == QrCodeQueryPayload.ACCOUNT) - { - account = JsonSerializer.Deserialize(query.Data.Payload.Raw); - await taskContext.SwitchToMainThreadAsync(); - Hide(); - return; - } + case GameLoginResultStatus.Init: + case GameLoginResultStatus.Scanned: + break; // @switch + case GameLoginResultStatus.Confirmed: + if (query.Data.Payload.Proto == GameLoginResultPayload.ACCOUNT) + { + account = JsonSerializer.Deserialize(query.Data.Payload.Raw); + await taskContext.SwitchToMainThreadAsync(); + Hide(); + return; // Stop timer + } - break; // @switch - } - } - else if (query.ReturnCode == (int)KnownReturnCode.QrCodeExpired) - { - Initialize().SafeForget(); - break; // @while + break; // @switch } } + else if (query.ReturnCode == (int)KnownReturnCode.QrCodeExpired) + { + FetchQrCodeAsync().SafeForget(); + break; // @while + } } } } diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml index fe59a7b2..d58233b3 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml @@ -158,7 +158,7 @@ Width="{StaticResource LargeAppBarButtonWidth}" MaxWidth="{StaticResource LargeAppBarButtonWidth}" Margin="2,-4" - Command="{Binding LoginQrCodeCommand}" + Command="{Binding LoginByQrCodeCommand}" Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentQrCode}}" Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginQrCodeAction}"/> ().ConfigureAwait(false); - ValueResult result = await dialog.GetAccountAsync().ConfigureAwait(false); + ValueResult result = await dialog.GetAccountAsync().ConfigureAwait(false); - if (result.TryGetValue(out QrCodeAccount account)) + if (result.TryGetValue(out UidGameToken account)) { Response gameTokenResp = await sessionAppClient.PostSTokenByGameTokenAsync(account).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs index e19db0fc..95032e1f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs @@ -16,15 +16,6 @@ namespace Snap.Hutao.Web; [SuppressMessage("", "SA1124")] internal static class ApiEndpoints { - #region ApiTakumiAccountSessionApp - - /// - /// 通过 GameToken 获取 SToken (V2) - /// - public const string STokenByGameToken = $"{ApiTakumiAccountSessionApp}/getTokenByGameToken"; - - #endregion - #region ApiTakumiAuthApi /// @@ -304,6 +295,11 @@ internal static class ApiEndpoints /// public const string AccountGetLTokenBySToken = $"{PassportApiAuthApi}/getLTokenBySToken"; + /// + /// 通过GameToken获取V2SToken + /// + public const string AccountGetSTokenByGameToken = $"{PassportApi}/account/ma-cn-session/app/getTokenByGameToken"; + /// /// 获取V2SToken /// @@ -378,7 +374,6 @@ internal static class ApiEndpoints #region Hosts | Queries private const string ApiTakumi = "https://api-takumi.mihoyo.com"; private const string ApiTakumiAuthApi = $"{ApiTakumi}/auth/api"; - private const string ApiTakumiAccountSessionApp = $"{ApiTakumi}/account/ma-cn-session/app"; private const string ApiTaKumiBindingApi = $"{ApiTakumi}/binding/api"; private const string ApiTakumiCardApi = $"{ApiTakumiRecord}/game_record/app/card/api"; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs deleted file mode 100644 index 7c5d5020..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeClient.cs +++ /dev/null @@ -1,63 +0,0 @@ -// 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.QrCode; - -[HighQuality] -[ConstructorGenerated(ResolveHttpClient = true)] -[HttpClient(HttpClientConfiguration.Default)] -internal sealed partial class QrCodeClient -{ - private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; - private readonly ILogger logger; - private readonly HttpClient httpClient; - - private readonly string device = Core.Random.GetLetterAndNumberString(64); - - /// - /// 异步获取扫码链接 - /// - /// 取消令牌 - /// login url - public async ValueTask> PostQrCodeFetchAsync(CancellationToken token = default) - { - QrCodeFetchOptions options = new(4, device); - - HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(ApiEndpoints.QrCodeFetch) - .PostJson(options); - - Response? resp = await builder - .TryCatchSendAsync>(httpClient, logger, token) - .ConfigureAwait(false); - - return Response.Response.DefaultIfNull(resp); - } - - /// - /// 异步获取扫码状态 - /// - /// 扫码链接中的ticket - /// 取消令牌/param> - /// 扫码状态 - public async ValueTask> PostQrCodeQueryAsync(string ticket, CancellationToken token = default) - { - QrCodeQueryOptions options = new(4, device, ticket); - - HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(ApiEndpoints.QrCodeQuery) - .PostJson(options); - - Response? resp = await builder - .TryCatchSendAsync>(httpClient, logger, token) - .ConfigureAwait(false); - - return Response.Response.DefaultIfNull(resp); - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs deleted file mode 100644 index df689141..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetchOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -[HighQuality] -internal sealed class QrCodeFetchOptions -{ - public QrCodeFetchOptions(int appId, string device) - { - AppId = appId; - Device = device; - } - - [JsonPropertyName("app_id")] - public int AppId { get; set; } - - [JsonPropertyName("device")] - public string Device { get; set; } = default!; -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs deleted file mode 100644 index 3091ec4e..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQuery.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -[HighQuality] -internal sealed class QrCodeQuery -{ - [JsonPropertyName("stat")] - public string Stat { get; set; } = default!; - - [JsonPropertyName("payload")] - public QrCodeQueryPayload Payload { get; set; } = default!; -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs deleted file mode 100644 index 9f2806d8..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryOptions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -[HighQuality] -internal sealed class QrCodeQueryOptions -{ - public QrCodeQueryOptions(int appId, string device, string ticket) - { - AppId = appId; - Device = device; - Ticket = ticket; - } - - [JsonPropertyName("app_id")] - public int AppId { get; set; } - - [JsonPropertyName("device")] - public string Device { get; set; } = default!; - - [JsonPropertyName("ticket")] - public string Ticket { get; set; } = default!; -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs deleted file mode 100644 index ce0e9813..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryStatus.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -/// -/// 扫码状态 -/// -internal sealed partial class QrCodeQueryStatus -{ - public const string INIT = "Init"; - public const string SCANNED = "Scanned"; - public const string CONFIRMED = "Confirmed"; -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs index f5e391a6..4610b2c9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs @@ -37,6 +37,11 @@ internal sealed class HoyolabOptions : IOptions /// public static string DeviceId { get; } = Guid.NewGuid().ToString(); + /// + /// 扫码登录设备Id + /// + public static string Device { get; } = Core.Random.GetLetterAndNumberString(64); + /// /// 盐 /// diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs new file mode 100644 index 00000000..fb3b77a4 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs @@ -0,0 +1,26 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Passport; + +/// +/// 扫码登录请求配置 +/// +[HighQuality] +internal sealed class GameLoginRequestOptions +{ + [JsonPropertyName("app_id")] + public int AppId { get; set; } + + [JsonPropertyName("device")] + public string Device { get; set; } = default!; + + public static GameLoginRequestOptions Create(int appId, string device) + { + return new GameLoginRequestOptions + { + AppId = appId, + Device = device, + }; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestResult.cs similarity index 56% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestResult.cs index bac70add..13f067b2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeFetch.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestResult.cs @@ -1,10 +1,13 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; +namespace Snap.Hutao.Web.Hoyolab.Passport; +/// +/// 扫码登录请求结果 +/// [HighQuality] -internal sealed class QrCodeFetch +internal sealed class GameLoginRequestResult { [JsonPropertyName("url")] public string Url { get; set; } = default!; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs new file mode 100644 index 00000000..c3757f69 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs @@ -0,0 +1,21 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.Json.Annotation; +using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; + +namespace Snap.Hutao.Web.Hoyolab.Passport; + +/// +/// 扫码登录结果 +/// +[HighQuality] +internal sealed class GameLoginResult +{ + [JsonPropertyName("stat")] + [JsonEnum(JsonSerializeType.String)] + public GameLoginResultStatus Stat { get; set; } = default!; + + [JsonPropertyName("payload")] + public GameLoginResultPayload Payload { get; set; } = default!; +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs new file mode 100644 index 00000000..d5f17cd6 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs @@ -0,0 +1,30 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Passport; + +/// +/// 扫码登录结果请求配置 +/// +[HighQuality] +internal sealed class GameLoginResultOptions +{ + [JsonPropertyName("app_id")] + public int AppId { get; set; } + + [JsonPropertyName("device")] + public string Device { get; set; } = default!; + + [JsonPropertyName("ticket")] + public string Ticket { get; set; } = default!; + + public static GameLoginResultOptions Create(int appId, string device, string ticket) + { + return new GameLoginResultOptions + { + AppId = appId, + Device = device, + Ticket = ticket, + }; + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs similarity index 76% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs index d71edfd4..179b7501 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.Constant.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs @@ -4,9 +4,9 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; /// -/// Proto 常量 +/// Proto 类型常量 /// -internal sealed partial class QrCodeQueryPayload +internal sealed partial class GameLoginResultPayload { public const string ACCOUNT = "Account"; public const string RAW = "Raw"; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.cs similarity index 77% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.cs index e66b64ba..8d7cd653 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeQueryPayload.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.cs @@ -3,8 +3,11 @@ namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; +/// +/// 扫码登录结果Payload +/// [HighQuality] -internal sealed partial class QrCodeQueryPayload +internal sealed partial class GameLoginResultPayload { [JsonPropertyName("proto")] public string Proto { get; set; } = default!; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs new file mode 100644 index 00000000..9f95988f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs @@ -0,0 +1,14 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Web.Hoyolab.Passport; + +/// +/// 扫码状态 +/// +internal enum GameLoginResultStatus +{ + Init, + Scanned, + Confirmed, +} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs index f6e3713f..be261a77 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs @@ -68,6 +68,47 @@ internal sealed partial class PassportClient2 return Response.Response.DefaultIfNull(resp); } + /// + /// 异步获取扫码链接 + /// + /// 取消令牌 + /// 二维码原始链接 + public async ValueTask> PostQrCodeFetchAsync(CancellationToken token = default) + { + GameLoginRequestOptions options = GameLoginRequestOptions.Create(4, HoyolabOptions.Device); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeFetch) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + + /// + /// 异步获取扫码状态 + /// + /// 扫码链接中的ticket + /// 取消令牌 + /// 扫码结果 + public async ValueTask> PostQrCodeQueryAsync(string ticket, CancellationToken token = default) + { + GameLoginResultOptions options = GameLoginResultOptions.Create(4, HoyolabOptions.Device, ticket); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeQuery) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + private class Timestamp { [JsonPropertyName("t")] diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UidGameToken.cs similarity index 63% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UidGameToken.cs index 9c48cd45..7f2ffc47 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/QrCode/QrCodeAccount.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UidGameToken.cs @@ -1,13 +1,13 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; +namespace Snap.Hutao.Web.Hoyolab.Passport; [HighQuality] -internal sealed class QrCodeAccount +internal sealed class UidGameToken { [JsonPropertyName("uid")] - public string Stuid { get; set; } = default!; + public string Uid { get; set; } = default!; [JsonPropertyName("token")] public string GameToken { get; set; } = default!; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs index 6cd5b0bf..57f1c63d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs @@ -2,7 +2,6 @@ // Licensed under the MIT license. using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; -using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; using Snap.Hutao.Web.Hoyolab.Passport; using Snap.Hutao.Web.Request.Builder; using Snap.Hutao.Web.Request.Builder.Abstraction; @@ -27,16 +26,16 @@ internal sealed partial class SessionAppClient /// 扫码获得的账户信息 /// 取消令牌 /// 登录结果 - public async ValueTask> PostSTokenByGameTokenAsync(QrCodeAccount account, CancellationToken token = default) + public async ValueTask> PostSTokenByGameTokenAsync(UidGameToken account, CancellationToken token = default) { GameTokenWrapper wrapper = new() { - Stuid = int.Parse(account.Stuid, CultureInfo.CurrentCulture), + Stuid = int.Parse(account.Uid, CultureInfo.CurrentCulture), GameToken = account.GameToken, }; HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(ApiEndpoints.STokenByGameToken) + .SetRequestUri(ApiEndpoints.AccountGetSTokenByGameToken) .PostJson(wrapper); Response? resp = await builder diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs index d1f701b1..5725680a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs @@ -84,6 +84,11 @@ internal enum KnownReturnCode /// AppIdError = -109, + /// + /// 二维码已过期 + /// + QrCodeExpired = -106, + /// /// 验证密钥过期 /// @@ -138,9 +143,4 @@ internal enum KnownReturnCode /// 实时便笺 /// CODE10104 = 10104, - - /// - /// 二维码已过期 - /// - QrCodeExpired = -106, } From 217586fece779c2add1719a52be6858c88b1764f Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Mon, 11 Dec 2023 22:55:47 +0800 Subject: [PATCH 3/5] Device needs rework --- .../Snap.Hutao/Control/Theme/Glyph.xaml | 3 +- ...EnumerableExtension.NameValueCollection.cs | 2 +- .../IQRCodeFactory.cs} | 6 +- .../QRCodeFactory.cs} | 6 +- .../Snap.Hutao/Resource/Localization/SH.resx | 4 +- .../GachaLogQueryManualInputProvider.cs | 2 +- src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 4 +- .../View/Dialog/QrCodeDialog.xaml.cs | 112 ------------- ...rCodeDialog.xaml => UserQRCodeDialog.xaml} | 11 +- .../View/Dialog/UserQRCodeDialog.xaml.cs | 156 ++++++++++++++++++ src/Snap.Hutao/Snap.Hutao/View/UserView.xaml | 2 +- .../ViewModel/User/UserViewModel.cs | 4 +- src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs | 18 +- .../Web/Hoyolab/Passport/GameLoginResult.cs | 4 +- .../GameLoginResultPayload.ProtoTypes.cs | 13 -- .../Hoyolab/Passport/GameLoginResultStatus.cs | 14 -- .../Web/Hoyolab/Passport/PassportClient2.cs | 8 +- ...ameLoginRequestResult.cs => UrlWrapper.cs} | 6 +- 18 files changed, 194 insertions(+), 181 deletions(-) rename src/Snap.Hutao/Snap.Hutao/Factory/{QrCode/IQrCodeFactory.cs => QuickResponse/IQRCodeFactory.cs} (60%) rename src/Snap.Hutao/Snap.Hutao/Factory/{QrCode/QrCodeFactory.cs => QuickResponse/QRCodeFactory.cs} (76%) delete mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs rename src/Snap.Hutao/Snap.Hutao/View/Dialog/{QrCodeDialog.xaml => UserQRCodeDialog.xaml} (69%) create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/{GameLoginRequestResult.cs => UrlWrapper.cs} (64%) diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml index d658fc29..4f8f0190 100644 --- a/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml +++ b/src/Snap.Hutao/Snap.Hutao/Control/Theme/Glyph.xaml @@ -1,4 +1,5 @@ + @@ -12,10 +13,10 @@ + - \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.NameValueCollection.cs b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.NameValueCollection.cs index 9d3840c0..2f4bafda 100644 --- a/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.NameValueCollection.cs +++ b/src/Snap.Hutao/Snap.Hutao/Extension/EnumerableExtension.NameValueCollection.cs @@ -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)) { diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/IQRCodeFactory.cs similarity index 60% rename from src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs rename to src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/IQRCodeFactory.cs index 0ab732fa..8ba3ea82 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/IQrCodeFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/IQRCodeFactory.cs @@ -3,7 +3,7 @@ namespace Snap.Hutao.Factory.QrCode; -internal interface IQrCodeFactory +internal interface IQRCodeFactory { - byte[] CreateQrCodeByteArray(string source); -} + byte[] Create(string source); +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/QRCodeFactory.cs similarity index 76% rename from src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs rename to src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/QRCodeFactory.cs index 4ee567fc..fb28e857 100644 --- a/src/Snap.Hutao/Snap.Hutao/Factory/QrCode/QrCodeFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Factory/QuickResponse/QRCodeFactory.cs @@ -6,10 +6,10 @@ using QRCoder; namespace Snap.Hutao.Factory.QrCode; [ConstructorGenerated] -[Injection(InjectAs.Singleton, typeof(IQrCodeFactory))] -internal class QrCodeFactory : IQrCodeFactory +[Injection(InjectAs.Singleton, typeof(IQRCodeFactory))] +internal class QRCodeFactory : IQRCodeFactory { - public byte[] CreateQrCodeByteArray(string source) + public byte[] Create(string source) { using (QRCodeGenerator generator = new()) { diff --git a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx index 42907257..bcbcfdb2 100644 --- a/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx +++ b/src/Snap.Hutao/Snap.Hutao/Resource/Localization/SH.resx @@ -1265,7 +1265,7 @@ 正在转换客户端 - + 使用米游社扫描二维码 @@ -2621,7 +2621,7 @@ 网页登录 - + 扫码登录 diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs index 9e7b98fc..45be4b6b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryManualInputProvider.cs @@ -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)) diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 7f248f3e..10c37a81 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -173,8 +173,8 @@ - + @@ -332,7 +332,7 @@ - + MSBuild:Compile diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs deleted file mode 100644 index 235f766b..00000000 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml.cs +++ /dev/null @@ -1,112 +0,0 @@ -// 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.Imaging; -using Snap.Hutao.Factory.QrCode; -using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; -using Snap.Hutao.Web.Hoyolab.Passport; -using Snap.Hutao.Web.Response; -using System.IO; -using System.Text.RegularExpressions; - -namespace Snap.Hutao.View.Dialog; - -/// -/// 扫描二维码对话框 -/// -[HighQuality] -internal sealed partial class QrCodeDialog : ContentDialog -{ - private readonly ITaskContext taskContext; - private readonly PassportClient2 passportClient2; - private readonly IQrCodeFactory qrCodeFactory; - - private UidGameToken? account; - - /// - /// 构造一个新的扫描二维码对话框 - /// - /// 服务提供器 - public QrCodeDialog(IServiceProvider serviceProvider) - { - InitializeComponent(); - - taskContext = serviceProvider.GetRequiredService(); - passportClient2 = serviceProvider.GetRequiredService(); - qrCodeFactory = serviceProvider.GetRequiredService(); - - FetchQrCodeAsync().SafeForget(); - } - - /// - /// 获取登录的用户 - /// - /// QrCodeAccount - [SuppressMessage("", "SH007")] - public async ValueTask> GetAccountAsync() - { - await taskContext.SwitchToMainThreadAsync(); - ContentDialogResult result = await ShowAsync(); - return new(account is not null, account!); - } - - private async ValueTask FetchQrCodeAsync() - { - Response fetch = await passportClient2.PostQrCodeFetchAsync().ConfigureAwait(false); - if (fetch.IsOk()) - { - string url = Regex.Unescape(fetch.Data.Url); - string ticket = url.ToUri().Query.Split('&').Last().Split('=').Last(); - - await taskContext.SwitchToMainThreadAsync(); - BitmapImage bitmap = new(); - await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.CreateQrCodeByteArray(url)).AsRandomAccessStream()); - - ImageView.Source = bitmap; - if (bitmap is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 }) - { - VisualStateManager.GoToState(this, "Loaded", true); - } - - await taskContext.SwitchToBackgroundAsync(); - await CheckStatusAsync(ticket).ConfigureAwait(false); - } - } - - private async ValueTask CheckStatusAsync(string ticket) - { - using (PeriodicTimer timer = new(TimeSpan.FromSeconds(3))) - { - while (await timer.WaitForNextTickAsync().ConfigureAwait(false)) - { - Response query = await passportClient2.PostQrCodeQueryAsync(ticket).ConfigureAwait(false); - if (query.IsOk(false)) - { - switch (query.Data.Stat) - { - case GameLoginResultStatus.Init: - case GameLoginResultStatus.Scanned: - break; // @switch - case GameLoginResultStatus.Confirmed: - if (query.Data.Payload.Proto == GameLoginResultPayload.ACCOUNT) - { - account = JsonSerializer.Deserialize(query.Data.Payload.Raw); - await taskContext.SwitchToMainThreadAsync(); - Hide(); - return; // Stop timer - } - - break; // @switch - } - } - else if (query.ReturnCode == (int)KnownReturnCode.QrCodeExpired) - { - FetchQrCodeAsync().SafeForget(); - break; // @while - } - } - } - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml similarity index 69% rename from src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml rename to src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml index 177a814c..62e88581 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/QrCodeDialog.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml @@ -1,11 +1,12 @@ + Width="320" + Height="320" + Source="{x:Bind QRCodeSource}"/> diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs new file mode 100644 index 00000000..a9f55887 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs @@ -0,0 +1,156 @@ +// 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.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 PassportClient2 passportClient2; + private readonly IQRCodeFactory qrCodeFactory; + + private readonly CancellationTokenSource userManualCancellationTokenSource = new(); + private bool disposed; + + public UserQRCodeDialog(IServiceProvider serviceProvider) + { + InitializeComponent(); + + taskContext = serviceProvider.GetRequiredService(); + passportClient2 = serviceProvider.GetRequiredService(); + qrCodeFactory = serviceProvider.GetRequiredService(); + } + + ~UserQRCodeDialog() + { + Dispose(); + } + + public void Dispose() + { + if (!disposed) + { + userManualCancellationTokenSource.Dispose(); + disposed = true; + } + + GC.SuppressFinalize(this); + } + + public async ValueTask> GetUidGameTokenAsync() + { + try + { + return await GetUidGameTokenCoreAsync().ConfigureAwait(false); + } + finally + { + userManualCancellationTokenSource.Dispose(); + } + } + + [Command("CancelCommand")] + private void Cancel() + { + userManualCancellationTokenSource.Cancel(); + } + + private async ValueTask> 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 FetchQRCodeAndSetImageAsync(CancellationToken token) + { + Response fetchResponse = await passportClient2.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 urlSpan) + { + ReadOnlySpan 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 WaitQueryQRCodeConfirmAsync(string ticket, CancellationToken token) + { + using (PeriodicTimer timer = new(new(0, 0, 3))) + { + while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false)) + { + Response query = await passportClient2.QRCodeQueryAsync(ticket, token).ConfigureAwait(false); + + if (query is { ReturnCode: 0, Data: { Stat: "Confirmed", Payload.Proto: "Account" } }) + { + UidGameToken? uidGameToken = JsonSerializer.Deserialize(query.Data.Payload.Raw); + ArgumentNullException.ThrowIfNull(uidGameToken); + return uidGameToken; + } + else if (query.ReturnCode == (int)KnownReturnCode.QrCodeExpired) + { + break; + } + } + } + + return null; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml index d58233b3..bff2ca0b 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml @@ -160,7 +160,7 @@ Margin="2,-4" Command="{Binding LoginByQrCodeCommand}" Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentQrCode}}" - Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginQrCodeAction}"/> + Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginQRCodeAction}"/> ().ConfigureAwait(false); - ValueResult result = await dialog.GetAccountAsync().ConfigureAwait(false); + UserQRCodeDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); + ValueResult result = await dialog.GetUidGameTokenAsync().ConfigureAwait(false); if (result.TryGetValue(out UidGameToken account)) { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs index 95032e1f..2774436b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs @@ -283,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 /// @@ -363,14 +371,6 @@ internal static class ApiEndpoints // https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api/content?key=eYd89JmJ&language=zh-cn&launcher_id=18 #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 Hosts | Queries private const string ApiTakumi = "https://api-takumi.mihoyo.com"; private const string ApiTakumiAuthApi = $"{ApiTakumi}/auth/api"; @@ -418,4 +418,4 @@ internal static class ApiEndpoints private const string AnnouncementQuery = "game=hk4e&game_biz=hk4e_cn&lang=zh-cn&bundle_id=hk4e_cn&platform=pc®ion=cn_gf01&level=55&uid=100000000"; #endregion -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs index c3757f69..bd52a99c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs @@ -1,7 +1,6 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Core.Json.Annotation; using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; namespace Snap.Hutao.Web.Hoyolab.Passport; @@ -13,8 +12,7 @@ namespace Snap.Hutao.Web.Hoyolab.Passport; internal sealed class GameLoginResult { [JsonPropertyName("stat")] - [JsonEnum(JsonSerializeType.String)] - public GameLoginResultStatus Stat { get; set; } = default!; + public string Stat { get; set; } = default!; [JsonPropertyName("payload")] public GameLoginResultPayload Payload { get; set; } = default!; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs deleted file mode 100644 index 179b7501..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.ProtoTypes.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -/// -/// Proto 类型常量 -/// -internal sealed partial class GameLoginResultPayload -{ - public const string ACCOUNT = "Account"; - public const string RAW = "Raw"; -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs deleted file mode 100644 index 9f95988f..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultStatus.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Passport; - -/// -/// 扫码状态 -/// -internal enum GameLoginResultStatus -{ - Init, - Scanned, - Confirmed, -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs index be261a77..8516723b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs @@ -73,7 +73,7 @@ internal sealed partial class PassportClient2 /// /// 取消令牌 /// 二维码原始链接 - public async ValueTask> PostQrCodeFetchAsync(CancellationToken token = default) + public async ValueTask> QRCodeFetchAsync(CancellationToken token = default) { GameLoginRequestOptions options = GameLoginRequestOptions.Create(4, HoyolabOptions.Device); @@ -81,8 +81,8 @@ internal sealed partial class PassportClient2 .SetRequestUri(ApiEndpoints.QrCodeFetch) .PostJson(options); - Response? resp = await builder - .TryCatchSendAsync>(httpClient, logger, token) + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) .ConfigureAwait(false); return Response.Response.DefaultIfNull(resp); @@ -94,7 +94,7 @@ internal sealed partial class PassportClient2 /// 扫码链接中的ticket /// 取消令牌 /// 扫码结果 - public async ValueTask> PostQrCodeQueryAsync(string ticket, CancellationToken token = default) + public async ValueTask> QRCodeQueryAsync(string ticket, CancellationToken token = default) { GameLoginResultOptions options = GameLoginResultOptions.Create(4, HoyolabOptions.Device, ticket); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestResult.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UrlWrapper.cs similarity index 64% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestResult.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UrlWrapper.cs index 13f067b2..bda18383 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UrlWrapper.cs @@ -3,11 +3,7 @@ namespace Snap.Hutao.Web.Hoyolab.Passport; -/// -/// 扫码登录请求结果 -/// -[HighQuality] -internal sealed class GameLoginRequestResult +internal sealed class UrlWrapper { [JsonPropertyName("url")] public string Url { get; set; } = default!; From f4547b60de6e1b81bc604eddde47a4d9e9804bb5 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Tue, 12 Dec 2023 14:22:15 +0800 Subject: [PATCH 4/5] completing --- .../IocHttpClientConfiguration.cs | 6 +- src/Snap.Hutao/Snap.Hutao/Core/Random.cs | 5 -- src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs | 61 +++++++++++++++++++ .../Service/User/UserFingerprintService.cs | 2 +- .../View/Dialog/UserQRCodeDialog.xaml.cs | 9 +-- src/Snap.Hutao/Snap.Hutao/View/UserView.xaml | 2 +- .../ViewModel/User/UserViewModel.cs | 33 +++++----- .../Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs | 2 +- .../Sdk/Combo/GameLoginRequest.cs} | 13 ++-- .../Sdk/Combo}/GameLoginResult.cs | 4 +- .../Sdk/Combo}/GameLoginResultPayload.cs | 5 +- .../Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs | 49 +++++++++++++++ .../Sdk/Combo}/UrlWrapper.cs | 2 +- .../Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs | 22 +++++-- .../AccountIdGameToken.cs} | 6 +- .../Passport/GameLoginRequestOptions.cs | 26 -------- .../Web/Hoyolab/Passport/PassportClient2.cs | 44 ++++--------- .../Takumi/Account/SessionAppClient.cs | 47 -------------- 18 files changed, 177 insertions(+), 161 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Passport/GameLoginResultOptions.cs => Hk4e/Sdk/Combo/GameLoginRequest.cs} (55%) rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Passport => Hk4e/Sdk/Combo}/GameLoginResult.cs (81%) rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Passport => Hk4e/Sdk/Combo}/GameLoginResultPayload.cs (79%) create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Passport => Hk4e/Sdk/Combo}/UrlWrapper.cs (80%) rename src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/{Takumi/Account/GameTokenWrapper.cs => Passport/AccountIdGameToken.cs} (62%) delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs delete mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs index 2a20113f..1d091b32 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs @@ -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); } /// @@ -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); } /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Random.cs b/src/Snap.Hutao/Snap.Hutao/Core/Random.cs index f1427404..74afc366 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Random.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Random.cs @@ -19,9 +19,4 @@ internal static class Random { return new(System.Random.Shared.GetItems("0123456789abcdefghijklmnopqrstuvwxyz".AsSpan(), length)); } - - public static string GetLetterAndNumberString(int length) - { - return new(System.Random.Shared.GetItems("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".AsSpan(), length)); - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs b/src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs new file mode 100644 index 00000000..b0d74b4f --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/Uuid.cs @@ -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 namespaceBuffer = stackalloc byte[16]; + Verify.Operation(namespaceId.TryWriteBytes(namespaceBuffer), "Failed to copy namespace guid bytes"); + Span nameBytes = Encoding.UTF8.GetBytes(name); + + if (BitConverter.IsLittleEndian) + { + ReverseEndianness(namespaceBuffer); + } + + Span data = stackalloc byte[namespaceBuffer.Length + nameBytes.Length]; + namespaceBuffer.CopyTo(data); + nameBytes.CopyTo(data[namespaceBuffer.Length..]); + + Span temp = stackalloc byte[20]; + Verify.Operation(SHA1.TryHashData(data, temp, out _), "Failed to compute SHA1 hash of UUID"); + + Span 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 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 guid, int left, int right) + { + (guid[right], guid[left]) = (guid[left], guid[right]); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs index 864346e5..a8a1f59e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserFingerprintService.cs @@ -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, }; diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs index a9f55887..dcbaa0fc 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/UserQRCodeDialog.xaml.cs @@ -6,6 +6,7 @@ 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; @@ -18,7 +19,7 @@ namespace Snap.Hutao.View.Dialog; internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable { private readonly ITaskContext taskContext; - private readonly PassportClient2 passportClient2; + private readonly PandaClient pandaClient; private readonly IQRCodeFactory qrCodeFactory; private readonly CancellationTokenSource userManualCancellationTokenSource = new(); @@ -29,7 +30,7 @@ internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable InitializeComponent(); taskContext = serviceProvider.GetRequiredService(); - passportClient2 = serviceProvider.GetRequiredService(); + pandaClient = serviceProvider.GetRequiredService(); qrCodeFactory = serviceProvider.GetRequiredService(); } @@ -100,7 +101,7 @@ internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable private async ValueTask FetchQRCodeAndSetImageAsync(CancellationToken token) { - Response fetchResponse = await passportClient2.QRCodeFetchAsync(token).ConfigureAwait(false); + Response fetchResponse = await pandaClient.QRCodeFetchAsync(token).ConfigureAwait(false); if (!fetchResponse.IsOk()) { return string.Empty; @@ -136,7 +137,7 @@ internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable { while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false)) { - Response query = await passportClient2.QRCodeQueryAsync(ticket, token).ConfigureAwait(false); + Response query = await pandaClient.QRCodeQueryAsync(ticket, token).ConfigureAwait(false); if (query is { ReturnCode: 0, Data: { Stat: "Confirmed", Payload.Proto: "Account" } }) { diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml index bff2ca0b..eb88a523 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml @@ -158,7 +158,7 @@ Width="{StaticResource LargeAppBarButtonWidth}" MaxWidth="{StaticResource LargeAppBarButtonWidth}" Margin="2,-4" - Command="{Binding LoginByQrCodeCommand}" + Command="{Binding LoginByQRCodeCommand}" Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentQrCode}}" Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginQRCodeAction}"/> ? users; @@ -177,26 +175,29 @@ internal sealed partial class UserViewModel : ObservableObject } } - [Command("LoginByQrCodeCommand")] - private async Task LoginByQrCode() + [Command("LoginByQRCodeCommand")] + private async Task LoginByQRCode() { - // ContentDialog must be created by main thread. - await taskContext.SwitchToMainThreadAsync(); - UserQRCodeDialog dialog = await contentDialogFactory.CreateInstanceAsync().ConfigureAwait(false); - ValueResult result = await dialog.GetUidGameTokenAsync().ConfigureAwait(false); + (bool isOk, UidGameToken? token) = await dialog.GetUidGameTokenAsync().ConfigureAwait(false); - if (result.TryGetValue(out UidGameToken account)) + if (!isOk) { - Response gameTokenResp = await sessionAppClient.PostSTokenByGameTokenAsync(account).ConfigureAwait(false); + return; + } - if (gameTokenResp.IsOk()) - { - Cookie stokenV2 = Cookie.FromLoginResult(gameTokenResp.Data); - (UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(stokenV2, false).ConfigureAwait(false); + Response sTokenResponse = await serviceProvider + .GetRequiredService() + .GetSTokenByGameTokenAsync(token) + .ConfigureAwait(false); - await HandleUserOptionResultAsync(optionResult, uid).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); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs index d9992754..3466901d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Bridge/MiHoYoJSBridge.cs @@ -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" }, }; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginRequest.cs similarity index 55% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginRequest.cs index d5f17cd6..479d4218 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginRequest.cs @@ -1,13 +1,10 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hoyolab.Passport; +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; -/// -/// 扫码登录结果请求配置 -/// [HighQuality] -internal sealed class GameLoginResultOptions +internal sealed class GameLoginRequest { [JsonPropertyName("app_id")] public int AppId { get; set; } @@ -16,11 +13,11 @@ internal sealed class GameLoginResultOptions public string Device { get; set; } = default!; [JsonPropertyName("ticket")] - public string Ticket { get; set; } = default!; + public string? Ticket { get; set; } - public static GameLoginResultOptions Create(int appId, string device, string ticket) + public static GameLoginRequest Create(int appId, string device, string? ticket = null) { - return new GameLoginResultOptions + return new() { AppId = appId, Device = device, diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResult.cs similarity index 81% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResult.cs index bd52a99c..92e6456e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResult.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResult.cs @@ -1,9 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -using Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; - -namespace Snap.Hutao.Web.Hoyolab.Passport; +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; /// /// 扫码登录结果 diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResultPayload.cs similarity index 79% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResultPayload.cs index 8d7cd653..80580331 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginResultPayload.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/GameLoginResultPayload.cs @@ -1,11 +1,8 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hoyolab.Hk4e.QrCode; +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; -/// -/// 扫码登录结果Payload -/// [HighQuality] internal sealed partial class GameLoginResultPayload { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs new file mode 100644 index 00000000..90ec1cfc --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/PandaClient.cs @@ -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 logger; + private readonly HttpClient httpClient; + + public async ValueTask> QRCodeFetchAsync(CancellationToken token = default) + { + GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeFetch) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } + + public async ValueTask> QRCodeQueryAsync(string ticket, CancellationToken token = default) + { + GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40, ticket); + + HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() + .SetRequestUri(ApiEndpoints.QrCodeQuery) + .PostJson(options); + + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UrlWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/UrlWrapper.cs similarity index 80% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UrlWrapper.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/UrlWrapper.cs index bda18383..9a962e6a 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/UrlWrapper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Hk4e/Sdk/Combo/UrlWrapper.cs @@ -1,7 +1,7 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hoyolab.Passport; +namespace Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo; internal sealed class UrlWrapper { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs index 4610b2c9..ad3dff78 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs @@ -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; /// /// 米游社选项 /// -internal sealed class HoyolabOptions : IOptions +internal static class HoyolabOptions { /// /// 米游社请求UA @@ -35,12 +35,12 @@ internal sealed class HoyolabOptions : IOptions /// /// 米游社设备Id /// - public static string DeviceId { get; } = Guid.NewGuid().ToString(); + public static string DeviceId36 { get; } = Guid.NewGuid().ToString(); /// /// 扫码登录设备Id /// - public static string Device { get; } = Core.Random.GetLetterAndNumberString(64); + public static string DeviceId40 { get; } = GenerateDeviceId40(); /// /// 盐 @@ -61,6 +61,16 @@ internal sealed class HoyolabOptions : IOptions [SaltType.OSX6] = "okr4obncj8bw5a65hbnn5oo6ixjc3l9w", }.ToFrozenDictionary(); - /// - public HoyolabOptions Value { get => this; } + [SuppressMessage("", "CA1308")] + private static string GenerateDeviceId40() + { + Guid uuid = Core.Uuid.NewV5(DeviceId36, new("9450ea74-be9c-35c0-9568-f97407856768")); + + Span uuidSpan = stackalloc byte[16]; + Span 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(); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/AccountIdGameToken.cs similarity index 62% rename from src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs rename to src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/AccountIdGameToken.cs index 1b62d7cb..0c76cc52 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/GameTokenWrapper.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/AccountIdGameToken.cs @@ -1,12 +1,12 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. -namespace Snap.Hutao.Web.Hoyolab.Takumi.Account; +namespace Snap.Hutao.Web.Hoyolab.Passport; -internal sealed class GameTokenWrapper +internal sealed class AccountIdGameToken { [JsonPropertyName("account_id")] - public int Stuid { get; set; } = default!; + public int AccountId { get; set; } = default!; [JsonPropertyName("game_token")] public string GameToken { get; set; } = default!; diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs deleted file mode 100644 index fb3b77a4..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/GameLoginRequestOptions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -namespace Snap.Hutao.Web.Hoyolab.Passport; - -/// -/// 扫码登录请求配置 -/// -[HighQuality] -internal sealed class GameLoginRequestOptions -{ - [JsonPropertyName("app_id")] - public int AppId { get; set; } - - [JsonPropertyName("device")] - public string Device { get; set; } = default!; - - public static GameLoginRequestOptions Create(int appId, string device) - { - return new GameLoginRequestOptions - { - AppId = appId, - Device = device, - }; - } -} diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs index 8516723b..be1ad870 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/PassportClient2.cs @@ -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,42 +70,20 @@ internal sealed partial class PassportClient2 return Response.Response.DefaultIfNull(resp); } - /// - /// 异步获取扫码链接 - /// - /// 取消令牌 - /// 二维码原始链接 - public async ValueTask> QRCodeFetchAsync(CancellationToken token = default) + public async ValueTask> GetSTokenByGameTokenAsync(UidGameToken account, CancellationToken token = default) { - GameLoginRequestOptions options = GameLoginRequestOptions.Create(4, HoyolabOptions.Device); + AccountIdGameToken data = new() + { + AccountId = int.Parse(account.Uid, CultureInfo.InvariantCulture), + GameToken = account.GameToken, + }; HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(ApiEndpoints.QrCodeFetch) - .PostJson(options); + .SetRequestUri(ApiEndpoints.AccountGetSTokenByGameToken) + .PostJson(data); - Response? resp = await builder - .TryCatchSendAsync>(httpClient, logger, token) - .ConfigureAwait(false); - - return Response.Response.DefaultIfNull(resp); - } - - /// - /// 异步获取扫码状态 - /// - /// 扫码链接中的ticket - /// 取消令牌 - /// 扫码结果 - public async ValueTask> QRCodeQueryAsync(string ticket, CancellationToken token = default) - { - GameLoginResultOptions options = GameLoginResultOptions.Create(4, HoyolabOptions.Device, ticket); - - HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(ApiEndpoints.QrCodeQuery) - .PostJson(options); - - Response? resp = await builder - .TryCatchSendAsync>(httpClient, logger, token) + Response? resp = await builder + .TryCatchSendAsync>(httpClient, logger, token) .ConfigureAwait(false); return Response.Response.DefaultIfNull(resp); diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs deleted file mode 100644 index 57f1c63d..00000000 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Account/SessionAppClient.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) DGP Studio. All rights reserved. -// Licensed under the MIT license. - -using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; -using Snap.Hutao.Web.Hoyolab.Passport; -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.Takumi.Account; - -[HighQuality] -[ConstructorGenerated(ResolveHttpClient = true)] -[HttpClient(HttpClientConfiguration.XRpc2)] -internal sealed partial class SessionAppClient -{ - private readonly IHttpRequestMessageBuilderFactory httpRequestMessageBuilderFactory; - private readonly ILogger logger; - private readonly HttpClient httpClient; - - /// - /// 通过 GameToken 获取 SToken (V2) - /// - /// 扫码获得的账户信息 - /// 取消令牌 - /// 登录结果 - public async ValueTask> PostSTokenByGameTokenAsync(UidGameToken account, CancellationToken token = default) - { - GameTokenWrapper wrapper = new() - { - Stuid = int.Parse(account.Uid, CultureInfo.CurrentCulture), - GameToken = account.GameToken, - }; - - HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create() - .SetRequestUri(ApiEndpoints.AccountGetSTokenByGameToken) - .PostJson(wrapper); - - Response? resp = await builder - .TryCatchSendAsync>(httpClient, logger, token) - .ConfigureAwait(false); - - return Response.Response.DefaultIfNull(resp); - } -} From ad20b83b4eab3c8ef1f6573eb02da6b44eb27e79 Mon Sep 17 00:00:00 2001 From: Lightczx <1686188646@qq.com> Date: Tue, 12 Dec 2023 14:25:05 +0800 Subject: [PATCH 5/5] minor fix --- src/Snap.Hutao/Snap.Hutao/View/UserView.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml index eb88a523..8b119393 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml @@ -159,7 +159,7 @@ MaxWidth="{StaticResource LargeAppBarButtonWidth}" Margin="2,-4" Command="{Binding LoginByQRCodeCommand}" - Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentQrCode}}" + Icon="{shcm:FontIcon Glyph={StaticResource FontIconContentQRCode}}" Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginQRCodeAction}"/>