Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

welcome back qr login #2060

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Snap.Hutao/Snap.Hutao/ApiEndpoints.csv
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ Name,CN,OS
AccountCreateActionTicket(),https://passport-api.mihoyo.com/account/ma-cn-verifier/app/createActionTicketByToken,
AccountCreateAuthTicketByGameBiz(),https://passport-api.mihoyo.com/account/ma-cn-verifier/app/createAuthTicketByGameBiz,https://sg-public-api.hoyoverse.com/account/ma-verifier/api/createAuthTicketBySToken
AccountCreateLoginCaptcha(),https://passport-api.mihoyo.com/account/ma-cn-verifier/verifier/createLoginCaptcha,
AccountCreateQrLogin(),https://passport-api.mihoyo.com/account/ma-cn-passport/app/createQRLogin,
AccountGetCookieTokenBySToken(),https://passport-api.mihoyo.com/account/auth/api/getCookieAccountInfoBySToken,https://api-account-os.hoyoverse.com/account/auth/api/getCookieAccountInfoBySToken
AccountGetLTokenBySToken(),https://passport-api.mihoyo.com/account/auth/api/getLTokenBySToken,https://api-account-os.hoyoverse.com/account/auth/api/getLTokenBySToken
AccountGetSTokenByGameToken(),https://passport-api.mihoyo.com/account/ma-cn-session/app/getTokenByGameToken,
AccountGetSTokenByOldToken(),https://passport-api.mihoyo.com/account/ma-cn-session/app/getTokenBySToken,
AccountLoginByMobileCaptcha(),https://passport-api.mihoyo.com/account/ma-cn-passport/app/loginByMobileCaptcha,
AccountLoginByPassword(),https://passport-api.mihoyo.com/account/ma-cn-passport/app/loginByPassword,https://sg-public-api.hoyoverse.com/account/ma-passport/api/appLoginByPassword
AccountLoginByThirdParty(),,https://sg-public-api.hoyoverse.com/account/ma-passport/api/appLoginByThirdParty
AccountQueryQrLoginStatus(),https://passport-api.mihoyo.com/account/ma-cn-passport/app/queryQRLoginStatus,
AccountVerifyLtoken(),https://passport-api-v4.mihoyo.com/account/ma-cn-session/web/verifyLtoken,
ActHoyolabReferer(),,https://act.hoyolab.com/
"AnnContent(string languageCode, Region region)","https://hk4e-ann-api.mihoyo.com/common/hk4e_cn/announcement/api/getAnnContent?{AnnouncementQuery(languageCode, region)}","https://sg-hk4e-api.hoyoverse.com/common/hk4e_global/announcement/api/getAnnContent?{AnnouncementQuery(languageCode, region)}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,20 @@
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Factory.QuickResponse;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Web.Hoyolab.Hk4e.Sdk.Combo;
using Snap.Hutao.Web.Hoyolab.Passport;
using Snap.Hutao.Web.Response;
using System.Collections.Specialized;
using System.IO;
using System.Web;

namespace Snap.Hutao.UI.Xaml.View.Dialog;

[ConstructorGenerated(InitializeComponent = true)]
[DependencyProperty("QRCodeSource", typeof(ImageSource))]
internal sealed partial class UserQRCodeDialog : ContentDialog, IDisposable
{
private readonly HoyoPlayPassportClient hoyoPlayPassportClient;
private readonly IInfoBarService infoBarService;
private readonly IQRCodeFactory qrCodeFactory;
private readonly ITaskContext taskContext;
private readonly PandaClient pandaClient;

private readonly CancellationTokenSource userManualCancellationTokenSource = new();
private bool disposed;
Expand All @@ -43,11 +40,11 @@ public void Dispose()
GC.SuppressFinalize(this);
}

public async ValueTask<ValueResult<bool, UidGameToken>> GetUidGameTokenAsync()
public async ValueTask<ValueResult<bool, QrLoginResult>> GetQrLoginResultAsync()
{
try
{
return await GetUidGameTokenCoreAsync().ConfigureAwait(false);
return await GetQrLoginResultCoreAsync().ConfigureAwait(false);
}
finally
{
Expand All @@ -61,7 +58,7 @@ private void Cancel()
userManualCancellationTokenSource.Cancel();
}

private async ValueTask<ValueResult<bool, UidGameToken>> GetUidGameTokenCoreAsync()
private async ValueTask<ValueResult<bool, QrLoginResult>> GetQrLoginResultCoreAsync()
{
await taskContext.SwitchToMainThreadAsync();
_ = ShowAsync();
Expand All @@ -72,7 +69,7 @@ private async ValueTask<ValueResult<bool, UidGameToken>> GetUidGameTokenCoreAsyn
{
CancellationToken token = userManualCancellationTokenSource.Token;
string ticket = await FetchQRCodeAndSetImageAsync(token).ConfigureAwait(false);
UidGameToken? uidGameToken = await WaitQueryQRCodeConfirmAsync(ticket, token).ConfigureAwait(false);
QrLoginResult? uidGameToken = await WaitQueryQRCodeConfirmAsync(ticket, token).ConfigureAwait(false);

if (uidGameToken is null)
{
Expand All @@ -94,47 +91,35 @@ private async ValueTask<ValueResult<bool, UidGameToken>> GetUidGameTokenCoreAsyn

private async ValueTask<string> FetchQRCodeAndSetImageAsync(CancellationToken token)
{
Response<UrlWrapper> fetchResponse = await pandaClient.QRCodeFetchAsync(token).ConfigureAwait(false);
if (!ResponseValidator.TryValidate(fetchResponse, infoBarService, out UrlWrapper? wrapper))
Response<QrLogin> qrLoginResponse = await hoyoPlayPassportClient.CreateQrLoginAsync(token).ConfigureAwait(false);
if (!ResponseValidator.TryValidate(qrLoginResponse, infoBarService, out QrLogin? qrLogin))
{
return string.Empty;
}

string url = wrapper.Url;
string ticket = GetTicketFromUrl(url);

await taskContext.SwitchToMainThreadAsync();

BitmapImage bitmap = new();
await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.Create(url)).AsRandomAccessStream());
await bitmap.SetSourceAsync(new MemoryStream(qrCodeFactory.Create(qrLogin.Url)).AsRandomAccessStream());
QRCodeSource = bitmap;

return ticket;

static string GetTicketFromUrl(in ReadOnlySpan<char> urlSpan)
{
ReadOnlySpan<char> querySpan = urlSpan[urlSpan.IndexOf('?')..];
NameValueCollection queryCollection = HttpUtility.ParseQueryString(querySpan.ToString());
return queryCollection.TryGetSingleValue("ticket", out string? ticket) ? ticket : string.Empty;
}
return qrLogin.Ticket;
}

private async ValueTask<UidGameToken?> WaitQueryQRCodeConfirmAsync(string ticket, CancellationToken token)
private async ValueTask<QrLoginResult?> WaitQueryQRCodeConfirmAsync(string ticket, CancellationToken token)
{
using (PeriodicTimer timer = new(new(0, 0, 3)))
{
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
{
Response<GameLoginResult> query = await pandaClient.QRCodeQueryAsync(ticket, token).ConfigureAwait(false);
Response<QrLoginResult> query = await hoyoPlayPassportClient.QueryQrLoginStatusAsync(ticket, token).ConfigureAwait(false);

if (query is { ReturnCode: 0, Data: { Stat: "Confirmed", Payload.Proto: "Account" } })
if (query is { ReturnCode: 0, Data: { Status: "Confirmed", Tokens: [{ TokenType: 1 }] } })
{
UidGameToken? uidGameToken = JsonSerializer.Deserialize<UidGameToken>(query.Data.Payload.Raw);
ArgumentNullException.ThrowIfNull(uidGameToken);
return uidGameToken;
return query.Data;
}

if (query.ReturnCode is (int)KnownReturnCode.QrCodeExpired)
if (query.ReturnCode is (int)KnownReturnCode.QRLoginExpired)
{
break;
}
Expand Down
9 changes: 8 additions & 1 deletion src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/UserView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,13 @@
<AppBarButton.Flyout>
<Flyout FlyoutPresenterStyle="{ThemeResource FlyoutPresenterPadding2Style}" Placement="Right">
<StackPanel Orientation="Horizontal" Spacing="0">
<AppBarButton
Width="{StaticResource LargeAppBarButtonWidth}"
MaxWidth="{StaticResource LargeAppBarButtonWidth}"
Margin="0,-4"
Command="{Binding LoginByQRCodeCommand}"
Icon="{shuxm:FontIcon Glyph={StaticResource FontIconContentQRCode}}"
Label="{shuxm:ResourceString Name=ViewUserCookieOperationLoginQRCodeAction}"/>
<AppBarButton
Width="{StaticResource LargeAppBarButtonWidth}"
MaxWidth="{StaticResource LargeAppBarButtonWidth}"
Expand Down Expand Up @@ -480,4 +487,4 @@

<NavigationViewItemSeparator/>
</StackPanel>
</UserControl>
</UserControl>
20 changes: 4 additions & 16 deletions src/Snap.Hutao/Snap.Hutao/ViewModel/User/UserViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,28 +189,16 @@ private void NavigateToLoginPage<TPage>()
private async Task LoginByQRCodeAsync()
{
UserQRCodeDialog dialog = await contentDialogFactory.CreateInstanceAsync<UserQRCodeDialog>().ConfigureAwait(false);
(bool isOk, UidGameToken? token) = await dialog.GetUidGameTokenAsync().ConfigureAwait(false);
(bool isOk, QrLoginResult? qrLoginResult) = await dialog.GetQrLoginResultAsync().ConfigureAwait(false);

if (!isOk)
{
return;
}

using (IServiceScope scope = serviceProvider.CreateScope())
{
Response<LoginResult> response = await scope.ServiceProvider
.GetRequiredService<IOverseaSupportFactory<IPassportClient>>()
.Create(false)
.LoginByGameTokenAsync(token)
.ConfigureAwait(false);

if (ResponseValidator.TryValidate(response, scope.ServiceProvider, out LoginResult? loginResult))
{
Cookie stokenV2 = Cookie.FromLoginResult(loginResult);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(InputCookie.CreateForDeviceFpInference(stokenV2, false)).ConfigureAwait(false);
HandleUserOptionResult(optionResult, uid);
}
}
Cookie stokenV2 = Cookie.FromQrLoginResult(qrLoginResult);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputCookieAsync(InputCookie.CreateForDeviceFpInference(stokenV2, false)).ConfigureAwait(false);
HandleUserOptionResult(optionResult, uid);
}

[Command("LoginByMobileCaptchaCommand")]
Expand Down
17 changes: 17 additions & 0 deletions src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ public static Cookie FromLoginResult(LoginResult? loginResult)
return new(cookieMap);
}

public static Cookie FromQrLoginResult(QrLoginResult? qrLoginResult)
{
if (qrLoginResult is null)
{
return new();
}

SortedDictionary<string, string> cookieMap = new()
{
[STUID] = qrLoginResult.UserInfo.Aid,
[STOKEN] = qrLoginResult.Tokens.Single(token => token.TokenType is 1).Token,
[MID] = qrLoginResult.UserInfo.Mid,
};

return new(cookieMap);
}

public static Cookie FromSToken(string stuid, string stoken)
{
SortedDictionary<string, string> cookieMap = new()
Expand Down
4 changes: 4 additions & 0 deletions src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/HoyolabOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Snap.Hutao.Web.Hoyolab.DataSigning;
using System.Collections.Frozen;
using System.Security.Cryptography;
using Random = Snap.Hutao.Core.Random;

namespace Snap.Hutao.Web.Hoyolab;

Expand Down Expand Up @@ -46,6 +47,9 @@ internal static class HoyolabOptions
/// </summary>
public static string DeviceId40 { get; } = GenerateDeviceId40();

// TODO: 53位设备Id
public static string DeviceId53 { get; } = Random.GetLowerAndNumberString(53);

/// <summary>
/// 盐
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,39 @@ public async ValueTask<Response<AuthTicketWrapper>> CreateAuthTicketAsync(User u
return Response.Response.DefaultIfNull(resp);
}

public async ValueTask<Response<QrLogin>> CreateQrLoginAsync(CancellationToken token = default)
{
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(apiEndpoints.AccountCreateQrLogin())
.SetHeader("x-rpc-device_id", HoyolabOptions.DeviceId53)
.PostJson(default(EmptyContent));

Response<QrLogin>? resp = await builder
.SendAsync<Response<QrLogin>>(httpClient, logger, token)
.ConfigureAwait(false);

return Response.Response.DefaultIfNull(resp);
}

public async ValueTask<Response<QrLoginResult>> QueryQrLoginStatusAsync(string ticket, CancellationToken token = default)
{
QrTicketWrapper data = new()
{
Ticket = ticket,
};

HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
.SetRequestUri(apiEndpoints.AccountQueryQrLoginStatus())
.SetHeader("x-rpc-device_id", HoyolabOptions.DeviceId53)
.PostJson(data);

Response<QrLoginResult>? resp = await builder
.SendAsync<Response<QrLoginResult>>(httpClient, logger, token)
.ConfigureAwait(false);

return Response.Response.DefaultIfNull(resp);
}

public ValueTask<(string? Aigis, Response<LoginResult> Response)> LoginByPasswordAsync(IPassportPasswordProvider provider, CancellationToken token = default)
{
return ValueTask.FromException<(string? Aigis, Response<LoginResult> Response)>(new NotSupportedException());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ public async ValueTask<Response<AuthTicketWrapper>> CreateAuthTicketAsync(User u
return Response.Response.DefaultIfNull(resp);
}

public ValueTask<Response<QrLogin>> CreateQrLoginAsync(CancellationToken token = default)
{
return ValueTask.FromException<Response<QrLogin>>(new NotSupportedException());
}

public ValueTask<Response<QrLoginResult>> QueryQrLoginStatusAsync(string ticket, CancellationToken token = default)
{
return ValueTask.FromException<Response<QrLoginResult>>(new NotSupportedException());
}

public ValueTask<(string? Aigis, Response<LoginResult> Response)> LoginByPasswordAsync(IPassportPasswordProvider provider, CancellationToken token = default)
{
ArgumentNullException.ThrowIfNull(provider.Account);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ internal interface IHoyoPlayPassportClient
{
ValueTask<Response<AuthTicketWrapper>> CreateAuthTicketAsync(User user, CancellationToken token = default);

ValueTask<Response<QrLogin>> CreateQrLoginAsync(CancellationToken token = default);

ValueTask<Response<QrLoginResult>> QueryQrLoginStatusAsync(string ticket, CancellationToken token = default);

ValueTask<(string? Aigis, Response<LoginResult> Response)> LoginByPasswordAsync(IPassportPasswordProvider provider, CancellationToken token = default);

ValueTask<(string? Aigis, Response<LoginResult> Response)> LoginByPasswordAsync(string account, string password, string? aigis, CancellationToken token = default);
Expand Down
13 changes: 13 additions & 0 deletions src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/QrLogin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.

namespace Snap.Hutao.Web.Hoyolab.Passport;

internal sealed class QrLogin
{
[JsonPropertyName("url")]
public string Url { get; set; } = default!;

[JsonPropertyName("ticket")]
public string Ticket { get; set; } = default!;
}
40 changes: 40 additions & 0 deletions src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/QrLoginResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.

namespace Snap.Hutao.Web.Hoyolab.Passport;

internal sealed class QrLoginResult
{
[JsonPropertyName("status")]
public string Status { get; set; } = default!;

[JsonPropertyName("app_id")]
public string AppId { get; set; } = default!;

[JsonPropertyName("client_type")]
public int ClientType { get; set; }

[JsonPropertyName("created_at")]
public string CreatedAt { get; set; } = default!;

[JsonPropertyName("scanned_at")]
public string ScannedAt { get; set; } = default!;

[JsonPropertyName("tokens")]
public List<TokenWrapper> Tokens { get; set; } = default!;

[JsonPropertyName("user_info")]
public UserInformation UserInfo { get; set; } = default!;

[JsonPropertyName("realname_info")]
public RealnameInfo RealnameInfo { get; set; } = default!;

[JsonPropertyName("need_realperson")]
public bool NeedRealperson { get; set; }

[JsonPropertyName("ext")]
public string Ext { get; set; } = default!;

[JsonPropertyName("scan_game_biz")]
public string ScanGameBiz { get; set; } = default!;
}
10 changes: 10 additions & 0 deletions src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/QrTicketWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.

namespace Snap.Hutao.Web.Hoyolab.Passport;

internal sealed class QrTicketWrapper
{
[JsonPropertyName("ticket")]
public string Ticket { get; set; } = default!;
}
16 changes: 16 additions & 0 deletions src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Passport/RealnameInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.

namespace Snap.Hutao.Web.Hoyolab.Passport;

internal sealed class RealnameInfo
{
[JsonPropertyName("required")]
public bool Required { get; set; }

[JsonPropertyName("action_type")]
public string ActionType { get; set; } = default!;

[JsonPropertyName("action_ticket")]
public string ActionTicket { get; set; } = default!;
}
6 changes: 6 additions & 0 deletions src/Snap.Hutao/Snap.Hutao/Web/Request/Builder/EmptyContent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.

namespace Snap.Hutao.Web.Request.Builder;

internal readonly struct EmptyContent;
Loading
Loading