diff --git a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientGenerator.cs b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientGenerator.cs index 4c104834..a83e2136 100644 --- a/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientGenerator.cs +++ b/src/Snap.Hutao/Snap.Hutao.SourceGeneration/DedendencyInjection/HttpClientGenerator.cs @@ -24,6 +24,7 @@ public class HttpClientGenerator : ISourceGenerator private const string DefaultName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.Default"; private const string XRpcName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc"; private const string XRpc2Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc2"; + private const string XRpc3Name = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.HttpClientConfigration.XRpc3"; private const string PrimaryHttpMessageHandlerAttributeName = "Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient.PrimaryHttpMessageHandlerAttribute"; private const string DynamicSecretAttributeName = "Snap.Hutao.Web.Hoyolab.DynamicSecret.UseDynamicSecretAttribute"; @@ -105,6 +106,9 @@ internal static partial class IocHttpClientConfiguration case XRpc2Name: lineBuilder.Append("XRpc2Configuration)"); break; + case XRpc3Name: + lineBuilder.Append("XRpc3Configuration)"); + break; default: throw new InvalidOperationException($"非法的HttpClientConfigration值: [{injectAsName}]"); } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs index bfc9ad0a..4684dc78 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs @@ -29,6 +29,11 @@ internal static class CoreEnvironment /// public const string HoyolabMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/{HoyolabXrpcVersion}"; + /// + /// Hoyolab iPhone 移动端请求UA + /// + public const string HoyolabOsMobileUA = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) miHoYoBBSOversea/2.28.0"; + /// /// 米游社 Rpc 版本 /// @@ -45,6 +50,7 @@ internal static class CoreEnvironment [SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs", [SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v", [SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS", + [SaltType.OS] = "6cqshh5dhw73bzxn20oexa9k516chk7s", }.ToImmutableDictionary(); /// diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfigration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfigration.cs index 4d12e220..6bc5419c 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfigration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/Annotation/HttpClient/HttpClientConfigration.cs @@ -23,4 +23,9 @@ internal enum HttpClientConfigration /// 米游社登录请求配置 /// XRpc2, + + /// + /// 国际服Hoyolab请求配置 + /// + XRpc3, } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs index 1a532ad2..dc6de29b 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocHttpClientConfiguration.cs @@ -60,4 +60,18 @@ internal static partial class IocHttpClientConfiguration client.DefaultRequestHeaders.Add("x-rpc-game_biz", "bbs_cn"); client.DefaultRequestHeaders.Add("x-rpc-sdk_version", "1.3.1.2"); } + + /// + /// 对于需要添加动态密钥的客户端使用此配置 + /// 国际服 API 测试 + /// + /// 配置后的客户端 + private static void XRpc3Configuration(HttpClient client) + { + client.Timeout = Timeout.InfiniteTimeSpan; + client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36"); + client.DefaultRequestHeaders.Accept.ParseAdd("application/json"); + client.DefaultRequestHeaders.Add("x-rpc-app_version", "1.5.0"); + client.DefaultRequestHeaders.Add("x-rpc-client_type", "4"); + } } \ No newline at end of file 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 4da00a9b..f3fca7fb 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,16 @@ 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; + + if (!user.Entity.IsOversea) + { + isOk = await user.InitializeCoreAsync(token).ConfigureAwait(false); + } + else + { + isOk = await user.InitializeCoreOsAsync(token).ConfigureAwait(false); + } if (!isOk) { @@ -127,6 +137,7 @@ internal sealed class User : ObservableObject entity.Aid = cookie.GetValueOrDefault(Cookie.STUID); entity.Mid = cookie.GetValueOrDefault(Cookie.MID); + entity.IsOversea = false; if (entity.Aid != null && entity.Mid != null) { @@ -141,6 +152,38 @@ internal sealed class User : ObservableObject } } + /// + /// 创建并初始化国际服用户(临时) + /// + /// cookie + /// 取消令牌 + /// 用户 + internal static async Task CreateOsUserAsync(Cookie cookie, CancellationToken token = default) + { + // 这里只负责创建实体用户,稍后在用户服务中保存到数据库 + EntityUser entity = EntityUser.CreateOs(cookie); + + entity.Aid = cookie.GetValueOrDefault(Cookie.STUID); + + // Note: Currently we dont know how to get "mid" for hoyolab user, + // mid is set as the same value of ltuid(stuid/user id) + entity.Mid = entity.Aid; + + entity.IsOversea = true; + + if (entity.Aid != 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) @@ -268,4 +311,87 @@ internal sealed class User : ObservableObject return false; } } + + private async Task InitializeCoreOsAsync(CancellationToken token = default) + { + if (isInitialized) + { + return true; + } + + if (SToken == null) + { + return false; + } + + using (IServiceScope scope = Ioc.Default.CreateScope()) + { + + // 自动填充 Ltoken + if (LToken == null) + { + Response ltokenResponse = await scope.ServiceProvider + .GetRequiredService() + .GetLtokenBySTokenAsync(Entity, token) + .ConfigureAwait(false); + + if (ltokenResponse.IsOk()) + { + Cookie ltokenCookie = Cookie.Parse($"ltuid={Entity.Aid};ltoken={ltokenResponse.Data.Ltoken}"); + Entity.LToken = ltokenCookie; + } + else + { + return false; + } + } + + // Fetch user info + Response response = await scope.ServiceProvider + .GetRequiredService() + .GetOsUserFullInfoAsync(Entity, token) + .ConfigureAwait(false); + UserInfo = response.Data?.UserInfo; + + // 自动填充 CookieToken + if (CookieToken == null) + { + Response cookieTokenResponse = await scope.ServiceProvider + .GetRequiredService() + .GetCookieAccountInfoBySTokenAsync(Entity, token) + .ConfigureAwait(false); + + if (cookieTokenResponse.IsOk()) + { + Cookie cookieTokenCookie = Cookie.Parse($"account_id={Entity.Aid};cookie_token={cookieTokenResponse.Data.CookieToken}"); + Entity.CookieToken = cookieTokenCookie; + } + else + { + 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 4b492b93..d77bfff2 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; @@ -72,4 +73,18 @@ internal sealed class User : ISelectable return new() { SToken = stoken, LToken = ltoken, CookieToken = cookieToken }; } + + /// + /// 创建一个国际服用户 + /// + /// cookie + /// 新创建的用户 + public static User CreateOs(Cookie cookie) + { + _ = cookie.TryGetAsStokenV1(out Cookie? stoken); + _ = cookie.TryGetAsLtoken(out Cookie? ltoken); + _ = cookie.TryGetAsCookieToken(out Cookie? cookieToken); + + return new() { SToken = stoken, LToken = ltoken, CookieToken = cookieToken }; + } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs index 68b036a9..0a47f975 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/AvatarInfo/AvatarInfoDbOperation.cs @@ -90,48 +90,72 @@ internal sealed class AvatarInfoDbOperation .ToList(); EnsureItemsAvatarIdDistinct(ref dbInfos, uid); - GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService(); - Response playerInfoResponse = await gameRecordClient - .GetPlayerInfoAsync(userAndUid, token) - .ConfigureAwait(false); + Response playerInfoResponse; + Response charactersResponse; + + if (userAndUid.Uid.Region == "cn_gf01" || userAndUid.Uid.Region == "cn_qd01") + { + GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService(); + playerInfoResponse = await gameRecordClient + .GetPlayerInfoAsync(userAndUid, token) + .ConfigureAwait(false); + + if (!playerInfoResponse.IsOk()) + { + return GetDbAvatarInfos(uid); + } + + charactersResponse = await gameRecordClient + .GetCharactersAsync(userAndUid, playerInfoResponse.Data, token) + .ConfigureAwait(false); + } + else + { + GameRecordClientOs gameRecordClientOs = Ioc.Default.GetRequiredService(); + playerInfoResponse = await gameRecordClientOs + .GetPlayerInfoAsync(userAndUid, token) + .ConfigureAwait(false); + + if (!playerInfoResponse.IsOk()) + { + return GetDbAvatarInfos(uid); + } + + charactersResponse = await gameRecordClientOs + .GetCharactersAsync(userAndUid, playerInfoResponse.Data, token) + .ConfigureAwait(false); + } token.ThrowIfCancellationRequested(); - if (playerInfoResponse.IsOk()) + if (charactersResponse.IsOk()) { - Response charactersResponse = await gameRecordClient - .GetCharactersAsync(userAndUid, playerInfoResponse.Data, token) - .ConfigureAwait(false); + List characters = charactersResponse.Data.Avatars; - if (charactersResponse.IsOk()) + GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService(); + + foreach (RecordCharacter character in characters) { - List characters = charactersResponse.Data.Avatars; - - GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService(); - - foreach (RecordCharacter character in characters) + if (AvatarIds.IsPlayer(character.Id)) { - if (AvatarIds.IsPlayer(character.Id)) - { - continue; - } + continue; + } - token.ThrowIfCancellationRequested(); + token.ThrowIfCancellationRequested(); - ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == character.Id); + ModelAvatarInfo? entity = dbInfos.SingleOrDefault(i => i.Info.AvatarId == character.Id); - if (entity == null) - { - EnkaAvatarInfo avatarInfo = new() { AvatarId = character.Id }; - avatarInfo = await composer.ComposeAsync(avatarInfo, character).ConfigureAwait(false); - entity = ModelAvatarInfo.Create(uid, avatarInfo); - appDbContext.AvatarInfos.AddAndSave(entity); - } - else - { - entity.Info = await composer.ComposeAsync(entity.Info, character).ConfigureAwait(false); - appDbContext.AvatarInfos.UpdateAndSave(entity); - } + if (entity == null) + { + EnkaAvatarInfo avatarInfo = new() { AvatarId = character.Id }; + avatarInfo = await composer.ComposeAsync(avatarInfo, character).ConfigureAwait(false); + entity = ModelAvatarInfo.Create(uid, avatarInfo); + appDbContext.AvatarInfos.AddAndSave(entity); + } + else + { + entity.Info = await composer.ComposeAsync(entity.Info, character).ConfigureAwait(false); + appDbContext.AvatarInfos.UpdateAndSave(entity); } } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs index 8bf52e47..71e6bb59 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteNotifier.cs @@ -60,16 +60,13 @@ internal sealed class DailyNoteNotifier BindingClient bindingClient = scope.ServiceProvider.GetRequiredService(); AuthClient authClient = scope.ServiceProvider.GetRequiredService(); - Response actionTicketResponse = await authClient - .GetActionTicketByStokenAsync("game_role", entry.User) - .ConfigureAwait(false); - string? attribution = SH.ServiceDailyNoteNotifierAttribution; - if (actionTicketResponse.IsOk()) + + if (entry.User.IsOversea) { Response> rolesResponse = await scope.ServiceProvider .GetRequiredService() - .GetUserGameRolesByActionTicketAsync(actionTicketResponse.Data.Ticket, entry.User) + .GetOsUserGameRolesByCookieAsync(entry.User) .ConfigureAwait(false); if (rolesResponse.IsOk()) @@ -77,6 +74,27 @@ internal sealed class DailyNoteNotifier List roles = rolesResponse.Data.List; attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? "Unkonwn"; } + + } + else + { + Response actionTicketResponse = await authClient + .GetActionTicketByStokenAsync("game_role", entry.User) + .ConfigureAwait(false); + + if (actionTicketResponse.IsOk()) + { + Response> rolesResponse = await scope.ServiceProvider + .GetRequiredService() + .GetUserGameRolesByActionTicketAsync(actionTicketResponse.Data.Ticket, entry.User) + .ConfigureAwait(false); + + if (rolesResponse.IsOk()) + { + List roles = rolesResponse.Data.List; + attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? "Unkonwn"; + } + } } ToastContentBuilder builder = new ToastContentBuilder() diff --git a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs index 3add8387..37fca642 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/DailyNote/DailyNoteService.cs @@ -12,6 +12,7 @@ using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Service.Abstraction; using Snap.Hutao.Service.Game; using Snap.Hutao.Service.User; +using Snap.Hutao.Web.Hoyolab; using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord; using System.Collections.ObjectModel; using WebDailyNote = Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.DailyNote.DailyNote; @@ -61,14 +62,27 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient(); GameRecordClient gameRecordClient = scope.ServiceProvider.GetRequiredService(); + GameRecordClientOs gameRecordClientOs = scope.ServiceProvider.GetRequiredService(); if (!appDbContext.DailyNotes.Any(n => n.Uid == roleUid)) { DailyNoteEntry newEntry = DailyNoteEntry.Create(role); - Web.Response.Response dailyNoteResponse = await gameRecordClient + // 根据 Uid 的地区选择不同的 API + Web.Response.Response dailyNoteResponse; + PlayerUid playerUid = new(roleUid); + if (playerUid.Region == "cn_gf01" || playerUid.Region == "cn_qd01") + { + dailyNoteResponse = await gameRecordClient .GetDailyNoteAsync(role) .ConfigureAwait(false); + } + else + { + dailyNoteResponse = await gameRecordClientOs + .GetDailyNoteAsync(role) + .ConfigureAwait(false); + } if (dailyNoteResponse.IsOk()) { @@ -110,11 +124,13 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient(); GameRecordClient gameRecordClient = scope.ServiceProvider.GetRequiredService(); + GameRecordClientOs gameRecordClientOs = scope.ServiceProvider.GetRequiredService(); bool isSilentMode = appDbContext.Settings .SingleOrAdd(SettingEntry.DailyNoteSilentWhenPlayingGame, Core.StringLiterals.False) .GetBoolean(); bool isGameRunning = scope.ServiceProvider.GetRequiredService().IsGameRunning(); + if (isSilentMode && isGameRunning) { // Prevent notify when we are in game && silent mode. @@ -123,9 +139,20 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient n.User)) { - Web.Response.Response dailyNoteResponse = await gameRecordClient + Web.Response.Response dailyNoteResponse; + PlayerUid playerUid = new(entry.Uid); + if (playerUid.Region == "cn_gf01" || playerUid.Region == "cn_qd01") + { + dailyNoteResponse = await gameRecordClient .GetDailyNoteAsync(new(entry.User, entry.Uid)) .ConfigureAwait(false); + } + else + { + dailyNoteResponse = await gameRecordClientOs + .GetDailyNoteAsync(new(entry.User, entry.Uid)) + .ConfigureAwait(false); + } if (dailyNoteResponse.ReturnCode == 0) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryStokenProvider.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryStokenProvider.cs index 2db0d111..701129c8 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryStokenProvider.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/QueryProvider/GachaLogQueryStokenProvider.cs @@ -38,6 +38,11 @@ internal sealed class GachaLogQueryStokenProvider : IGachaLogQueryProvider { if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid)) { + if (userAndUid.Uid.Region != "cn_gf01" && userAndUid.Uid.Region != "cn_qd01") + { + return new(false, "Unsupported for hoyoverse account"); + } + GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(userAndUid.Uid); Response authkeyResponse = await bindingClient2.GenerateAuthenticationKeyAsync(userAndUid.User, data).ConfigureAwait(false); diff --git a/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs b/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs index 45365467..f2fd9c83 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/SpiralAbyss/SpiralAbyssRecordService.cs @@ -21,6 +21,7 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService { private readonly AppDbContext appDbContext; private readonly GameRecordClient gameRecordClient; + private readonly GameRecordClientOs gameRecordClientOs; private string? uid; private ObservableCollection? spiralAbysses; @@ -30,10 +31,11 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService /// /// 数据库上下文 /// 游戏记录客户端 - public SpiralAbyssRecordService(AppDbContext appDbContext, GameRecordClient gameRecordClient) + public SpiralAbyssRecordService(AppDbContext appDbContext, GameRecordClient gameRecordClient, GameRecordClientOs gameRecordClientOs) { this.appDbContext = appDbContext; this.gameRecordClient = gameRecordClient; + this.gameRecordClientOs = gameRecordClientOs; } /// @@ -70,9 +72,21 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService private async Task RefreshSpiralAbyssCoreAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule) { - Response response = await gameRecordClient + Response response; + + // server determination + if (userAndUid.Uid.Region == "cn_gf01" || userAndUid.Uid.Region == "cn_qd01") + { + response = await gameRecordClient .GetSpiralAbyssAsync(userAndUid, schedule) .ConfigureAwait(false); + } + else + { + response = await gameRecordClientOs + .GetSpiralAbyssAsync(userAndUid, schedule) + .ConfigureAwait(false); + } if (response.IsOk()) { diff --git a/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs b/src/Snap.Hutao/Snap.Hutao/Service/User/IUserService.cs index 8546d4a8..8c4502d7 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,需包含 stuid, stoken 字段 + /// 处理的结果 + 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 e111713c..a4aa33cd 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/User/UserService.cs @@ -225,7 +225,49 @@ 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? stuid = cookie.GetValueOrDefault(Cookie.STUID); + + if (stuid == null) + { + return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoMid); + } + + // 检查 stuid 对应用户是否存在 + if (TryGetUser(userCollection!, stuid, out BindingUser? user)) + { + // Note: Currently we dont know how to get "mid" for hoyolab user, + // mid is set as the same value of ltuid(stuid/user id) + user.Entity.Mid = stuid; + using (IServiceScope scope = scopeFactory.CreateScope()) + { + AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService(); + + if (cookie.TryGetAsStoken(out Cookie? stoken)) + { + user.SToken = stoken; + user.LToken = cookie.TryGetAsLtoken(out Cookie? ltoken) ? ltoken : user.LToken; + user.CookieToken = cookie.TryGetAsCookieToken(out Cookie? cookieToken) ? cookieToken : user.CookieToken; + + await appDbContext.Users.UpdateAndSaveAsync(user.Entity).ConfigureAwait(false); + return new(UserOptionResult.Updated, stuid); + } + else + { + return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoStoken); + } + } + } + else + { + return await TryCreateUserAndAddAsync(cookie, true).ConfigureAwait(false); } } @@ -234,10 +276,21 @@ internal class UserService : IUserService { using (IServiceScope scope = scopeFactory.CreateScope()) { - Response cookieTokenResponse = await scope.ServiceProvider + Response cookieTokenResponse; + if (user.Entity.IsOversea) + { + cookieTokenResponse = await scope.ServiceProvider + .GetRequiredService() + .GetCookieAccountInfoBySTokenAsync(user.Entity) + .ConfigureAwait(false); + } + else + { + cookieTokenResponse = await scope.ServiceProvider .GetRequiredService() .GetCookieAccountInfoBySTokenAsync(user.Entity) .ConfigureAwait(false); + } if (cookieTokenResponse.IsOk()) { @@ -265,14 +318,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/Dialog/SignInWebViewDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs index e712f613..33a686e0 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/SignInWebViewDialog.xaml.cs @@ -47,9 +47,18 @@ internal sealed partial class SignInWebViewDialog : ContentDialog return; } - coreWebView2.SetCookie(user.CookieToken, user.LToken, null).SetMobileUserAgent(); - signInJsInterface = new(coreWebView2, scope.ServiceProvider); - coreWebView2.Navigate("https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?act_id=e202009291139501"); + if (user.Entity.IsOversea) + { + coreWebView2.SetCookie(user.CookieToken, user.LToken, null).SetOsMobileUserAgent(); + signInJsInterface = new(coreWebView2, scope.ServiceProvider); + coreWebView2.Navigate("https://act.hoyolab.com/ys/event/signin-sea-v3/index.html?act_id=e202102251931481&hyl_presentation_style=fullscreen"); + } + else + { + coreWebView2.SetCookie(user.CookieToken, user.LToken, null).SetMobileUserAgent(); + signInJsInterface = new(coreWebView2, scope.ServiceProvider); + coreWebView2.Navigate("https://webstatic.mihoyo.com/bbs/event/signin-ys/index.html?act_id=e202009291139501"); + } } private void OnContentDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args) diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml new file mode 100644 index 00000000..e705ba28 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + +