Support adding hoyoverse accounts by cookie input

This commit is contained in:
xhichn
2023-03-12 23:25:25 +08:00
parent 0295d4fc22
commit b89c66fd6b
9 changed files with 276 additions and 4 deletions

View File

@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Migrations;
using Snap.Hutao.Web.Hoyolab; using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Bbs.User; using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.Passport; using Snap.Hutao.Web.Hoyolab.Passport;
@@ -103,7 +104,17 @@ internal sealed class User : ObservableObject
internal static async Task<User> ResumeAsync(EntityUser inner, CancellationToken token = default) internal static async Task<User> ResumeAsync(EntityUser inner, CancellationToken token = default)
{ {
User user = new(inner); User user = new(inner);
bool isOk = await user.InitializeCoreAsync(token).ConfigureAwait(false); bool isOk = false;
// TODO: 这里暂时使用是否存在 stoken 来判断是否为国际服,需要改进
if (user.Entity.Stoken != null)
{
isOk = await user.InitializeCoreAsync(token).ConfigureAwait(false);
}
else
{
isOk = await user.InitializeCoreOsAsync(token).ConfigureAwait(false);
}
if (!isOk) if (!isOk)
{ {
@@ -141,6 +152,34 @@ internal sealed class User : ObservableObject
} }
} }
/// <summary>
/// 创建并初始化国际服用户(临时)
/// </summary>
/// <param name="cookie">cookie</param>
/// <param name="token">取消令牌</param>
/// <returns>用户</returns>
internal static async Task<User?> CreateOsUserAsync(Cookie cookie, CancellationToken token = default)
{
// 这里只负责创建实体用户,稍后在用户服务中保存到数据库
EntityUser entity = EntityUser.CreateOs(cookie);
// 临时使用 ltuid 代替 aid 与 mid
entity.Aid = cookie.GetValueOrDefault(Cookie.LTUID);
entity.Mid = cookie.GetValueOrDefault(Cookie.LTUID);
if (entity.Aid != null && entity.Mid != null)
{
User user = new(entity);
bool initialized = await user.InitializeCoreOsAsync(token).ConfigureAwait(false);
return initialized ? user : null;
}
else
{
return null;
}
}
private async Task<bool> InitializeCoreAsync(CancellationToken token = default) private async Task<bool> InitializeCoreAsync(CancellationToken token = default)
{ {
if (isInitialized) if (isInitialized)
@@ -234,4 +273,49 @@ internal sealed class User : ObservableObject
return UserInfo != null && UserGameRoles.Any(); return UserInfo != null && UserGameRoles.Any();
} }
private async Task<bool> InitializeCoreOsAsync(CancellationToken token = default)
{
if (isInitialized)
{
return true;
}
using (IServiceScope scope = Ioc.Default.CreateScope())
{
// 获取账户信息
Response<UserFullInfoWrapper> response = await scope.ServiceProvider
.GetRequiredService<UserClient>()
.GetOsUserFullInfoAsync(Entity, token)
.ConfigureAwait(false);
UserInfo = response.Data?.UserInfo;
// Ltoken 和 cookieToken 直接从网页或者输入获取
if (Ltoken == null || CookieToken == null)
{
return false;
}
// 获取游戏角色
Response<ListWrapper<UserGameRole>> userGameRolesResponse = await scope.ServiceProvider
.GetRequiredService<BindingClient>()
.GetOsUserGameRolesByCookieAsync(Entity, token)
.ConfigureAwait(false);
if (userGameRolesResponse.IsOk())
{
UserGameRoles = userGameRolesResponse.Data.List;
}
else
{
return false;
}
}
SelectedUserGameRole = UserGameRoles.FirstOrFirstOrDefault(role => role.IsChosen);
isInitialized = true;
return UserInfo != null && UserGameRoles.Any();
}
} }

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.Database; using Snap.Hutao.Core.Database;
using Snap.Hutao.Web.Hoyolab; using Snap.Hutao.Web.Hoyolab;
using System;
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
@@ -65,4 +66,18 @@ internal sealed class User : ISelectable
return new() { Stoken = stoken, Ltoken = ltoken, CookieToken = cookieToken }; return new() { Stoken = stoken, Ltoken = ltoken, CookieToken = cookieToken };
} }
/// <summary>
/// 创建一个国际服用户
/// </summary>
/// <param name="cookie">cookie</param>
/// <returns>新创建的用户</returns>
public static User CreateOs(Cookie cookie)
{
// 不需要 Stoken
_ = cookie.TryGetAsLtoken(out Cookie? ltoken);
_ = cookie.TryGetAsOsCookieToken(out Cookie? cookieToken);
return new() { Ltoken = ltoken, CookieToken = cookieToken };
}
} }

View File

@@ -48,6 +48,13 @@ internal interface IUserService
/// <returns>处理的结果</returns> /// <returns>处理的结果</returns>
Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie); Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie);
/// <summary>
/// 尝试异步处理输入的国际服 Cookie
/// </summary>
/// <param name="cookie">Cookie</param>
/// <returns>处理的结果</returns>
Task<ValueResult<UserOptionResult, string>> ProcessInputOsCookieAsync(Cookie cookie);
/// <summary> /// <summary>
/// 异步刷新 Cookie 的 CookieToken /// 异步刷新 Cookie 的 CookieToken
/// </summary> /// </summary>

View File

@@ -225,7 +225,46 @@ internal class UserService : IUserService
} }
else else
{ {
return await TryCreateUserAndAddAsync(cookie).ConfigureAwait(false); return await TryCreateUserAndAddAsync(cookie, false).ConfigureAwait(false);
}
}
/// <inheritdoc/>
public async Task<ValueResult<UserOptionResult, string>> ProcessInputOsCookieAsync(Cookie cookie)
{
await ThreadHelper.SwitchToBackgroundAsync();
string? ltuid = cookie.GetValueOrDefault(Cookie.LTUID);
if (ltuid == null)
{
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoMid);
}
// 检查 ltuid 对应用户是否存在
if (TryGetUser(userCollection!, ltuid, out BindingUser? user))
{
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
if (cookie.TryGetAsLtoken(out Cookie? ltoken))
{
user.Stoken = null;
user.Ltoken = user.Ltoken;
user.CookieToken = cookie.TryGetAsOsCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken;
await appDbContext.Users.UpdateAndSaveAsync(user.Entity).ConfigureAwait(false);
return new(UserOptionResult.Updated, ltuid);
}
else
{
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoStoken);
}
}
}
else
{
return await TryCreateUserAndAddAsync(cookie, true).ConfigureAwait(false);
} }
} }
@@ -265,14 +304,24 @@ internal class UserService : IUserService
return user != null; return user != null;
} }
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie) private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea)
{ {
await ThreadHelper.SwitchToBackgroundAsync(); await ThreadHelper.SwitchToBackgroundAsync();
using (IServiceScope scope = scopeFactory.CreateScope()) using (IServiceScope scope = scopeFactory.CreateScope())
{ {
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
BindingUser? newUser;
// 判断是否为国际服
if (isOversea)
{
newUser = await BindingUser.CreateOsUserAsync(cookie).ConfigureAwait(false);
}
else
{
newUser = await BindingUser.CreateAsync(cookie).ConfigureAwait(false);
}
BindingUser? newUser = await BindingUser.CreateAsync(cookie).ConfigureAwait(false);
if (newUser != null) if (newUser != null)
{ {
// Sync cache // Sync cache

View File

@@ -225,6 +225,19 @@
Icon="{shcm:FontIcon Glyph=&#xE710;}" Icon="{shcm:FontIcon Glyph=&#xE710;}"
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/> Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
</StackPanel> </StackPanel>
<TextBlock
Margin="10,6,0,6"
Style="{StaticResource BaseTextBlockStyle}"
Text="HoyoVerse Account"/>
<StackPanel
Margin="0,0,6,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<AppBarButton
Command="{Binding AddOsUserCommand}"
Icon="{shcm:FontIcon Glyph=&#xE710;}"
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
</StackPanel>
</StackPanel> </StackPanel>
</Flyout> </Flyout>
</Button.Flyout> </Button.Flyout>

View File

@@ -46,6 +46,7 @@ internal sealed class UserViewModel : ObservableObject
OpenUICommand = new AsyncRelayCommand(OpenUIAsync); OpenUICommand = new AsyncRelayCommand(OpenUIAsync);
AddUserCommand = new AsyncRelayCommand(AddUserAsync); AddUserCommand = new AsyncRelayCommand(AddUserAsync);
AddOsUserCommand = new AsyncRelayCommand(AddOsUserAsync);
LoginMihoyoUserCommand = new RelayCommand(LoginMihoyoUser); LoginMihoyoUserCommand = new RelayCommand(LoginMihoyoUser);
RemoveUserCommand = new AsyncRelayCommand<User>(RemoveUserAsync); RemoveUserCommand = new AsyncRelayCommand<User>(RemoveUserAsync);
CopyCookieCommand = new RelayCommand<User>(CopyCookie); CopyCookieCommand = new RelayCommand<User>(CopyCookie);
@@ -87,6 +88,11 @@ internal sealed class UserViewModel : ObservableObject
/// </summary> /// </summary>
public ICommand AddUserCommand { get; } public ICommand AddUserCommand { get; }
/// <summary>
/// 添加国际服用户命令
/// </summary>
public ICommand AddOsUserCommand { get; }
/// <summary> /// <summary>
/// 登录米游社命令 /// 登录米游社命令
/// </summary> /// </summary>
@@ -161,6 +167,47 @@ internal sealed class UserViewModel : ObservableObject
} }
} }
private async Task AddOsUserAsync()
{
// ContentDialog must be created by main thread.
await ThreadHelper.SwitchToMainThreadAsync();
// Get cookie from user input
ValueResult<bool, string> result = await new UserDialog().GetInputCookieAsync().ConfigureAwait(false);
// User confirms the input
if (result.IsOk)
{
Cookie cookie = Cookie.Parse(result.Value);
(UserOptionResult optionResult, string uid) = await userService.ProcessInputOsCookieAsync(cookie).ConfigureAwait(false);
switch (optionResult)
{
case UserOptionResult.Added:
if (Users!.Count == 1)
{
await ThreadHelper.SwitchToMainThreadAsync();
SelectedUser = Users.Single();
}
infoBarService.Success(string.Format(SH.ViewModelUserAdded, uid));
break;
case UserOptionResult.Incomplete:
infoBarService.Information(SH.ViewModelUserIncomplete);
break;
case UserOptionResult.Invalid:
infoBarService.Information(SH.ViewModelUserInvalid);
break;
case UserOptionResult.Updated:
infoBarService.Success(string.Format(SH.ViewModelUserUpdated, uid));
break;
default:
throw Must.NeverHappen();
}
}
}
private void LoginMihoyoUser() private void LoginMihoyoUser()
{ {
if (Core.WebView2Helper.IsSupported) if (Core.WebView2Helper.IsSupported)

View File

@@ -52,4 +52,22 @@ internal sealed class UserClient
return Response.Response.DefaultIfNull(resp); return Response.Response.DefaultIfNull(resp);
} }
/// <summary>
/// 获取当前用户详细信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
[ApiInformation(Cookie = CookieType.Stoken, Salt = SaltType.K2)]
public async Task<Response<UserFullInfoWrapper>> GetOsUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
{
Response<UserFullInfoWrapper>? resp = await httpClient
.SetUser(user, CookieType.Cookie)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OS, false)
.TryCatchGetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiOsEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
} }

View File

@@ -156,6 +156,26 @@ internal sealed partial class Cookie
return false; return false;
} }
public bool TryGetAsOsCookieToken([NotNullWhen(true)] out Cookie? cookie)
{
bool hasLtUid = TryGetValue(LTUID, out string? ltUid);
bool hasCookieToken = TryGetValue(COOKIE_TOKEN, out string? cookieToken);
if (hasLtUid && hasCookieToken)
{
cookie = new Cookie(new()
{
[LTUID] = ltUid!,
[COOKIE_TOKEN] = cookieToken!,
});
return true;
}
cookie = null;
return false;
}
/// <inheritdoc cref="Dictionary{TKey, TValue}.TryGetValue(TKey, out TValue)"/> /// <inheritdoc cref="Dictionary{TKey, TValue}.TryGetValue(TKey, out TValue)"/>
public bool TryGetValue(string key, [NotNullWhen(true)] out string? value) public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
{ {

View File

@@ -52,4 +52,23 @@ internal sealed class BindingClient
return Response.Response.DefaultIfNull(resp); return Response.Response.DefaultIfNull(resp);
} }
/// <summary>
/// 获取国际服用户角色信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns>
[ApiInformation(Cookie = CookieType.Ltoken)]
public async Task<Response<ListWrapper<UserGameRole>>> GetOsUserGameRolesByCookieAsync(User user, CancellationToken token = default)
{
string url = ApiOsEndpoints.UserGameRolesByCookie;
Response<ListWrapper<UserGameRole>>? resp = await httpClient
.SetUser(user, CookieType.Ltoken)
.TryCatchGetFromJsonAsync<Response<ListWrapper<UserGameRole>>>(url, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
} }