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..1781289c 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/CoreEnvironment.cs
@@ -45,6 +45,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 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..038fc354 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.TryGetAsCookieToken(out Cookie? cookieToken);
+
+ return new() { 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/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/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..e24c9d5a 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
+ ///
+ /// 来自网页 www.hoyolab.com 的 Cookie,需包含 ltuid, ltoken 和 cookie_token 字段
+ /// 处理的结果
+ 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..4c08593e 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.TryGetAsCookieToken(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/Page/LoginHoyoverseUserPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml
new file mode 100644
index 00000000..a6031a73
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml.cs
new file mode 100644
index 00000000..4180e1e3
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LoginHoyoverseUserPage.xaml.cs
@@ -0,0 +1,98 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Microsoft.UI.Xaml;
+using Microsoft.Web.WebView2.Core;
+using Snap.Hutao.Service.Abstraction;
+using Snap.Hutao.Service.Navigation;
+using Snap.Hutao.Service.User;
+using Snap.Hutao.Web.Hoyolab;
+using Snap.Hutao.Web.Hoyolab.Passport;
+using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
+using Snap.Hutao.Web.Response;
+
+namespace Snap.Hutao.View.Page;
+
+///
+/// ¼֤ͨҳ
+///
+[HighQuality]
+internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Controls.Page
+{
+ ///
+ /// һµĵ¼֤ͨҳ
+ ///
+ public LoginHoyoverseUserPage()
+ {
+ InitializeComponent();
+ }
+
+ [SuppressMessage("", "VSTHRD100")]
+ private async void OnRootLoaded(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ await WebView.EnsureCoreWebView2Async();
+
+ CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
+ IReadOnlyList cookies = await manager.GetCookiesAsync("https://www.hoyolab.com");
+ foreach (CoreWebView2Cookie item in cookies)
+ {
+ manager.DeleteCookie(item);
+ }
+
+ WebView.CoreWebView2.Navigate("https://www.hoyolab.com");
+ }
+ catch (Exception ex)
+ {
+ Ioc.Default.GetRequiredService().Error(ex);
+ }
+ }
+
+ private async Task HandleCurrentCookieAsync(CancellationToken token)
+ {
+ CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
+ IReadOnlyList cookies = await manager.GetCookiesAsync("https://www.hoyolab.com");
+
+ Cookie hoyoLabCookie = Cookie.FromCoreWebView2Cookies(cookies);
+
+ // cookie û
+ (UserOptionResult result, string nickname) = await Ioc.Default
+ .GetRequiredService()
+ .ProcessInputOsCookieAsync(hoyoLabCookie)
+ .ConfigureAwait(false);
+
+ Ioc.Default.GetRequiredService().GoBack();
+ IInfoBarService infoBarService = Ioc.Default.GetRequiredService();
+
+ switch (result)
+ {
+ case UserOptionResult.Added:
+ ViewModel.UserViewModel vm = Ioc.Default.GetRequiredService();
+ if (vm.Users!.Count == 1)
+ {
+ await ThreadHelper.SwitchToMainThreadAsync();
+ vm.SelectedUser = vm.Users.Single();
+ }
+
+ infoBarService.Success($"û [{nickname}] ӳɹ");
+ break;
+ case UserOptionResult.Incomplete:
+ infoBarService.Information($" Cookie ʧ");
+ break;
+ case UserOptionResult.Invalid:
+ infoBarService.Information($" Cookie Чʧ");
+ break;
+ case UserOptionResult.Updated:
+ infoBarService.Success($"û [{nickname}] ³ɹ");
+ break;
+ default:
+ throw Must.NeverHappen();
+ }
+ }
+
+ private void CookieButtonClick(object sender, RoutedEventArgs e)
+ {
+ HandleCurrentCookieAsync(CancellationToken.None).SafeForget();
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml
index 8bd5f811..ddba3e58 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml
@@ -225,6 +225,23 @@
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..fc3c289f 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/UserViewModel.cs
@@ -46,7 +46,9 @@ internal sealed class UserViewModel : ObservableObject
OpenUICommand = new AsyncRelayCommand(OpenUIAsync);
AddUserCommand = new AsyncRelayCommand(AddUserAsync);
+ AddOsUserCommand = new AsyncRelayCommand(AddOsUserAsync);
LoginMihoyoUserCommand = new RelayCommand(LoginMihoyoUser);
+ LoginHoyoverseUserCommand = new RelayCommand(LoginHoyoverseUser);
RemoveUserCommand = new AsyncRelayCommand(RemoveUserAsync);
CopyCookieCommand = new RelayCommand(CopyCookie);
RefreshCookieTokenCommand = new AsyncRelayCommand(RefreshCookieTokenAsync);
@@ -87,11 +89,21 @@ internal sealed class UserViewModel : ObservableObject
///
public ICommand AddUserCommand { get; }
+ ///
+ /// 添加国际服用户命令
+ ///
+ public ICommand AddOsUserCommand { get; }
+
///
/// 登录米游社命令
///
public ICommand LoginMihoyoUserCommand { get; }
+ ///
+ /// 登录米游社命令
+ ///
+ public ICommand LoginHoyoverseUserCommand { get; }
+
///
/// 移除用户命令
///
@@ -161,6 +173,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)
@@ -173,6 +226,21 @@ internal sealed class UserViewModel : ObservableObject
}
}
+ ///
+ /// 打开浏览器登录 hoyolab 以获取 cookie
+ ///
+ private void LoginHoyoverseUser()
+ {
+ if (Core.WebView2Helper.IsSupported)
+ {
+ serviceProvider.GetRequiredService().Navigate(INavigationAwaiter.Default);
+ }
+ else
+ {
+ infoBarService.Warning(SH.CoreWebView2HelperVersionUndetected);
+ }
+ }
+
private async Task RemoveUserAsync(User? user)
{
if (user != null)
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs
index 6743bd2d..4dc726ac 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs
@@ -1,7 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
+using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Game;
+using Snap.Hutao.Web.Hoyolab;
namespace Snap.Hutao.Web;
@@ -13,6 +15,76 @@ namespace Snap.Hutao.Web;
[SuppressMessage("", "SA1124")]
internal static class ApiOsEndpoints
{
+ #region ApiOsTaKumiApi
+
+ ///
+ /// 用户游戏角色
+ ///
+ /// 用户游戏角色字符串
+ public const string UserGameRolesByCookie = $"{ApiOsTaKumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_global";
+
+ #endregion
+
+ #region BbsApiOsApi
+
+ ///
+ /// 查询其他用户详细信息
+ ///
+ /// bbs Uid
+ /// 查询其他用户详细信息字符串
+ public static string UserFullInfoQuery(string bbsUid)
+ {
+ return $"{BbsApiOs}/community/painter/wapi/user/full";
+ }
+
+ ///
+ /// 国际服角色基本信息
+ ///
+ /// uid
+ /// 角色基本信息字符串
+ public static string GameRecordRoleBasicInfo(PlayerUid uid)
+ {
+ return $"{BbsApiOsGameRecordApi}/roleBasicInfo?role_id={uid.Value}&server={uid.Region}";
+ }
+
+ ///
+ /// 国际服角色信息
+ ///
+ public const string GameRecordCharacter = $"{BbsApiOsGameRecordApi}/character";
+
+ ///
+ /// 国际服游戏记录实时便笺
+ ///
+ /// uid
+ /// 游戏记录实时便笺字符串
+ public static string GameRecordDailyNote(PlayerUid uid)
+ {
+ return $"{BbsApiOsGameRecordApi}/dailyNote?server={uid.Region}&role_id={uid.Value}";
+ }
+
+ ///
+ /// 国际服游戏记录主页
+ ///
+ /// uid
+ /// 游戏记录主页字符串
+ public static string GameRecordIndex(PlayerUid uid)
+ {
+ return $"{BbsApiOsGameRecordApi}/index?server={uid.Region}&role_id={uid.Value}";
+ }
+
+ ///
+ /// 国际服深渊信息
+ ///
+ /// 深渊类型
+ /// Uid
+ /// 深渊信息字符串
+ public static string GameRecordSpiralAbyss(Hoyolab.Takumi.GameRecord.SpiralAbyssSchedule scheduleType, PlayerUid uid)
+ {
+ return $"{BbsApiOsGameRecordApi}/spiralAbyss?schedule_type={(int)scheduleType}&role_id={uid.Value}&server={uid.Region}";
+ }
+
+ #endregion
+
#region Hk4eApiOsGachaInfoApi
///
@@ -40,6 +112,12 @@ internal static class ApiOsEndpoints
#endregion
#region Hosts | Queries
+ private const string ApiOsTaKumi = "https://api-os-takumi.hoyoverse.com";
+ private const string ApiOsTaKumiBindingApi = $"{ApiOsTaKumi}/binding/api";
+
+ private const string BbsApiOs = "https://bbs-api-os.hoyolab.com";
+ private const string BbsApiOsGameRecordApi = $"{BbsApiOs}/game_record/genshin/api";
+
private const string Hk4eApiOs = "https://hk4e-api-os.hoyoverse.com";
private const string Hk4eApiOsGachaInfoApi = $"{Hk4eApiOs}/event/gacha_info/api";
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/DynamicSecret/SaltType.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/SaltType.cs
index e9c18fcd..2683b1aa 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/SaltType.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DynamicSecret/SaltType.cs
@@ -38,4 +38,9 @@ internal enum SaltType
/// LK2
///
LK2,
+
+ ///
+ /// 国际服
+ ///
+ OS,
}
\ No newline at end of file
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);
+ }
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClientOs.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClientOs.cs
new file mode 100644
index 00000000..63b25753
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/Takumi/GameRecord/GameRecordClientOs.cs
@@ -0,0 +1,133 @@
+using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
+using Snap.Hutao.Model.Binding.User;
+using Snap.Hutao.Model.Primitive;
+using Snap.Hutao.Service.Abstraction;
+using Snap.Hutao.Web.Hoyolab.Annotation;
+using Snap.Hutao.Web.Hoyolab.DynamicSecret;
+using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
+using Snap.Hutao.Web.Response;
+using System.Net.Http;
+
+namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
+
+///
+/// Hoyoverse game record provider
+///
+[HighQuality]
+[UseDynamicSecret]
+[HttpClient(HttpClientConfigration.XRpc3)]
+[PrimaryHttpMessageHandler(UseCookies = false)]
+internal sealed class GameRecordClientOs
+{
+ private readonly HttpClient httpClient;
+ private readonly JsonSerializerOptions options;
+ private readonly ILogger logger;
+
+ ///
+ /// 构造一个新的游戏记录提供器
+ ///
+ /// 请求器
+ /// json序列化选项
+ /// 日志器
+ public GameRecordClientOs(HttpClient httpClient, JsonSerializerOptions options, ILogger logger)
+ {
+ this.httpClient = httpClient;
+ this.options = options;
+ this.logger = logger;
+ }
+
+ ///
+ /// 异步获取实时便笺
+ ///
+ /// 用户与角色
+ /// 取消令牌
+ /// 实时便笺
+ [ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OS)]
+ public async Task> GetDailyNoteAsync(UserAndUid userAndUid, CancellationToken token = default)
+ {
+ Response? resp = await httpClient
+ .SetUser(userAndUid.User, CookieType.Cookie)
+ .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OS, false)
+ .TryCatchGetFromJsonAsync>(ApiOsEndpoints.GameRecordDailyNote(userAndUid.Uid.Value), options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
+
+ ///
+ /// 获取玩家基础信息
+ ///
+ /// 用户与角色
+ /// 取消令牌
+ /// 玩家的基础信息
+ [ApiInformation(Cookie = CookieType.Ltoken, Salt = SaltType.OS)]
+ public async Task> GetPlayerInfoAsync(UserAndUid userAndUid, CancellationToken token = default)
+ {
+ Response? resp = await httpClient
+ .SetUser(userAndUid.User, CookieType.Cookie)
+ .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OS, false)
+ .TryCatchGetFromJsonAsync>(ApiOsEndpoints.GameRecordIndex(userAndUid.Uid), options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
+
+ ///
+ /// 获取玩家深渊信息
+ ///
+ /// 用户
+ /// 1:当期,2:上期
+ /// 取消令牌
+ /// 深渊信息
+ [ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OS)]
+ public async Task> GetSpiralAbyssAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule, CancellationToken token = default)
+ {
+ Response? resp = await httpClient
+ .SetUser(userAndUid.User, CookieType.Cookie)
+ .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OS, false)
+ .TryCatchGetFromJsonAsync>(ApiOsEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid), options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
+
+ ///
+ /// 获取玩家角色详细信息
+ ///
+ /// 用户与角色
+ /// 玩家的基础信息
+ /// 取消令牌
+ /// 角色列表
+ [ApiInformation(Cookie = CookieType.Ltoken, Salt = SaltType.X4)]
+ public async Task> GetCharactersAsync(UserAndUid userAndUid, PlayerInfo playerInfo, CancellationToken token = default)
+ {
+ CharacterData data = new(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id));
+
+ Response? resp = await httpClient
+ .SetUser(userAndUid.User, CookieType.Cookie)
+ .UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OS, false)
+ .TryCatchPostAsJsonAsync>(ApiOsEndpoints.GameRecordCharacter, data, options, logger, token)
+ .ConfigureAwait(false);
+
+ return Response.Response.DefaultIfNull(resp);
+ }
+
+ private class CharacterData
+ {
+ public CharacterData(PlayerUid uid, IEnumerable characterIds)
+ {
+ CharacterIds = characterIds;
+ Uid = uid.Value;
+ Server = uid.Region;
+ }
+
+ [JsonPropertyName("character_ids")]
+ public IEnumerable CharacterIds { get; }
+
+ [JsonPropertyName("role_id")]
+ public string Uid { get; }
+
+ [JsonPropertyName("server")]
+ public string Server { get; }
+ }
+}