mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Login from hoyolab account website to get stoken
This commit is contained in:
@@ -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}]");
|
||||
}
|
||||
|
||||
@@ -29,6 +29,11 @@ internal static class CoreEnvironment
|
||||
/// </summary>
|
||||
public const string HoyolabMobileUA = $"Mozilla/5.0 (Linux; Android 12) Mobile miHoYoBBS/{HoyolabXrpcVersion}";
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab iPhone 移动端请求UA
|
||||
/// </summary>
|
||||
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";
|
||||
|
||||
/// <summary>
|
||||
/// 米游社 Rpc 版本
|
||||
/// </summary>
|
||||
@@ -45,6 +50,7 @@ internal static class CoreEnvironment
|
||||
[SaltType.X4] = "xV8v4Qu54lUKrEYFZkJhB8cuOh9Asafs",
|
||||
[SaltType.X6] = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v",
|
||||
[SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
|
||||
[SaltType.OS] = "6cqshh5dhw73bzxn20oexa9k516chk7s",
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -23,4 +23,9 @@ internal enum HttpClientConfigration
|
||||
/// 米游社登录请求配置
|
||||
/// </summary>
|
||||
XRpc2,
|
||||
|
||||
/// <summary>
|
||||
/// 国际服Hoyolab请求配置
|
||||
/// </summary>
|
||||
XRpc3,
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对于需要添加动态密钥的客户端使用此配置
|
||||
/// 国际服 API 测试
|
||||
/// </summary>
|
||||
/// <param name="client">配置后的客户端</param>
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -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<User> 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
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
|
||||
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<bool> InitializeCoreAsync(CancellationToken token = default)
|
||||
{
|
||||
if (isInitialized)
|
||||
@@ -268,4 +311,87 @@ internal sealed class User : ObservableObject
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> InitializeCoreOsAsync(CancellationToken token = default)
|
||||
{
|
||||
if (isInitialized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (SToken == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using (IServiceScope scope = Ioc.Default.CreateScope())
|
||||
{
|
||||
|
||||
// 自动填充 Ltoken
|
||||
if (LToken == null)
|
||||
{
|
||||
Response<LtokenWrapper> ltokenResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<PassportClientOs>()
|
||||
.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<UserFullInfoWrapper> response = await scope.ServiceProvider
|
||||
.GetRequiredService<UserClient>()
|
||||
.GetOsUserFullInfoAsync(Entity, token)
|
||||
.ConfigureAwait(false);
|
||||
UserInfo = response.Data?.UserInfo;
|
||||
|
||||
// 自动填充 CookieToken
|
||||
if (CookieToken == null)
|
||||
{
|
||||
Response<UidCookieToken> cookieTokenResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<PassportClientOs>()
|
||||
.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<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;
|
||||
|
||||
@@ -72,4 +73,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)
|
||||
{
|
||||
_ = cookie.TryGetAsStokenV1(out Cookie? stoken);
|
||||
_ = cookie.TryGetAsLtoken(out Cookie? ltoken);
|
||||
_ = cookie.TryGetAsCookieToken(out Cookie? cookieToken);
|
||||
|
||||
return new() { SToken = stoken, LToken = ltoken, CookieToken = cookieToken };
|
||||
}
|
||||
}
|
||||
@@ -90,48 +90,72 @@ internal sealed class AvatarInfoDbOperation
|
||||
.ToList();
|
||||
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
|
||||
|
||||
GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService<GameRecordClient>();
|
||||
Response<RecordPlayerInfo> playerInfoResponse = await gameRecordClient
|
||||
.GetPlayerInfoAsync(userAndUid, token)
|
||||
.ConfigureAwait(false);
|
||||
Response<RecordPlayerInfo> playerInfoResponse;
|
||||
Response<Web.Hoyolab.Takumi.GameRecord.Avatar.CharacterWrapper> charactersResponse;
|
||||
|
||||
if (userAndUid.Uid.Region == "cn_gf01" || userAndUid.Uid.Region == "cn_qd01")
|
||||
{
|
||||
GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService<GameRecordClient>();
|
||||
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<GameRecordClientOs>();
|
||||
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<Web.Hoyolab.Takumi.GameRecord.Avatar.CharacterWrapper> charactersResponse = await gameRecordClient
|
||||
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
|
||||
.ConfigureAwait(false);
|
||||
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
|
||||
|
||||
if (charactersResponse.IsOk())
|
||||
GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService<GameRecordCharacterAvatarInfoComposer>();
|
||||
|
||||
foreach (RecordCharacter character in characters)
|
||||
{
|
||||
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
|
||||
|
||||
GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService<GameRecordCharacterAvatarInfoComposer>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,16 +60,13 @@ internal sealed class DailyNoteNotifier
|
||||
BindingClient bindingClient = scope.ServiceProvider.GetRequiredService<BindingClient>();
|
||||
AuthClient authClient = scope.ServiceProvider.GetRequiredService<AuthClient>();
|
||||
|
||||
Response<ActionTicketWrapper> actionTicketResponse = await authClient
|
||||
.GetActionTicketByStokenAsync("game_role", entry.User)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
string? attribution = SH.ServiceDailyNoteNotifierAttribution;
|
||||
if (actionTicketResponse.IsOk())
|
||||
|
||||
if (entry.User.IsOversea)
|
||||
{
|
||||
Response<ListWrapper<UserGameRole>> rolesResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<BindingClient>()
|
||||
.GetUserGameRolesByActionTicketAsync(actionTicketResponse.Data.Ticket, entry.User)
|
||||
.GetOsUserGameRolesByCookieAsync(entry.User)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (rolesResponse.IsOk())
|
||||
@@ -77,6 +74,27 @@ internal sealed class DailyNoteNotifier
|
||||
List<UserGameRole> roles = rolesResponse.Data.List;
|
||||
attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? "Unkonwn";
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
Response<ActionTicketWrapper> actionTicketResponse = await authClient
|
||||
.GetActionTicketByStokenAsync("game_role", entry.User)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (actionTicketResponse.IsOk())
|
||||
{
|
||||
Response<ListWrapper<UserGameRole>> rolesResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<BindingClient>()
|
||||
.GetUserGameRolesByActionTicketAsync(actionTicketResponse.Data.Ticket, entry.User)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (rolesResponse.IsOk())
|
||||
{
|
||||
List<UserGameRole> roles = rolesResponse.Data.List;
|
||||
attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? "Unkonwn";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToastContentBuilder builder = new ToastContentBuilder()
|
||||
|
||||
@@ -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<UserRemov
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
GameRecordClient gameRecordClient = scope.ServiceProvider.GetRequiredService<GameRecordClient>();
|
||||
GameRecordClientOs gameRecordClientOs = scope.ServiceProvider.GetRequiredService<GameRecordClientOs>();
|
||||
|
||||
if (!appDbContext.DailyNotes.Any(n => n.Uid == roleUid))
|
||||
{
|
||||
DailyNoteEntry newEntry = DailyNoteEntry.Create(role);
|
||||
|
||||
Web.Response.Response<WebDailyNote> dailyNoteResponse = await gameRecordClient
|
||||
// 根据 Uid 的地区选择不同的 API
|
||||
Web.Response.Response<WebDailyNote> 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<UserRemov
|
||||
{
|
||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
GameRecordClient gameRecordClient = scope.ServiceProvider.GetRequiredService<GameRecordClient>();
|
||||
GameRecordClientOs gameRecordClientOs = scope.ServiceProvider.GetRequiredService<GameRecordClientOs>();
|
||||
|
||||
bool isSilentMode = appDbContext.Settings
|
||||
.SingleOrAdd(SettingEntry.DailyNoteSilentWhenPlayingGame, Core.StringLiterals.False)
|
||||
.GetBoolean();
|
||||
bool isGameRunning = scope.ServiceProvider.GetRequiredService<IGameService>().IsGameRunning();
|
||||
|
||||
if (isSilentMode && isGameRunning)
|
||||
{
|
||||
// Prevent notify when we are in game && silent mode.
|
||||
@@ -123,9 +139,20 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient<UserRemov
|
||||
|
||||
foreach (DailyNoteEntry entry in appDbContext.DailyNotes.Include(n => n.User))
|
||||
{
|
||||
Web.Response.Response<WebDailyNote> dailyNoteResponse = await gameRecordClient
|
||||
Web.Response.Response<WebDailyNote> 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)
|
||||
{
|
||||
|
||||
@@ -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<GameAuthKey> authkeyResponse = await bindingClient2.GenerateAuthenticationKeyAsync(userAndUid.User, data).ConfigureAwait(false);
|
||||
|
||||
|
||||
@@ -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<SpiralAbyssEntry>? spiralAbysses;
|
||||
@@ -30,10 +31,11 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
|
||||
/// </summary>
|
||||
/// <param name="appDbContext">数据库上下文</param>
|
||||
/// <param name="gameRecordClient">游戏记录客户端</param>
|
||||
public SpiralAbyssRecordService(AppDbContext appDbContext, GameRecordClient gameRecordClient)
|
||||
public SpiralAbyssRecordService(AppDbContext appDbContext, GameRecordClient gameRecordClient, GameRecordClientOs gameRecordClientOs)
|
||||
{
|
||||
this.appDbContext = appDbContext;
|
||||
this.gameRecordClient = gameRecordClient;
|
||||
this.gameRecordClientOs = gameRecordClientOs;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -70,9 +72,21 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
|
||||
|
||||
private async Task RefreshSpiralAbyssCoreAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule)
|
||||
{
|
||||
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> response = await gameRecordClient
|
||||
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> 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())
|
||||
{
|
||||
|
||||
@@ -48,6 +48,13 @@ internal interface IUserService
|
||||
/// <returns>处理的结果</returns>
|
||||
Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie);
|
||||
|
||||
/// <summary>
|
||||
/// 尝试异步处理国际服 Cookie
|
||||
/// </summary>
|
||||
/// <param name="cookie">Cookie,需包含 stuid, stoken 字段</param>
|
||||
/// <returns>处理的结果</returns>
|
||||
Task<ValueResult<UserOptionResult, string>> ProcessInputOsCookieAsync(Cookie cookie);
|
||||
|
||||
/// <summary>
|
||||
/// 异步刷新 Cookie 的 CookieToken
|
||||
/// </summary>
|
||||
|
||||
@@ -225,7 +225,49 @@ 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? 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<AppDbContext>();
|
||||
|
||||
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<UidCookieToken> cookieTokenResponse = await scope.ServiceProvider
|
||||
Response<UidCookieToken> cookieTokenResponse;
|
||||
if (user.Entity.IsOversea)
|
||||
{
|
||||
cookieTokenResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<PassportClientOs>()
|
||||
.GetCookieAccountInfoBySTokenAsync(user.Entity)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
cookieTokenResponse = await scope.ServiceProvider
|
||||
.GetRequiredService<PassportClient2>()
|
||||
.GetCookieAccountInfoBySTokenAsync(user.Entity)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (cookieTokenResponse.IsOk())
|
||||
{
|
||||
@@ -265,14 +318,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<Page
|
||||
x:Class="Snap.Hutao.View.Page.LoginHoyoverseUserPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:shcm="using:Snap.Hutao.Control.Markup"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid Loaded="OnRootLoaded">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
Margin="12,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="{shcm:ResourceString Name=ViewPageLoginMihoyoUserTitle}"/>
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Margin="16"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBox
|
||||
Grid.Column="0"
|
||||
Width="240"
|
||||
MaxLength="9"
|
||||
x:Name="UidInput"
|
||||
Margin="0,0,16,0"
|
||||
PlaceholderText="Please input your user id here."
|
||||
HorizontalAlignment="Right"/>
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
RelativePanel.RightOf="UidInput"
|
||||
Click="CookieButtonClick"
|
||||
Content="{shcm:ResourceString Name=ViewPageLoginMihoyoUserLoggedInAction}"/>
|
||||
</Grid>
|
||||
|
||||
<WebView2
|
||||
x:Name="WebView"
|
||||
Grid.Row="2"
|
||||
Margin="0,0,0,0"/>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,124 @@
|
||||
// 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.View.Dialog;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Passport;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Diagnostics.Eventing.Reader;
|
||||
|
||||
namespace Snap.Hutao.View.Page;
|
||||
|
||||
/// <summary>
|
||||
/// <20><>¼<EFBFBD><EFBFBD><D7B9><EFBFBD>ͨ<EFBFBD><CDA8>֤ҳ<D6A4><D2B3>
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Controls.Page
|
||||
{
|
||||
/// <summary>
|
||||
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µĵ<C2B5>¼<EFBFBD><EFBFBD><D7B9><EFBFBD>ͨ<EFBFBD><CDA8>֤ҳ<D6A4><D2B3>
|
||||
/// </summary>
|
||||
public LoginHoyoverseUserPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
[SuppressMessage("", "VSTHRD100")]
|
||||
private async void OnRootLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await WebView.EnsureCoreWebView2Async();
|
||||
|
||||
|
||||
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
|
||||
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://account.hoyolab.com");
|
||||
foreach (CoreWebView2Cookie item in cookies)
|
||||
{
|
||||
manager.DeleteCookie(item);
|
||||
}
|
||||
|
||||
WebView.CoreWebView2.Navigate("https://account.hoyolab.com/#/login");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Ioc.Default.GetRequiredService<IInfoBarService>().Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleCurrentCookieAsync(CancellationToken token)
|
||||
{
|
||||
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
|
||||
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://account.hoyolab.com");
|
||||
|
||||
// Get user id from text input, login_uid is missed in cookie
|
||||
string uid = UidInput.Text;
|
||||
|
||||
if (uid.Length != 9)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Cookie loginTicketCookie = Cookie.FromCoreWebView2Cookies(cookies);
|
||||
loginTicketCookie["login_uid"] = uid;
|
||||
|
||||
// ʹ<><CAB9> loginTicket <20><>ȡ stoken
|
||||
Response<ListWrapper<NameToken>> multiTokenResponse = await Ioc.Default
|
||||
.GetRequiredService<AuthClientOs>()
|
||||
.GetMultiTokenByLoginTicketAsync(loginTicketCookie, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!multiTokenResponse.IsOk())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, string> multiTokenMap = multiTokenResponse.Data.List.ToDictionary(n => n.Name, n => n.Token);
|
||||
Cookie hoyoLabCookie = Cookie.Parse($"stoken={multiTokenMap["stoken"]}; stuid={uid}");
|
||||
|
||||
// <20><><EFBFBD><EFBFBD> cookie <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
|
||||
(UserOptionResult result, string nickname) = await Ioc.Default
|
||||
.GetRequiredService<IUserService>()
|
||||
.ProcessInputOsCookieAsync(hoyoLabCookie)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
Ioc.Default.GetRequiredService<INavigationService>().GoBack();
|
||||
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
|
||||
|
||||
switch (result)
|
||||
{
|
||||
case UserOptionResult.Added:
|
||||
ViewModel.UserViewModel vm = Ioc.Default.GetRequiredService<ViewModel.UserViewModel>();
|
||||
if (vm.Users!.Count == 1)
|
||||
{
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
vm.SelectedUser = vm.Users.Single();
|
||||
}
|
||||
|
||||
infoBarService.Success($"<22>û<EFBFBD> [{nickname}] <20><><EFBFBD>ӳɹ<D3B3>");
|
||||
break;
|
||||
case UserOptionResult.Incomplete:
|
||||
infoBarService.Information($"<22><> Cookie <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> user id <20><><EFBFBD><EFBFBD><F3A3ACB2><EFBFBD>ʧ<EFBFBD><CAA7>");
|
||||
break;
|
||||
case UserOptionResult.Invalid:
|
||||
infoBarService.Information($"<22><> Cookie <20><>Ч<EFBFBD><D0A7><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>");
|
||||
break;
|
||||
case UserOptionResult.Updated:
|
||||
infoBarService.Success($"<22>û<EFBFBD> [{nickname}] <20><><EFBFBD>³ɹ<C2B3>");
|
||||
break;
|
||||
default:
|
||||
throw Must.NeverHappen();
|
||||
}
|
||||
}
|
||||
|
||||
private void CookieButtonClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
HandleCurrentCookieAsync(CancellationToken.None).SafeForget();
|
||||
}
|
||||
}
|
||||
@@ -225,6 +225,23 @@
|
||||
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 LoginHoyoverseUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationLoginMihoyoUserAction}"/>
|
||||
<AppBarButton
|
||||
Command="{Binding AddOsUserCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Label="{shcm:ResourceString Name=ViewUserCookieOperationManualInputAction}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
|
||||
@@ -247,9 +247,17 @@ internal sealed class DailyNoteViewModel : Abstraction.ViewModel
|
||||
{
|
||||
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
|
||||
{
|
||||
// ContentDialog must be created by main thread.
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await new DailyNoteVerificationDialog(userAndUid).ShowAsync();
|
||||
// TODO: Add verify support for oversea user
|
||||
if (userAndUid.User.IsOversea)
|
||||
{
|
||||
serviceProvider.GetRequiredService<IInfoBarService>().Warning("Unsupported for hoyoverse account");
|
||||
}
|
||||
else
|
||||
{
|
||||
// ContentDialog must be created by main thread.
|
||||
await ThreadHelper.SwitchToMainThreadAsync();
|
||||
await new DailyNoteVerificationDialog(userAndUid).ShowAsync();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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<User>(RemoveUserAsync);
|
||||
CopyCookieCommand = new RelayCommand<User>(CopyCookie);
|
||||
RefreshCookieTokenCommand = new AsyncRelayCommand(RefreshCookieTokenAsync);
|
||||
@@ -87,11 +89,21 @@ internal sealed class UserViewModel : ObservableObject
|
||||
/// </summary>
|
||||
public ICommand AddUserCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 添加国际服用户命令
|
||||
/// </summary>
|
||||
public ICommand AddOsUserCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 登录米游社命令
|
||||
/// </summary>
|
||||
public ICommand LoginMihoyoUserCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 登录米游社命令
|
||||
/// </summary>
|
||||
public ICommand LoginHoyoverseUserCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 移除用户命令
|
||||
/// </summary>
|
||||
@@ -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<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)
|
||||
@@ -173,6 +226,21 @@ internal sealed class UserViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开浏览器登录 hoyolab 以获取 cookie
|
||||
/// </summary>
|
||||
private void LoginHoyoverseUser()
|
||||
{
|
||||
if (Core.WebView2Helper.IsSupported)
|
||||
{
|
||||
serviceProvider.GetRequiredService<INavigationService>().Navigate<LoginHoyoverseUserPage>(INavigationAwaiter.Default);
|
||||
}
|
||||
else
|
||||
{
|
||||
infoBarService.Warning(SH.CoreWebView2HelperVersionUndetected);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveUserAsync(User? user)
|
||||
{
|
||||
if (user != null)
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore.Sqlite.Query.Internal;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.Service.Game;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
|
||||
namespace Snap.Hutao.Web;
|
||||
|
||||
@@ -13,6 +17,178 @@ namespace Snap.Hutao.Web;
|
||||
[SuppressMessage("", "SA1124")]
|
||||
internal static class ApiOsEndpoints
|
||||
{
|
||||
#region ApiGeetest
|
||||
|
||||
/// <summary>
|
||||
/// 获取GT码
|
||||
/// </summary>
|
||||
/// <param name="gt">gt</param>
|
||||
/// <returns>GT码Url</returns>
|
||||
public static string GeetestGetType(string gt)
|
||||
{
|
||||
return $"{ApiNaGeetest}/gettype.php?gt={gt}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证接口
|
||||
/// </summary>
|
||||
/// <param name="gt">gt</param>
|
||||
/// <param name="challenge">challenge流水号</param>
|
||||
/// <returns>验证接口Url</returns>
|
||||
public static string GeetestAjax(string gt, string challenge)
|
||||
{
|
||||
return $"{ApiNaGeetest}/ajax.php?gt={gt}&challenge={challenge}&lang=zh-cn&pt=0&client_type=web";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ApiOsTakumiAuthApi
|
||||
|
||||
/// <summary>
|
||||
/// 获取 stoken 与 ltoken
|
||||
/// </summary>
|
||||
/// <param name="loginTicket">登录票证</param>
|
||||
/// <param name="loginUid">uid</param>
|
||||
/// <returns>Url</returns>
|
||||
public static string AuthMultiToken(string loginTicket, string loginUid)
|
||||
{
|
||||
return $"{ApiAccountOsAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 stoken 与 ltoken
|
||||
/// </summary>
|
||||
/// <param name="actionType">操作类型 game_role</param>
|
||||
/// <param name="stoken">Stoken</param>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>Url</returns>
|
||||
public static string AuthActionTicket(string actionType, string stoken, string uid)
|
||||
{
|
||||
return $"{ApiAccountOsAuthApi}/getActionTicketBySToken?action_type={actionType}&stoken={Uri.EscapeDataString(stoken)}&uid={uid}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ApiOsTaKumiApi
|
||||
|
||||
/// <summary>
|
||||
/// 用户游戏角色
|
||||
/// </summary>
|
||||
/// <returns>用户游戏角色字符串</returns>
|
||||
public const string UserGameRolesByCookie = $"{ApiOsTaKumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_global";
|
||||
|
||||
/// <summary>
|
||||
/// 用户游戏角色
|
||||
/// </summary>
|
||||
/// <param name="region">地区代号</param>
|
||||
/// <returns>用户游戏角色字符串</returns>
|
||||
public static string UserGameRolesByLtoken(string region)
|
||||
{
|
||||
return $"{ApiAccountOsBindingApi}/getUserGameRolesByLtoken?game_biz=hk4e_global®ion={region}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region SgPublicApi
|
||||
|
||||
/// <summary>
|
||||
/// 计算器家具计算
|
||||
/// </summary>
|
||||
public const string CalculateOsFurnitureCompute = $"{SgPublicApi}/event/calculateos/furniture/list";
|
||||
|
||||
/// <summary>
|
||||
/// 计算器角色列表 size 20
|
||||
/// </summary>
|
||||
public const string CalculateOsAvatarList = $"{SgPublicApi}/event/calculateos/avatar/list";
|
||||
|
||||
/// <summary>
|
||||
/// 计算器武器列表 size 20
|
||||
/// </summary>
|
||||
public const string CalculateOsWeaponList = $"{SgPublicApi}/event/calculateos/weapon/list";
|
||||
|
||||
/// <summary>
|
||||
/// 计算器结果
|
||||
/// </summary>
|
||||
public const string CalculateOsCompute = $"{SgPublicApi}/event/calculateos/compute";
|
||||
|
||||
/// <summary>
|
||||
/// 计算器同步角色详情 size 20
|
||||
/// </summary>
|
||||
/// <param name="avatarId">角色Id</param>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>角色详情</returns>
|
||||
public static string CalculateOsSyncAvatarDetail(AvatarId avatarId, PlayerUid uid)
|
||||
{
|
||||
return $"{SgPublicApi}/event/calculateos/sync/avatar/detail?avatar_id={avatarId.Value}&uid={uid.Value}®ion={uid.Region}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算器同步角色列表 size 20
|
||||
/// </summary>
|
||||
public const string CalculateOsSyncAvatarList = $"{SgPublicApi}/event/calculateos/sync/avatar/list";
|
||||
|
||||
#endregion
|
||||
|
||||
#region BbsApiOsApi
|
||||
|
||||
/// <summary>
|
||||
/// 查询其他用户详细信息
|
||||
/// </summary>
|
||||
/// <param name="bbsUid">bbs Uid</param>
|
||||
/// <returns>查询其他用户详细信息字符串</returns>
|
||||
public static string UserFullInfoQuery(string bbsUid)
|
||||
{
|
||||
return $"{BbsApiOs}/community/painter/wapi/user/full";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 国际服角色基本信息
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>角色基本信息字符串</returns>
|
||||
public static string GameRecordRoleBasicInfo(PlayerUid uid)
|
||||
{
|
||||
return $"{BbsApiOsGameRecordApi}/roleBasicInfo?role_id={uid.Value}&server={uid.Region}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 国际服角色信息
|
||||
/// </summary>
|
||||
public const string GameRecordCharacter = $"{BbsApiOsGameRecordApi}/character";
|
||||
|
||||
/// <summary>
|
||||
/// 国际服游戏记录实时便笺
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>游戏记录实时便笺字符串</returns>
|
||||
public static string GameRecordDailyNote(PlayerUid uid)
|
||||
{
|
||||
return $"{BbsApiOsGameRecordApi}/dailyNote?server={uid.Region}&role_id={uid.Value}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 国际服游戏记录主页
|
||||
/// </summary>
|
||||
/// <param name="uid">uid</param>
|
||||
/// <returns>游戏记录主页字符串</returns>
|
||||
public static string GameRecordIndex(PlayerUid uid)
|
||||
{
|
||||
return $"{BbsApiOsGameRecordApi}/index?server={uid.Region}&role_id={uid.Value}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 国际服深渊信息
|
||||
/// </summary>
|
||||
/// <param name="scheduleType">深渊类型</param>
|
||||
/// <param name="uid">Uid</param>
|
||||
/// <returns>深渊信息字符串</returns>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
@@ -26,6 +202,25 @@ internal static class ApiOsEndpoints
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ApiAccountOsApi
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab App Login api
|
||||
/// Can fetch stoken
|
||||
/// </summary>
|
||||
public const string WebLoginByPassword = $"{ApiAccountOsAuthApi}/webLoginByPassword";
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Ltoken
|
||||
/// </summary>
|
||||
public const string AccountGetLtokenByStoken = $"{ApiAccountOsAuthApi}/getLTokenBySToken";
|
||||
|
||||
/// <summary>
|
||||
/// fetch CookieToken
|
||||
/// </summary>
|
||||
public const string AccountGetCookieTokenBySToken = $"{ApiAccountOsAuthApi}/getCookieAccountInfoBySToken";
|
||||
#endregion
|
||||
|
||||
#region SdkStaticLauncherApi
|
||||
|
||||
/// <summary>
|
||||
@@ -40,10 +235,31 @@ internal static class ApiOsEndpoints
|
||||
#endregion
|
||||
|
||||
#region Hosts | Queries
|
||||
private const string ApiNaGeetest = "https://api-na.geetest.com";
|
||||
|
||||
private const string ApiOsTaKumi = "https://api-os-takumi.hoyoverse.com";
|
||||
private const string ApiOsTaKumiBindingApi = $"{ApiOsTaKumi}/binding/api";
|
||||
|
||||
private const string ApiAccountOs = "https://api-account-os.hoyolab.com";
|
||||
private const string ApiAccountOsBindingApi = $"{ApiAccountOs}/binding/api";
|
||||
private const string ApiAccountOsAuthApi = $"{ApiAccountOs}/account/auth/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";
|
||||
|
||||
private const string SdkOsStatic = "https://sdk-os-static.mihoyo.com";
|
||||
private const string SdkOsStaticLauncherApi = $"{SdkOsStatic}/hk4e_global/mdk/launcher/api";
|
||||
|
||||
private const string SgPublicApi = "https://sg-public-api.hoyolab.com";
|
||||
|
||||
/// <summary>
|
||||
/// Web static referer
|
||||
/// </summary>
|
||||
public const string WebStaticSeaMihoyoReferer = "https://webstatic-sea.mihoyo.com";
|
||||
public const string ActHoyolabReferer = "https://act.hoyolab.com/";
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -23,6 +23,17 @@ internal static class CoreWebView2Extension
|
||||
return webView;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置 移动端UA
|
||||
/// </summary>
|
||||
/// <param name="webView">webview2</param>
|
||||
/// <returns>链式调用的WebView2</returns>
|
||||
public static CoreWebView2 SetOsMobileUserAgent(this CoreWebView2 webView)
|
||||
{
|
||||
webView.Settings.UserAgent = Core.CoreEnvironment.HoyolabOsMobileUA;
|
||||
return webView;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置WebView2的Cookie
|
||||
/// </summary>
|
||||
|
||||
@@ -52,4 +52,22 @@ internal sealed class UserClient
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前用户详细信息,使用 Ltoken
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>详细信息</returns>
|
||||
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OS)]
|
||||
public async Task<Response<UserFullInfoWrapper>> GetOsUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
|
||||
{
|
||||
Response<UserFullInfoWrapper>? resp = await httpClient
|
||||
.SetUser(user, CookieType.LToken)
|
||||
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OS, false)
|
||||
.TryCatchGetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiOsEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,31 @@ internal sealed partial class Cookie
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提取其中的 stoken 信息
|
||||
/// </summary>
|
||||
/// <param name="cookie">含有 Stoken 的 cookie</param>
|
||||
/// <returns>是否获取成功</returns>
|
||||
public bool TryGetAsStokenV1([NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
bool hasStoken = TryGetValue(STOKEN, out string? stoken);
|
||||
bool hasStuid = TryGetValue(STUID, out string? stuid);
|
||||
|
||||
if (hasStoken && hasStuid)
|
||||
{
|
||||
cookie = new Cookie(new()
|
||||
{
|
||||
[STOKEN] = stoken!,
|
||||
[STUID] = stuid!,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
cookie = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetAsLtoken([NotNullWhen(true)] out Cookie? cookie)
|
||||
{
|
||||
bool hasLtoken = TryGetValue(LTOKEN, out string? ltoken);
|
||||
|
||||
@@ -38,4 +38,9 @@ internal enum SaltType
|
||||
/// LK2
|
||||
/// </summary>
|
||||
LK2,
|
||||
|
||||
/// <summary>
|
||||
/// 国际服
|
||||
/// </summary>
|
||||
OS,
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Web.Hoyolab.Annotation;
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using Windows.ApplicationModel.Contacts;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Passport;
|
||||
|
||||
/// <summary>
|
||||
/// 通行证客户端 XRPC 版
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[UseDynamicSecret]
|
||||
[HttpClient(HttpClientConfigration.XRpc3)]
|
||||
internal sealed class PassportClientOs
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<PassportClient> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的国际服通行证客户端
|
||||
/// </summary>
|
||||
/// <param name="httpClient">http客户端</param>
|
||||
/// <param name="options">json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public PassportClientOs(HttpClient httpClient, JsonSerializerOptions options, ILogger<PassportClient> logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.options = options;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取 CookieToken
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>cookie token</returns>
|
||||
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.None)]
|
||||
public async Task<Response<UidCookieToken>> GetCookieAccountInfoBySTokenAsync(User user, CancellationToken token = default)
|
||||
{
|
||||
Response<UidCookieToken>? resp = null;
|
||||
string? stoken = user.SToken["stoken"];
|
||||
|
||||
if (stoken == null || user.Aid == null)
|
||||
{
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
StokenData data = new(stoken, user.Aid);
|
||||
resp = await httpClient
|
||||
.SetUser(user, CookieType.SToken)
|
||||
.TryCatchPostAsJsonAsync<StokenData, Response<UidCookieToken>>(ApiOsEndpoints.AccountGetCookieTokenBySToken, data, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取 Ltoken
|
||||
/// </summary>
|
||||
/// <param name="user">用户</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>uid 与 cookie token</returns>
|
||||
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.None)]
|
||||
public async Task<Response<LtokenWrapper>> GetLtokenBySTokenAsync(User user, CancellationToken token)
|
||||
{
|
||||
Response<LtokenWrapper>? resp = null;
|
||||
string? stoken = user.SToken["stoken"];
|
||||
|
||||
if (stoken == null || user.Aid == null)
|
||||
{
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
StokenData data = new(stoken, user.Aid);
|
||||
resp = await httpClient
|
||||
.SetUser(user, CookieType.SToken)
|
||||
.TryCatchPostAsJsonAsync<StokenData, Response<LtokenWrapper>>(ApiOsEndpoints.AccountGetLtokenByStoken, data, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
private class StokenData
|
||||
{
|
||||
public StokenData(string stoken, string uid)
|
||||
{
|
||||
Stoken = stoken;
|
||||
Uid = uid;
|
||||
}
|
||||
|
||||
[JsonPropertyName("stoken")]
|
||||
public string Stoken { get; set; }
|
||||
|
||||
[JsonPropertyName("uid")]
|
||||
public string Uid { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Web.Hoyolab.Annotation;
|
||||
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Net.Http;
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.Takumi.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Hoyolab 授权客户端
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[UseDynamicSecret]
|
||||
[HttpClient(HttpClientConfigration.Default)]
|
||||
internal sealed class AuthClientOs
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<BindingClient> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的 Hoyolab 授权客户端
|
||||
/// </summary>
|
||||
/// <param name="httpClient">Http客户端</param>
|
||||
/// <param name="options">Json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public AuthClientOs(HttpClient httpClient, JsonSerializerOptions options, ILogger<BindingClient> logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.options = options;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 MultiToken
|
||||
/// </summary>
|
||||
/// <param name="cookie">login cookie</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>包含token的字典</returns>
|
||||
public async Task<Response<ListWrapper<NameToken>>> GetMultiTokenByLoginTicketAsync(Cookie cookie, CancellationToken token)
|
||||
{
|
||||
string loginTicket = cookie["login_ticket"];
|
||||
string loginUid = cookie["login_uid"];
|
||||
|
||||
Response<ListWrapper<NameToken>>? resp = await httpClient
|
||||
.TryCatchGetFromJsonAsync<Response<ListWrapper<NameToken>>>(ApiOsEndpoints.AuthMultiToken(loginTicket, loginUid), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,11 +43,24 @@ internal sealed class CalculateClient
|
||||
[ApiInformation(Cookie = CookieType.Cookie)]
|
||||
public async Task<Response<Consumption>> ComputeAsync(Model.Entity.User user, AvatarPromotionDelta delta, CancellationToken token = default)
|
||||
{
|
||||
Response<Consumption>? resp = await httpClient
|
||||
Response<Consumption>? resp;
|
||||
|
||||
if (user.IsOversea)
|
||||
{
|
||||
resp = await httpClient
|
||||
.SetUser(user, CookieType.Cookie)
|
||||
.SetReferer(ApiOsEndpoints.ActHoyolabReferer)
|
||||
.TryCatchPostAsJsonAsync<AvatarPromotionDelta, Response<Consumption>>(ApiOsEndpoints.CalculateOsCompute, delta, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
resp = await httpClient
|
||||
.SetUser(user, CookieType.Cookie)
|
||||
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
|
||||
.TryCatchPostAsJsonAsync<AvatarPromotionDelta, Response<Consumption>>(ApiEndpoints.CalculateCompute, delta, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
@@ -65,14 +78,28 @@ internal sealed class CalculateClient
|
||||
|
||||
List<Avatar> avatars = new();
|
||||
Response<ListWrapper<Avatar>>? resp;
|
||||
httpClient.SetUser(userAndUid.User, CookieType.CookieToken);
|
||||
|
||||
// 根据 uid 所属服务器选择 referer 与 api
|
||||
string referer = ApiOsEndpoints.ActHoyolabReferer;
|
||||
string endpoint = ApiOsEndpoints.CalculateOsSyncAvatarList;
|
||||
|
||||
if (userAndUid.Uid.Region == "cn_gf01" || userAndUid.Uid.Region == "cn_qd01")
|
||||
{
|
||||
referer = ApiEndpoints.WebStaticMihoyoReferer;
|
||||
endpoint = ApiEndpoints.CalculateSyncAvatarList;
|
||||
httpClient.SetUser(userAndUid.User, CookieType.CookieToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
httpClient.SetUser(userAndUid.User, CookieType.Cookie);
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
filter.Page = currentPage++;
|
||||
resp = await httpClient
|
||||
.SetReferer(ApiEndpoints.WebStaticMihoyoReferer)
|
||||
.TryCatchPostAsJsonAsync<SyncAvatarFilter, Response<ListWrapper<Avatar>>>(ApiEndpoints.CalculateSyncAvatarList, filter, options, logger, token)
|
||||
.SetReferer(referer)
|
||||
.TryCatchPostAsJsonAsync<SyncAvatarFilter, Response<ListWrapper<Avatar>>>(endpoint, filter, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (resp != null && resp.IsOk())
|
||||
@@ -101,11 +128,21 @@ internal sealed class CalculateClient
|
||||
/// <returns>角色详情</returns>
|
||||
public async Task<Response<AvatarDetail>> GetAvatarDetailAsync(UserAndUid userAndUid, Avatar avatar, CancellationToken token = default)
|
||||
{
|
||||
Response<AvatarDetail>? resp = await httpClient
|
||||
Response<AvatarDetail>? resp;
|
||||
if (userAndUid.Uid.Region == "cn_gf01" || userAndUid.Uid.Region == "cn_qd01")
|
||||
{
|
||||
resp = await httpClient
|
||||
.SetUser(userAndUid.User, CookieType.CookieToken)
|
||||
.TryCatchGetFromJsonAsync<Response<AvatarDetail>>(ApiEndpoints.CalculateSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
resp = await httpClient
|
||||
.SetUser(userAndUid.User, CookieType.Cookie)
|
||||
.TryCatchGetFromJsonAsync<Response<AvatarDetail>>(ApiOsEndpoints.CalculateOsSyncAvatarDetail(avatar.Id, userAndUid.Uid.Value), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Hoyoverse game record provider
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[UseDynamicSecret]
|
||||
[HttpClient(HttpClientConfigration.XRpc3)]
|
||||
[PrimaryHttpMessageHandler(UseCookies = false)]
|
||||
internal sealed class GameRecordClientOs
|
||||
{
|
||||
private readonly HttpClient httpClient;
|
||||
private readonly JsonSerializerOptions options;
|
||||
private readonly ILogger<GameRecordClient> logger;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的游戏记录提供器
|
||||
/// </summary>
|
||||
/// <param name="httpClient">请求器</param>
|
||||
/// <param name="options">json序列化选项</param>
|
||||
/// <param name="logger">日志器</param>
|
||||
public GameRecordClientOs(HttpClient httpClient, JsonSerializerOptions options, ILogger<GameRecordClient> logger)
|
||||
{
|
||||
this.httpClient = httpClient;
|
||||
this.options = options;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取实时便笺
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>实时便笺</returns>
|
||||
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OS)]
|
||||
public async Task<Response<DailyNote.DailyNote>> GetDailyNoteAsync(UserAndUid userAndUid, CancellationToken token = default)
|
||||
{
|
||||
Response<DailyNote.DailyNote>? resp = await httpClient
|
||||
.SetUser(userAndUid.User, CookieType.Cookie)
|
||||
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OS, false)
|
||||
.TryCatchGetFromJsonAsync<Response<DailyNote.DailyNote>>(ApiOsEndpoints.GameRecordDailyNote(userAndUid.Uid.Value), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取玩家基础信息
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>玩家的基础信息</returns>
|
||||
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OS)]
|
||||
public async Task<Response<PlayerInfo>> GetPlayerInfoAsync(UserAndUid userAndUid, CancellationToken token = default)
|
||||
{
|
||||
Response<PlayerInfo>? resp = await httpClient
|
||||
.SetUser(userAndUid.User, CookieType.Cookie)
|
||||
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OS, false)
|
||||
.TryCatchGetFromJsonAsync<Response<PlayerInfo>>(ApiOsEndpoints.GameRecordIndex(userAndUid.Uid), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取玩家深渊信息
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户</param>
|
||||
/// <param name="schedule">1:当期,2:上期</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>深渊信息</returns>
|
||||
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OS)]
|
||||
public async Task<Response<SpiralAbyss.SpiralAbyss>> GetSpiralAbyssAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule, CancellationToken token = default)
|
||||
{
|
||||
Response<SpiralAbyss.SpiralAbyss>? resp = await httpClient
|
||||
.SetUser(userAndUid.User, CookieType.Cookie)
|
||||
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OS, false)
|
||||
.TryCatchGetFromJsonAsync<Response<SpiralAbyss.SpiralAbyss>>(ApiOsEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid), options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取玩家角色详细信息
|
||||
/// </summary>
|
||||
/// <param name="userAndUid">用户与角色</param>
|
||||
/// <param name="playerInfo">玩家的基础信息</param>
|
||||
/// <param name="token">取消令牌</param>
|
||||
/// <returns>角色列表</returns>
|
||||
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.X4)]
|
||||
public async Task<Response<CharacterWrapper>> GetCharactersAsync(UserAndUid userAndUid, PlayerInfo playerInfo, CancellationToken token = default)
|
||||
{
|
||||
CharacterData data = new(userAndUid.Uid, playerInfo.Avatars.Select(x => x.Id));
|
||||
|
||||
Response<CharacterWrapper>? resp = await httpClient
|
||||
.SetUser(userAndUid.User, CookieType.Cookie)
|
||||
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OS, false)
|
||||
.TryCatchPostAsJsonAsync<CharacterData, Response<CharacterWrapper>>(ApiOsEndpoints.GameRecordCharacter, data, options, logger, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
return Response.Response.DefaultIfNull(resp);
|
||||
}
|
||||
|
||||
private class CharacterData
|
||||
{
|
||||
public CharacterData(PlayerUid uid, IEnumerable<AvatarId> characterIds)
|
||||
{
|
||||
CharacterIds = characterIds;
|
||||
Uid = uid.Value;
|
||||
Server = uid.Region;
|
||||
}
|
||||
|
||||
[JsonPropertyName("character_ids")]
|
||||
public IEnumerable<AvatarId> CharacterIds { get; }
|
||||
|
||||
[JsonPropertyName("role_id")]
|
||||
public string Uid { get; }
|
||||
|
||||
[JsonPropertyName("server")]
|
||||
public string Server { get; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user