Skip to content

Commit

Permalink
welcome back qr login
Browse files Browse the repository at this point in the history
  • Loading branch information
qhy040404 committed Oct 19, 2024
1 parent 8f82f55 commit cde201a
Show file tree
Hide file tree
Showing 15 changed files with 186 additions and 46 deletions.
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 @@ -311,6 +311,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 @@ -478,4 +485,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 @@ -188,28 +188,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

0 comments on commit cde201a

Please sign in to comment.