mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Support adding hoyoverse accounts by cookie input
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Snap.Hutao.Migrations;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -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)
|
||||
{
|
||||
if (isInitialized)
|
||||
@@ -234,4 +273,49 @@ internal sealed class User : ObservableObject
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
@@ -65,4 +66,18 @@ internal sealed class User : ISelectable
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,13 @@ internal interface IUserService
|
||||
/// <returns>处理的结果</returns>
|
||||
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>
|
||||
/// 异步刷新 Cookie 的 CookieToken
|
||||
/// </summary>
|
||||
|
||||
@@ -225,7 +225,46 @@ internal class UserService : IUserService
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie)
|
||||
private async Task<ValueResult<UserOptionResult, string>> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea)
|
||||
{
|
||||
await ThreadHelper.SwitchToBackgroundAsync();
|
||||
using (IServiceScope scope = scopeFactory.CreateScope())
|
||||
{
|
||||
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)
|
||||
{
|
||||
// Sync cache
|
||||
|
||||
@@ -225,6 +225,19 @@
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
|
||||
</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=}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
|
||||
@@ -46,6 +46,7 @@ internal sealed class UserViewModel : ObservableObject
|
||||
|
||||
OpenUICommand = new AsyncRelayCommand(OpenUIAsync);
|
||||
AddUserCommand = new AsyncRelayCommand(AddUserAsync);
|
||||
AddOsUserCommand = new AsyncRelayCommand(AddOsUserAsync);
|
||||
LoginMihoyoUserCommand = new RelayCommand(LoginMihoyoUser);
|
||||
RemoveUserCommand = new AsyncRelayCommand<User>(RemoveUserAsync);
|
||||
CopyCookieCommand = new RelayCommand<User>(CopyCookie);
|
||||
@@ -87,6 +88,11 @@ internal sealed class UserViewModel : ObservableObject
|
||||
/// </summary>
|
||||
public ICommand AddUserCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 添加国际服用户命令
|
||||
/// </summary>
|
||||
public ICommand AddOsUserCommand { get; }
|
||||
|
||||
/// <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()
|
||||
{
|
||||
if (Core.WebView2Helper.IsSupported)
|
||||
|
||||
@@ -52,4 +52,22 @@ internal sealed class UserClient
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -156,6 +156,26 @@ internal sealed partial class Cookie
|
||||
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)"/>
|
||||
public bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
|
||||
{
|
||||
|
||||
@@ -52,4 +52,23 @@ internal sealed class BindingClient
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user