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!;