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.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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -225,6 +225,19 @@
|
|||||||
Icon="{shcm:FontIcon Glyph=}"
|
Icon="{shcm:FontIcon Glyph=}"
|
||||||
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=}"
|
||||||
|
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
|
||||||
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Flyout>
|
</Flyout>
|
||||||
</Button.Flyout>
|
</Button.Flyout>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user