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, }