diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/User/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/User/User.cs index e7427697..274137ec 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/User/User.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/User/User.cs @@ -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 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 } } + /// + /// 创建并初始化国际服用户(临时) + /// + /// cookie + /// 取消令牌 + /// 用户 + internal static async Task 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 InitializeCoreAsync(CancellationToken token = default) { if (isInitialized) @@ -234,4 +273,49 @@ internal sealed class User : ObservableObject return UserInfo != null && UserGameRoles.Any(); } + + private async Task InitializeCoreOsAsync(CancellationToken token = default) + { + if (isInitialized) + { + return true; + } + + using (IServiceScope scope = Ioc.Default.CreateScope()) + { + // 获取账户信息 + Response response = await scope.ServiceProvider + .GetRequiredService() + .GetOsUserFullInfoAsync(Entity, token) + .ConfigureAwait(false); + UserInfo = response.Data?.UserInfo; + + // Ltoken 和 cookieToken 直接从网页或者输入获取 + if (Ltoken == null || CookieToken == null) + { + return false; + } + + // 获取游戏角色 + Response> userGameRolesResponse = await scope.ServiceProvider + .GetRequiredService() + .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(); + } } diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs index 71d614a1..0451a335 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Entity/User.cs @@ -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 }; } + + /// + /// 创建一个国际服用户 + /// + /// cookie + /// 新创建的用户 + public static User CreateOs(Cookie cookie) + { + // 不需要 Stoken + _ = cookie.TryGetAsLtoken(out Cookie? ltoken); + _ = cookie.TryGetAsOsCookieToken(out Cookie? cookieToken); + + return new() { Ltoken = ltoken, CookieToken = cookieToken }; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs index 8546d4a8..e090d37b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs @@ -48,6 +48,13 @@ internal interface IUserService /// 处理的结果 Task> ProcessInputCookieAsync(Cookie cookie); + /// + /// 尝试异步处理输入的国际服 Cookie + /// + /// Cookie + /// 处理的结果 + Task> ProcessInputOsCookieAsync(Cookie cookie); + /// /// 异步刷新 Cookie 的 CookieToken /// diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs index 8d2ebc24..346daac3 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -225,7 +225,46 @@ internal class UserService : IUserService } else { - return await TryCreateUserAndAddAsync(cookie).ConfigureAwait(false); + return await TryCreateUserAndAddAsync(cookie, false).ConfigureAwait(false); + } + } + + /// + public async Task> 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(); + + 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> TryCreateUserAndAddAsync(Cookie cookie) + private async Task> TryCreateUserAndAddAsync(Cookie cookie, bool isOversea) { await ThreadHelper.SwitchToBackgroundAsync(); using (IServiceScope scope = scopeFactory.CreateScope()) { AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + 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 diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml index 8bd5f811..69f3a20a 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml @@ -225,6 +225,19 @@ Icon="{shcm:FontIcon Glyph=}" Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/> + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs index 1d8164d1..8cb5eb2d 100644 --- a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs +++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs @@ -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(RemoveUserAsync); CopyCookieCommand = new RelayCommand(CopyCookie); @@ -87,6 +88,11 @@ internal sealed class UserViewModel : ObservableObject /// public ICommand AddUserCommand { get; } + /// + /// 添加国际服用户命令 + /// + public ICommand AddOsUserCommand { get; } + /// /// 登录米游社命令 /// @@ -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 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) diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs index 8c9f8e06..f09ecbbd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Bbs/User/UserClient.cs @@ -52,4 +52,22 @@ internal sealed class UserClient return Response.Response.DefaultIfNull(resp); } + + /// + /// 获取当前用户详细信息 + /// + /// 用户 + /// 取消令牌 + /// 详细信息 + [ApiInformation(Cookie = CookieType.Stoken, Salt = SaltType.K2)] + public async Task> GetOsUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default) + { + Response? resp = await httpClient + .SetUser(user, CookieType.Cookie) + .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OS, false) + .TryCatchGetFromJsonAsync>(ApiOsEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs index 846117ea..0fd7ec68 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Cookie.cs @@ -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; + } + /// public bool TryGetValue(string key, [NotNullWhen(true)] out string? value) { diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient.cs index e41972a0..01217854 100644 --- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient.cs +++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/Binding/BindingClient.cs @@ -52,4 +52,23 @@ internal sealed class BindingClient return Response.Response.DefaultIfNull(resp); } + + /// + /// 获取国际服用户角色信息 + /// + /// 用户 + /// 取消令牌 + /// 用户角色信息 + [ApiInformation(Cookie = CookieType.Ltoken)] + public async Task>> GetOsUserGameRolesByCookieAsync(User user, CancellationToken token = default) + { + string url = ApiOsEndpoints.UserGameRolesByCookie; + + Response>? resp = await httpClient + .SetUser(user, CookieType.Ltoken) + .TryCatchGetFromJsonAsync>>(url, options, logger, token) + .ConfigureAwait(false); + + return Response.Response.DefaultIfNull(resp); + } }