This commit is contained in:
DismissedLight
2023-03-27 16:17:36 +08:00
parent 09abb46159
commit 97cbe7cf55
63 changed files with 947 additions and 867 deletions

4
.gitignore vendored
View File

@@ -15,5 +15,5 @@ src/Snap.Hutao/Snap.Hutao.Installer/Properties/PublishProfiles/FolderProfile.pub
src/Snap.Hutao/Snap.Hutao.SourceGeneration/bin/
src/Snap.Hutao/Snap.Hutao.SourceGeneration/obj/
src/Snap.Hutao/Snap.Hutao.Win32/bin/
src/Snap.Hutao/Snap.Hutao.Win32/obj/
src/Snap.Hutao/Snap.Hutao.Test/bin/
src/Snap.Hutao/Snap.Hutao.Test/obj/

View File

@@ -45,8 +45,6 @@ public class HttpClientGenerator : ISourceGenerator
return;
}
string toolName = this.GetGeneratorType().FullName;
StringBuilder sourceCodeBuilder = new();
sourceCodeBuilder.Append($$"""
@@ -63,13 +61,13 @@ public class HttpClientGenerator : ISourceGenerator
internal static partial class IocHttpClientConfiguration
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{toolName}}","1.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(HttpClientGenerator)}}","1.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static partial IServiceCollection AddHttpClients(this IServiceCollection services)
{
""");
FillWithInjectionServices(receiver, sourceCodeBuilder);
FillWithHttpClients(receiver, sourceCodeBuilder);
sourceCodeBuilder.Append("""
@@ -81,7 +79,7 @@ public class HttpClientGenerator : ISourceGenerator
context.AddSource("IocHttpClientConfiguration.g.cs", SourceText.From(sourceCodeBuilder.ToString(), Encoding.UTF8));
}
private static void FillWithInjectionServices(HttpClientSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
private static void FillWithHttpClients(HttpClientSyntaxContextReceiver receiver, StringBuilder sourceCodeBuilder)
{
List<string> lines = new();
StringBuilder lineBuilder = new();
@@ -90,16 +88,23 @@ public class HttpClientGenerator : ISourceGenerator
{
lineBuilder.Clear().Append("\r\n");
lineBuilder.Append(@" services.AddHttpClient<");
lineBuilder.Append($"{classSymbol.ToDisplayString()}>(");
AttributeData httpClientInfo = classSymbol
.GetAttributes()
.Single(attr => attr.AttributeClass!.ToDisplayString() == HttpClientSyntaxContextReceiver.AttributeName);
ImmutableArray<TypedConstant> arguments = httpClientInfo.ConstructorArguments;
TypedConstant injectAs = arguments[0];
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.Value}, ");
}
string injectAsName = injectAs.ToCSharpString();
TypedConstant configuration = arguments[0];
lineBuilder.Append($"{classSymbol.ToDisplayString()}>(");
string injectAsName = configuration.ToCSharpString();
switch (injectAsName)
{
case DefaultName:

View File

@@ -41,8 +41,6 @@ public class InjectionGenerator : ISourceGenerator
return;
}
string toolName = this.GetGeneratorType().FullName;
StringBuilder sourceCodeBuilder = new();
sourceCodeBuilder.Append($$"""
// Copyright (c) DGP Studio. All rights reserved.
@@ -56,7 +54,7 @@ public class InjectionGenerator : ISourceGenerator
internal static partial class ServiceCollectionExtension
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{toolName}}","1.0.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("{{nameof(InjectionGenerator)}}","1.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
public static partial IServiceCollection AddInjections(this IServiceCollection services)
{
@@ -64,6 +62,7 @@ public class InjectionGenerator : ISourceGenerator
FillWithInjectionServices(receiver, sourceCodeBuilder);
sourceCodeBuilder.Append("""
return services;
}
}
@@ -79,46 +78,42 @@ public class InjectionGenerator : ISourceGenerator
foreach (INamedTypeSymbol classSymbol in receiver.Classes)
{
IEnumerable<AttributeData> datas = classSymbol
AttributeData injectionInfo = classSymbol
.GetAttributes()
.Where(attr => attr.AttributeClass!.ToDisplayString() == InjectionSyntaxContextReceiver.AttributeName);
.Single(attr => attr.AttributeClass!.ToDisplayString() == InjectionSyntaxContextReceiver.AttributeName);
foreach (AttributeData injectionInfo in datas)
lineBuilder
.Clear()
.Append("\r\n");
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
TypedConstant injectAs = arguments[0];
string injectAsName = injectAs.ToCSharpString();
switch (injectAsName)
{
lineBuilder
.Clear()
.Append("\r\n");
ImmutableArray<TypedConstant> arguments = injectionInfo.ConstructorArguments;
TypedConstant injectAs = arguments[0];
string injectAsName = injectAs.ToCSharpString();
switch (injectAsName)
{
case InjectAsSingletonName:
lineBuilder.Append(@" services.AddSingleton(");
break;
case InjectAsTransientName:
lineBuilder.Append(@" services.AddTransient(");
break;
case InjectAsScopedName:
lineBuilder.Append(@" services.AddScoped(");
break;
default:
throw new InvalidOperationException($"非法的 InjectAs 值: [{injectAsName}]");
}
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.ToCSharpString()}, ");
}
lineBuilder.Append($"typeof({classSymbol.ToDisplayString()}));");
lines.Add(lineBuilder.ToString());
case InjectAsSingletonName:
lineBuilder.Append(@" services.AddSingleton<");
break;
case InjectAsTransientName:
lineBuilder.Append(@" services.AddTransient<");
break;
case InjectAsScopedName:
lineBuilder.Append(@" services.AddScoped<");
break;
default:
throw new InvalidOperationException($"非法的 InjectAs 值: [{injectAsName}]");
}
if (arguments.Length == 2)
{
TypedConstant interfaceType = arguments[1];
lineBuilder.Append($"{interfaceType.Value}, ");
}
lineBuilder.Append($"{classSymbol.ToDisplayString()}>();");
lines.Add(lineBuilder.ToString());
}
foreach (string line in lines.OrderBy(x => x))

View File

@@ -0,0 +1,32 @@
using Microsoft.Extensions.DependencyInjection;
using System;
namespace Snap.Hutao.Test;
[TestClass]
public class DependencyInjectionTest
{
[TestMethod]
public void OriginalTypeDiscoverable()
{
IServiceProvider services = new ServiceCollection()
.AddSingleton<IService, ServiceA>()
.AddSingleton<IService, ServiceB>()
.BuildServiceProvider();
Assert.IsNotNull(services.GetService<ServiceA>());
Assert.IsNotNull(services.GetService<ServiceB>());
}
private interface IService
{
}
private sealed class ServiceA : IService
{
}
private sealed class ServiceB : IService
{
}
}

View File

@@ -0,0 +1 @@
global using Microsoft.VisualStudio.TestTools.UnitTesting;

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>disable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.2" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.2" />
<PackageReference Include="coverlet.collector" Version="3.2.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.SourceGeneration", "Snap.Hutao.SourceGeneration\Snap.Hutao.SourceGeneration.csproj", "{8B96721E-5604-47D2-9B72-06FEBAD0CE00}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -64,6 +66,22 @@ Global
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x64.Build.0 = Release|x64
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.ActiveCfg = Release|Any CPU
{8B96721E-5604-47D2-9B72-06FEBAD0CE00}.Release|x86.Build.0 = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|arm64.ActiveCfg = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|arm64.Build.0 = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|x64.ActiveCfg = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|x64.Build.0 = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|x86.ActiveCfg = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Debug|x86.Build.0 = Debug|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|Any CPU.Build.0 = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|arm64.ActiveCfg = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|arm64.Build.0 = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.ActiveCfg = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x64.Build.0 = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.ActiveCfg = Release|Any CPU
{D691BA9F-904C-4229-87A5-E14F2EFF2F64}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -62,7 +62,7 @@ internal static class CoreEnvironment
[SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
// This SALT is not reliable
[SaltType.OS] = "6cqshh5dhw73bzxn20oexa9k516chk7s",
[SaltType.OSK2] = "6cqshh5dhw73bzxn20oexa9k516chk7s",
}.ToImmutableDictionary();
/// <summary>

View File

@@ -18,4 +18,13 @@ internal sealed class HttpClientAttribute : Attribute
public HttpClientAttribute(HttpClientConfiguration configration)
{
}
/// <summary>
/// 构造一个新的特性
/// </summary>
/// <param name="configration">配置</param>
/// <param name="interfaceType">实现的接口类型</param>
public HttpClientAttribute(HttpClientConfiguration configration, Type interfaceType)
{
}
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.DependencyInjection;
/// <summary>
/// 服务集合扩展
/// </summary>
internal static class EnumerableServiceExtension
{
/// <summary>
/// 选择对应的服务
/// </summary>
/// <typeparam name="TService">服务类型</typeparam>
/// <param name="services">服务集合</param>
/// <param name="name">名称</param>
/// <returns>对应的服务</returns>
public static TService Pick<TService>(this IEnumerable<TService> services, string name)
where TService : INamedService
{
return services.Single(s => s.Name == name);
}
/// <summary>
/// 选择对应的服务
/// </summary>
/// <typeparam name="TService">服务类型</typeparam>
/// <param name="services">服务集合</param>
/// <param name="isOversea">是否为海外服/Hoyolab</param>
/// <returns>对应的服务</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static TService Pick<TService>(this IEnumerable<TService> services, bool isOversea)
where TService : IOverseaSupport
{
return services.Single(s => s.IsOversea == isOversea);
}
/// <summary>
/// 选择对应的服务
/// </summary>
/// <typeparam name="TService">服务类型</typeparam>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="isOversea">是否为海外服/Hoyolab</param>
/// <returns>对应的服务</returns>
public static TService PickRequiredService<TService>(this IServiceProvider serviceProvider, bool isOversea)
where TService : IOverseaSupport
{
return serviceProvider.GetRequiredService<IEnumerable<TService>>().Pick(isOversea);
}
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Core.DependencyInjection;
/// <summary>
/// 有名称的对象

View File

@@ -0,0 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.DependencyInjection;
/// <summary>
/// 海外服/Hoyolab 可区分
/// </summary>
internal interface IOverseaSupport
{
/// <summary>
/// 是否为 海外服/Hoyolab
/// </summary>
public bool IsOversea { get; }
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Core.DependencyInjection;
/// <summary>
/// 命名服务扩展
/// </summary>
internal static class NamedServiceExtension
{
/// <summary>
/// 选择对应的服务
/// </summary>
/// <typeparam name="TService">服务类型</typeparam>
/// <param name="services">服务集合</param>
/// <param name="name">名称</param>
/// <returns>对应的服务</returns>
public static TService Pick<TService>(this IEnumerable<TService> services, string name)
where TService : INamedService
{
return services.Single(s => s.Name == name);
}
}

View File

@@ -6,6 +6,7 @@ global using CommunityToolkit.Mvvm.DependencyInjection;
// Microsoft
global using Microsoft;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Logging;
// Snap.Hutao

View File

@@ -103,11 +103,8 @@ internal sealed class User : ObservableObject
internal static async Task<User> ResumeAsync(EntityUser inner, CancellationToken token = default)
{
User user = new(inner);
bool isOk = user.Entity.IsOversea
? await user.InitializeCoreOsAsync(token).ConfigureAwait(false)
: await user.InitializeCoreAsync(token).ConfigureAwait(false);
if (!isOk)
if (!await user.InitializeCoreAsync(token).ConfigureAwait(false))
{
user.UserInfo = new() { Nickname = SH.ModelBindingUserInitializationFailed };
user.UserGameRoles = new();
@@ -120,15 +117,17 @@ internal sealed class User : ObservableObject
/// 创建并初始化用户
/// </summary>
/// <param name="cookie">cookie</param>
/// <param name="isOversea">是否为国际服</param>
/// <param name="token">取消令牌</param>
/// <returns>用户</returns>
internal static async Task<User?> CreateAsync(Cookie cookie, CancellationToken token = default)
internal static async Task<User?> CreateAsync(Cookie cookie, bool isOversea, CancellationToken token = default)
{
// 这里只负责创建实体用户,稍后在用户服务中保存到数据库
EntityUser entity = EntityUser.Create(cookie);
entity.Aid = cookie.GetValueOrDefault(Cookie.STUID);
entity.Mid = cookie.GetValueOrDefault(Cookie.MID);
entity.Mid = isOversea ? entity.Aid : cookie.GetValueOrDefault(Cookie.MID);
entity.IsOversea = isOversea;
if (entity.Aid != null && entity.Mid != null)
{
@@ -143,38 +142,6 @@ internal sealed class User : ObservableObject
}
}
/// <summary>
/// 创建并初始化国际服用户
/// </summary>
/// <param name="cookie">cookie</param>
/// <param name="token">取消令牌</param>
/// <returns>用户</returns>
internal static async Task<User?> CreateOsAsync(Cookie cookie, CancellationToken token = default)
{
// 这里只负责创建实体用户,稍后在用户服务中保存到数据库
EntityUser entity = EntityUser.CreateOs(cookie);
entity.Aid = cookie.GetValueOrDefault(Cookie.STUID);
// Note: Currently we don't know how to get "mid" for hoyolab user,
// mid is set as the same value of ltuid/stuid
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)
@@ -190,22 +157,24 @@ internal sealed class User : ObservableObject
using (IServiceScope scope = Ioc.Default.CreateScope())
{
if (!await TrySetUserInfoAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
bool isOversea = Entity.IsOversea;
if (!await TrySetLTokenAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
if (!await TrySetUserGameRolesAsync(scope.ServiceProvider, token).ConfigureAwait(false))
if (!await TrySetCookieTokenAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
if (!await TrySetCookieTokenAsync(scope.ServiceProvider, token).ConfigureAwait(false))
if (!await TrySetUserInfoAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
if (!await TrySetUserGameRolesAsync(scope.ServiceProvider, token).ConfigureAwait(false))
{
return false;
}
@@ -215,88 +184,6 @@ internal sealed class User : ObservableObject
return isInitialized = true;
}
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();
}
private async Task<bool> TrySetLTokenAsync(IServiceProvider provider, CancellationToken token)
{
if (LToken != null)
@@ -304,14 +191,14 @@ internal sealed class User : ObservableObject
return true;
}
Response<LtokenWrapper> lTokenResponse = await provider
.GetRequiredService<PassportClient2>()
Response<LTokenWrapper> lTokenResponse = await provider
.PickRequiredService<IPassportClient>(Entity.IsOversea)
.GetLTokenBySTokenAsync(Entity, token)
.ConfigureAwait(false);
if (lTokenResponse.IsOk())
{
LToken = Cookie.Parse($"{Cookie.LTUID}={Entity.Aid};{Cookie.LTOKEN}={lTokenResponse.Data.Ltoken}");
LToken = Cookie.Parse($"{Cookie.LTUID}={Entity.Aid};{Cookie.LTOKEN}={lTokenResponse.Data.LToken}");
return true;
}
else
@@ -328,7 +215,7 @@ internal sealed class User : ObservableObject
}
Response<UidCookieToken> cookieTokenResponse = await provider
.GetRequiredService<PassportClient2>()
.PickRequiredService<IPassportClient>(Entity.IsOversea)
.GetCookieAccountInfoBySTokenAsync(Entity, token)
.ConfigureAwait(false);
@@ -346,38 +233,32 @@ internal sealed class User : ObservableObject
private async Task<bool> TrySetUserInfoAsync(IServiceProvider provider, CancellationToken token)
{
Response<UserFullInfoWrapper> response = await provider
.GetRequiredService<UserClient>()
.PickRequiredService<IUserClient>(Entity.IsOversea)
.GetUserFullInfoAsync(Entity, token)
.ConfigureAwait(false);
UserInfo = response.Data?.UserInfo;
return UserInfo != null;
if (response.IsOk())
{
UserInfo = response.Data.UserInfo;
return true;
}
else
{
return false;
}
}
private async Task<bool> TrySetUserGameRolesAsync(IServiceProvider provider, CancellationToken token)
{
Response<ActionTicketWrapper> actionTicketResponse = await provider
.GetRequiredService<AuthClient>()
.GetActionTicketByStokenAsync("game_role", Entity)
.ConfigureAwait(false);
Response<ListWrapper<UserGameRole>> userGameRolesResponse = await provider
.GetRequiredService<BindingClient>()
.GetUserGameRolesOverseaAwareAsync(Entity, token)
.ConfigureAwait(false);
if (actionTicketResponse.IsOk())
if (userGameRolesResponse.IsOk())
{
string actionTicket = actionTicketResponse.Data.Ticket;
Response<ListWrapper<UserGameRole>> userGameRolesResponse = await provider
.GetRequiredService<BindingClient>()
.GetUserGameRolesByActionTicketAsync(actionTicket, Entity, token)
.ConfigureAwait(false);
if (userGameRolesResponse.IsOk())
{
UserGameRoles = userGameRolesResponse.Data.List;
return UserGameRoles.Any();
}
else
{
return false;
}
UserGameRoles = userGameRolesResponse.Data.List;
return UserGameRoles.Any();
}
else
{

View File

@@ -67,8 +67,8 @@ internal sealed class User : ISelectable
/// <returns>新创建的用户</returns>
public static User Create(Cookie cookie)
{
_ = cookie.TryGetAsStoken(out Cookie? stoken);
_ = cookie.TryGetAsLtoken(out Cookie? ltoken);
_ = cookie.TryGetAsSToken(out Cookie? stoken);
_ = cookie.TryGetAsLToken(out Cookie? ltoken);
_ = cookie.TryGetAsCookieToken(out Cookie? cookieToken);
return new() { SToken = stoken, LToken = ltoken, CookieToken = cookieToken };
@@ -81,8 +81,8 @@ internal sealed class User : ISelectable
/// <returns>新创建的用户</returns>
public static User CreateOs(Cookie cookie)
{
_ = cookie.TryGetAsLegacyStoken(out Cookie? stoken);
_ = cookie.TryGetAsLtoken(out Cookie? ltoken);
_ = cookie.TryGetAsLegacySToken(out Cookie? stoken);
_ = cookie.TryGetAsLToken(out Cookie? ltoken);
_ = cookie.TryGetAsCookieToken(out Cookie? cookieToken);
return new() { SToken = stoken, LToken = ltoken, CookieToken = cookieToken };

View File

@@ -1266,6 +1266,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 Hoyolab 账号不支持使用 SToken 刷新祈愿记录 的本地化字符串。
/// </summary>
internal static string ServiceGachaLogUrlProviderStokenUnsupported {
get {
return ResourceManager.GetString("ServiceGachaLogUrlProviderStokenUnsupported", resourceCulture);
}
}
/// <summary>
/// 查找类似 不支持的 Item Id{0} 的本地化字符串。
/// </summary>
@@ -1465,11 +1474,11 @@ namespace Snap.Hutao.Resource.Localization {
}
/// <summary>
/// 查找类似 输入的 Cookie 必须包含 Stoken 的本地化字符串。
/// 查找类似 输入的 Cookie 必须包含 SToken 的本地化字符串。
/// </summary>
internal static string ServiceUserProcessCookieNoStoken {
internal static string ServiceUserProcessCookieNoSToken {
get {
return ResourceManager.GetString("ServiceUserProcessCookieNoStoken", resourceCulture);
return ResourceManager.GetString("ServiceUserProcessCookieNoSToken", resourceCulture);
}
}
@@ -2005,7 +2014,7 @@ namespace Snap.Hutao.Resource.Localization {
}
/// <summary>
/// 查找类似 在此处输入包含 Stoken 的 Cookie 的本地化字符串。
/// 查找类似 在此处输入包含 SToken 的 Cookie 的本地化字符串。
/// </summary>
internal static string ViewDialogUserInputPlaceholder {
get {
@@ -2238,6 +2247,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 Hoyolab 账号不支持验证 的本地化字符串。
/// </summary>
internal static string ViewModelDailyNoteHoyolabVerificationUnsupported {
get {
return ResourceManager.GetString("ViewModelDailyNoteHoyolabVerificationUnsupported", resourceCulture);
}
}
/// <summary>
/// 查找类似 30 分钟 | 3.75 树脂 的本地化字符串。
/// </summary>
@@ -3301,20 +3319,20 @@ namespace Snap.Hutao.Resource.Localization {
}
/// <summary>
/// 查找类似 Stoken 刷新 的本地化字符串。
/// 查找类似 SToken 刷新 的本地化字符串。
/// </summary>
internal static string ViewPageGachaLogRefreshByStoken {
internal static string ViewPageGachaLogRefreshBySToken {
get {
return ResourceManager.GetString("ViewPageGachaLogRefreshByStoken", resourceCulture);
return ResourceManager.GetString("ViewPageGachaLogRefreshBySToken", resourceCulture);
}
}
/// <summary>
/// 查找类似 使用当前用户的 Cookie 信息刷新祈愿记录 的本地化字符串。
/// </summary>
internal static string ViewPageGachaLogRefreshByStokenDescription {
internal static string ViewPageGachaLogRefreshBySTokenDescription {
get {
return ResourceManager.GetString("ViewPageGachaLogRefreshByStokenDescription", resourceCulture);
return ResourceManager.GetString("ViewPageGachaLogRefreshBySTokenDescription", resourceCulture);
}
}
@@ -3975,6 +3993,15 @@ namespace Snap.Hutao.Resource.Localization {
}
}
/// <summary>
/// 查找类似 请输入你的 Hoyolab Uid 的本地化字符串。
/// </summary>
internal static string ViewPageLoginHoyoverseUserHint {
get {
return ResourceManager.GetString("ViewPageLoginHoyoverseUserHint", resourceCulture);
}
}
/// <summary>
/// 查找类似 我已登录 的本地化字符串。
/// </summary>

View File

@@ -582,8 +582,8 @@
<data name="ServiceUserProcessCookieNoMid" xml:space="preserve">
<value>输入的 Cookie 必须包含 Mid</value>
</data>
<data name="ServiceUserProcessCookieNoStoken" xml:space="preserve">
<value>输入的 Cookie 必须包含 Stoken</value>
<data name="ServiceUserProcessCookieNoSToken" xml:space="preserve">
<value>输入的 Cookie 必须包含 SToken</value>
</data>
<data name="ServiceUserProcessCookieRequestUserInfoFailed" xml:space="preserve">
<value>输入的 Cookie 无法获取用户信息</value>
@@ -757,7 +757,7 @@
<value>操作文档</value>
</data>
<data name="ViewDialogUserInputPlaceholder" xml:space="preserve">
<value>在此处输入包含 Stoken 的 Cookie</value>
<value>在此处输入包含 SToken 的 Cookie</value>
</data>
<data name="ViewGachaLogHeader" xml:space="preserve">
<value>祈愿记录</value>
@@ -1185,10 +1185,10 @@
<data name="ViewPageGachaLogRefreshBymanualInputDescription" xml:space="preserve">
<value>使用由你提供的 Url 刷新祈愿记录</value>
</data>
<data name="ViewPageGachaLogRefreshByStoken" xml:space="preserve">
<value>Stoken 刷新</value>
<data name="ViewPageGachaLogRefreshBySToken" xml:space="preserve">
<value>SToken 刷新</value>
</data>
<data name="ViewPageGachaLogRefreshByStokenDescription" xml:space="preserve">
<data name="ViewPageGachaLogRefreshBySTokenDescription" xml:space="preserve">
<value>使用当前用户的 Cookie 信息刷新祈愿记录</value>
</data>
<data name="ViewPageGachaLogRefreshByWebCache" xml:space="preserve">
@@ -1809,4 +1809,13 @@
<data name="ModelBindingUserInitializationFailed" xml:space="preserve">
<value>网络异常</value>
</data>
<data name="ServiceGachaLogUrlProviderStokenUnsupported" xml:space="preserve">
<value>Hoyolab 账号不支持使用 SToken 刷新祈愿记录</value>
</data>
<data name="ViewModelDailyNoteHoyolabVerificationUnsupported" xml:space="preserve">
<value>Hoyolab 账号不支持验证</value>
</data>
<data name="ViewPageLoginHoyoverseUserHint" xml:space="preserve">
<value>请输入你的 Hoyolab Uid</value>
</data>
</root>

View File

@@ -24,14 +24,17 @@ namespace Snap.Hutao.Service.AvatarInfo;
internal sealed class AvatarInfoDbOperation
{
private readonly AppDbContext appDbContext;
private readonly IServiceProvider serviceProvider;
/// <summary>
/// 构造一个新的角色信息数据库操作
/// </summary>
/// <param name="appDbContext">数据库上下文</param>
public AvatarInfoDbOperation(AppDbContext appDbContext)
/// <param name="serviceProvider">服务提供器</param>
public AvatarInfoDbOperation(AppDbContext appDbContext, IServiceProvider serviceProvider)
{
this.appDbContext = appDbContext;
this.serviceProvider = serviceProvider;
}
/// <summary>
@@ -90,72 +93,48 @@ internal sealed class AvatarInfoDbOperation
.ToList();
EnsureItemsAvatarIdDistinct(ref dbInfos, uid);
Response<RecordPlayerInfo> playerInfoResponse;
Response<Web.Hoyolab.Takumi.GameRecord.Avatar.CharacterWrapper> charactersResponse;
IGameRecordClient gameRecordClient = serviceProvider.PickRequiredService<IGameRecordClient>(userAndUid.User.IsOversea);
Response<RecordPlayerInfo> playerInfoResponse = await gameRecordClient
.GetPlayerInfoAsync(userAndUid, token)
.ConfigureAwait(false);
if (!userAndUid.User.IsOversea)
if (playerInfoResponse.IsOk())
{
GameRecordClient gameRecordClient = Ioc.Default.GetRequiredService<GameRecordClient>();
playerInfoResponse = await gameRecordClient
.GetPlayerInfoAsync(userAndUid, token)
.ConfigureAwait(false);
Response<Web.Hoyolab.Takumi.GameRecord.Avatar.CharacterWrapper> charactersResponse = await gameRecordClient
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
.ConfigureAwait(false);
if (!playerInfoResponse.IsOk())
token.ThrowIfCancellationRequested();
if (charactersResponse.IsOk())
{
return GetDbAvatarInfos(uid);
}
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
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);
GameRecordCharacterAvatarInfoComposer composer = serviceProvider.GetRequiredService<GameRecordCharacterAvatarInfoComposer>();
if (!playerInfoResponse.IsOk())
{
return GetDbAvatarInfos(uid);
}
charactersResponse = await gameRecordClientOs
.GetCharactersAsync(userAndUid, playerInfoResponse.Data, token)
.ConfigureAwait(false);
}
token.ThrowIfCancellationRequested();
if (charactersResponse.IsOk())
{
List<RecordCharacter> characters = charactersResponse.Data.Avatars;
GameRecordCharacterAvatarInfoComposer composer = Ioc.Default.GetRequiredService<GameRecordCharacterAvatarInfoComposer>();
foreach (RecordCharacter character in characters)
{
if (AvatarIds.IsPlayer(character.Id))
foreach (RecordCharacter character in characters)
{
continue;
}
if (AvatarIds.IsPlayer(character.Id))
{
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);
}
}
}
}
@@ -239,6 +218,7 @@ internal sealed class AvatarInfoDbOperation
int distinctCount = dbInfos.Select(info => info.Info.AvatarId).ToHashSet().Count;
// Avatars are actually less than the list told us.
// This means that there are duplicate items.
if (distinctCount < dbInfos.Count)
{
appDbContext.AvatarInfos.ExecuteDeleteWhere(i => i.Uid == uid);

View File

@@ -62,39 +62,15 @@ internal sealed class DailyNoteNotifier
string? attribution = SH.ServiceDailyNoteNotifierAttribution;
if (entry.User.IsOversea)
Response<ListWrapper<UserGameRole>> rolesResponse = await scope.ServiceProvider
.GetRequiredService<BindingClient>()
.GetUserGameRolesOverseaAwareAsync(entry.User)
.ConfigureAwait(false);
if (rolesResponse.IsOk())
{
Response<ListWrapper<UserGameRole>> rolesResponse = await scope.ServiceProvider
.GetRequiredService<BindingClient>()
.GetOsUserGameRolesByCookieAsync(entry.User)
.ConfigureAwait(false);
if (rolesResponse.IsOk())
{
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";
}
}
List<UserGameRole> roles = rolesResponse.Data.List;
attribution = roles.SingleOrDefault(r => r.GameUid == entry.Uid)?.ToString() ?? "Unknown";
}
ToastContentBuilder builder = new ToastContentBuilder()

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Message;
using Snap.Hutao.Model.Binding.User;
@@ -61,28 +60,15 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient<UserRemov
using (IServiceScope scope = scopeFactory.CreateScope())
{
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);
// 根据 Uid 的地区选择不同的 API
Web.Response.Response<WebDailyNote> dailyNoteResponse;
PlayerUid playerUid = new(roleUid);
if (playerUid.Region == "cn_gf01" || playerUid.Region == "cn_qd01")
{
dailyNoteResponse = await gameRecordClient
Web.Response.Response<WebDailyNote> dailyNoteResponse = await scope.ServiceProvider
.PickRequiredService<IGameRecordClient>(PlayerUid.IsOversea(roleUid))
.GetDailyNoteAsync(role)
.ConfigureAwait(false);
}
else
{
dailyNoteResponse = await gameRecordClientOs
.GetDailyNoteAsync(role)
.ConfigureAwait(false);
}
if (dailyNoteResponse.IsOk())
{
@@ -124,7 +110,7 @@ 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>();
GameRecordClientOversea gameRecordClientOs = scope.ServiceProvider.GetRequiredService<GameRecordClientOversea>();
bool isSilentMode = appDbContext.Settings
.SingleOrAdd(SettingEntry.DailyNoteSilentWhenPlayingGame, Core.StringLiterals.False)
@@ -139,20 +125,10 @@ internal sealed class DailyNoteService : IDailyNoteService, IRecipient<UserRemov
foreach (DailyNoteEntry entry in appDbContext.DailyNotes.Include(n => n.User))
{
Web.Response.Response<WebDailyNote> dailyNoteResponse;
PlayerUid playerUid = new(entry.Uid);
if (playerUid.Region == "cn_gf01" || playerUid.Region == "cn_qd01")
{
dailyNoteResponse = await gameRecordClient
Web.Response.Response<WebDailyNote> dailyNoteResponse = await scope.ServiceProvider
.PickRequiredService<IGameRecordClient>(PlayerUid.IsOversea(entry.Uid))
.GetDailyNoteAsync(new(entry.User, entry.Uid))
.ConfigureAwait(false);
}
else
{
dailyNoteResponse = await gameRecordClientOs
.GetDailyNoteAsync(new(entry.User, entry.Uid))
.ConfigureAwait(false);
}
if (dailyNoteResponse.ReturnCode == 0)
{

View File

@@ -178,7 +178,7 @@ internal sealed class GachaLogService : IGachaLogService
return option switch
{
RefreshOption.WebCache => urlProviders.Single(p => p.Name == nameof(GachaLogQueryWebCacheProvider)),
RefreshOption.Stoken => urlProviders.Single(p => p.Name == nameof(GachaLogQueryStokenProvider)),
RefreshOption.SToken => urlProviders.Single(p => p.Name == nameof(GachaLogQuerySTokenProvider)),
RefreshOption.ManualInput => urlProviders.Single(p => p.Name == nameof(GachaLogQueryManualInputProvider)),
_ => null,
};

View File

@@ -10,11 +10,11 @@ using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Service.GachaLog.QueryProvider;
/// <summary>
/// 使用Stokn提供祈愿Url
/// 使用 SToken 提供祈愿 Url
/// </summary>
[HighQuality]
[Injection(InjectAs.Transient, typeof(IGachaLogQueryProvider))]
internal sealed class GachaLogQueryStokenProvider : IGachaLogQueryProvider
internal sealed class GachaLogQuerySTokenProvider : IGachaLogQueryProvider
{
private readonly IUserService userService;
private readonly BindingClient2 bindingClient2;
@@ -24,14 +24,14 @@ internal sealed class GachaLogQueryStokenProvider : IGachaLogQueryProvider
/// </summary>
/// <param name="userService">用户服务</param>
/// <param name="bindingClient2">绑定客户端</param>
public GachaLogQueryStokenProvider(IUserService userService, BindingClient2 bindingClient2)
public GachaLogQuerySTokenProvider(IUserService userService, BindingClient2 bindingClient2)
{
this.userService = userService;
this.bindingClient2 = bindingClient2;
}
/// <inheritdoc/>
public string Name { get => nameof(GachaLogQueryStokenProvider); }
public string Name { get => nameof(GachaLogQuerySTokenProvider); }
/// <inheritdoc/>
public async Task<ValueResult<bool, GachaLogQuery>> GetQueryAsync()
@@ -40,7 +40,7 @@ internal sealed class GachaLogQueryStokenProvider : IGachaLogQueryProvider
{
if (userAndUid.User.IsOversea)
{
return new(false, "Unsupported for hoyoverse account");
return new(false, SH.ServiceGachaLogUrlProviderStokenUnsupported);
}
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(userAndUid.Uid);

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Service.GachaLog.QueryProvider;
/// <summary>

View File

@@ -24,7 +24,7 @@ internal enum RefreshOption
/// <summary>
/// 通过Stoken刷新
/// </summary>
Stoken,
SToken,
/// <summary>
/// 手动输入Url刷新

View File

@@ -1,8 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
namespace Snap.Hutao.Service.Game.Locator;
/// <summary>

View File

@@ -19,9 +19,8 @@ namespace Snap.Hutao.Service.SpiralAbyss;
[Injection(InjectAs.Scoped, typeof(ISpiralAbyssRecordService))]
internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
{
private readonly IServiceProvider serviceProvider;
private readonly AppDbContext appDbContext;
private readonly GameRecordClient gameRecordClient;
private readonly GameRecordClientOs gameRecordClientOs;
private string? uid;
private ObservableCollection<SpiralAbyssEntry>? spiralAbysses;
@@ -29,13 +28,11 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
/// <summary>
/// 构造一个新的深渊记录服务
/// </summary>
/// <param name="appDbContext">数据库上下文</param>
/// <param name="gameRecordClient">游戏记录客户端</param>
public SpiralAbyssRecordService(AppDbContext appDbContext, GameRecordClient gameRecordClient, GameRecordClientOs gameRecordClientOs)
/// <param name="serviceProvider">服务提供器</param>
public SpiralAbyssRecordService(IServiceProvider serviceProvider)
{
this.appDbContext = appDbContext;
this.gameRecordClient = gameRecordClient;
this.gameRecordClientOs = gameRecordClientOs;
appDbContext = serviceProvider.GetRequiredService<AppDbContext>();
this.serviceProvider = serviceProvider;
}
/// <inheritdoc/>
@@ -72,21 +69,10 @@ internal class SpiralAbyssRecordService : ISpiralAbyssRecordService
private async Task RefreshSpiralAbyssCoreAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule)
{
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> response;
// server determination
if (userAndUid.User.IsOversea)
{
response = await gameRecordClientOs
Response<Web.Hoyolab.Takumi.GameRecord.SpiralAbyss.SpiralAbyss> response = await serviceProvider
.PickRequiredService<IGameRecordClient>(userAndUid.User.IsOversea)
.GetSpiralAbyssAsync(userAndUid, schedule)
.ConfigureAwait(false);
}
else
{
response = await gameRecordClient
.GetSpiralAbyssAsync(userAndUid, schedule)
.ConfigureAwait(false);
}
if (response.IsOk())
{

View File

@@ -45,15 +45,9 @@ internal interface IUserService
/// 尝试异步处理输入的Cookie
/// </summary>
/// <param name="cookie">Cookie</param>
/// <param name="isOversea">是否为国际服</param>
/// <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);
Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie, bool isOversea);
/// <summary>
/// 异步刷新 Cookie 的 CookieToken

View File

@@ -191,7 +191,7 @@ internal class UserService : IUserService
}
/// <inheritdoc/>
public async Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie)
public async Task<ValueResult<UserOptionResult, string>> ProcessInputCookieAsync(Cookie cookie, bool isOversea)
{
await ThreadHelper.SwitchToBackgroundAsync();
string? mid = cookie.GetValueOrDefault(Cookie.MID);
@@ -208,10 +208,10 @@ internal class UserService : IUserService
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
if (cookie.TryGetAsStoken(out Cookie? stoken))
if (cookie.TryGetAsSToken(out Cookie? stoken))
{
user.SToken = stoken;
user.LToken = cookie.TryGetAsLtoken(out Cookie? ltoken) ? ltoken : user.LToken;
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);
@@ -219,55 +219,13 @@ internal class UserService : IUserService
}
else
{
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoStoken);
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoSToken);
}
}
}
else
{
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);
return await TryCreateUserAndAddAsync(cookie, isOversea).ConfigureAwait(false);
}
}
@@ -276,21 +234,10 @@ internal class UserService : IUserService
{
using (IServiceScope scope = scopeFactory.CreateScope())
{
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);
}
Response<UidCookieToken> cookieTokenResponse = await scope.ServiceProvider
.PickRequiredService<IPassportClient>(user.Entity.IsOversea)
.GetCookieAccountInfoBySTokenAsync(user.Entity)
.ConfigureAwait(false);
if (cookieTokenResponse.IsOk())
{
@@ -324,17 +271,7 @@ internal class UserService : IUserService
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
BindingUser? newUser;
// 判断是否为国际服
if (isOversea)
{
newUser = await BindingUser.CreateOsAsync(cookie).ConfigureAwait(false);
}
else
{
newUser = await BindingUser.CreateAsync(cookie).ConfigureAwait(false);
}
BindingUser? newUser = await BindingUser.CreateAsync(cookie, isOversea).ConfigureAwait(false);
if (newUser != null)
{

View File

@@ -49,9 +49,9 @@ internal sealed partial class SignInWebViewDialog : ContentDialog
if (user.Entity.IsOversea)
{
coreWebView2.SetCookie(user.CookieToken, user.LToken, null).SetMobileOsUserAgent();
coreWebView2.SetCookie(user.CookieToken, user.LToken, null).SetMobileOverseaUserAgent();
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");
coreWebView2.Navigate("https://act.hoyolab.com/ys/event/signin-sea-v3/index.html?act_id=e202102251931481");
}
else
{

View File

@@ -48,9 +48,9 @@
<AppBarButton.Flyout>
<MenuFlyout Placement="Bottom">
<MenuFlyoutItem
Command="{Binding RefreshByStokenCommand}"
Command="{Binding RefreshBySTokenCommand}"
Icon="{shcm:FontIcon Glyph=&#xE192;}"
Text="{shcm:ResourceString Name=ViewPageGachaLogRefreshByStoken}"/>
Text="{shcm:ResourceString Name=ViewPageGachaLogRefreshBySToken}"/>
<MenuFlyoutItem
Command="{Binding RefreshByWebCacheCommand}"
Icon="{shcm:FontIcon Glyph=&#xE81E;}"
@@ -487,9 +487,9 @@
Spacing="{StaticResource SettingsCardSpacing}">
<clw:SettingsCard
ActionIconToolTip="{shcm:ResourceString Name=ViewPageGachaLogRefreshAction}"
Command="{Binding RefreshByStokenCommand}"
Description="{shcm:ResourceString Name=ViewPageGachaLogRefreshByStokenDescription}"
Header="{shcm:ResourceString Name=ViewPageGachaLogRefreshByStoken}"
Command="{Binding RefreshBySTokenCommand}"
Description="{shcm:ResourceString Name=ViewPageGachaLogRefreshBySTokenDescription}"
Header="{shcm:ResourceString Name=ViewPageGachaLogRefreshBySToken}"
HeaderIcon="{shcm:FontIcon Glyph=&#xE192;}"
IsClickEnabled="True"/>
<clw:SettingsCard

View File

@@ -28,22 +28,20 @@
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBox
TextChanging="TextBoxOnTextChanging"
InputScope="Number"
MaxLength="9"
x:Name="UidInputText"
Grid.Column="0"
Width="240"
x:Name="UidInput"
Margin="0,0,16,0"
PlaceholderText="Please fill in your User ID here"
HorizontalAlignment="Right"/>
HorizontalAlignment="Right"
InputScope="Number"
MaxLength="9"
PlaceholderText="Please fill in your User ID here"/>
<Button
Grid.Column="1"
RelativePanel.RightOf="UidInput"
Click="CookieButtonClick"
Content="{shcm:ResourceString Name=ViewPageLoginMihoyoUserLoggedInAction}"/>
Click="CookieButtonClick"
Content="{shcm:ResourceString Name=ViewPageLoginMihoyoUserLoggedInAction}"/>
</Grid>
<WebView2
x:Name="WebView"
Grid.Row="2"

View File

@@ -16,7 +16,6 @@ namespace Snap.Hutao.View.Page;
/// <summary>
/// 登录米哈游通行证页面
/// </summary>
[HighQuality]
internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Controls.Page
{
/// <summary>
@@ -34,7 +33,6 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
{
await WebView.EnsureCoreWebView2Async();
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://account.hoyolab.com");
foreach (CoreWebView2Cookie item in cookies)
@@ -50,7 +48,7 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
}
}
private async Task HandleCurrentCookieAsync(CancellationToken token)
private async Task HandleCurrentCookieAsync(CancellationToken token = default)
{
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://account.hoyolab.com");
@@ -58,7 +56,7 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
// Get user id from text input, login_uid is missed in cookie
string uid = UidInput.Text;
string uid = UidInputText.Text;
if (uid.Length != 9)
{
@@ -72,8 +70,8 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
// 使用 loginTicket 获取 stoken
Response<ListWrapper<NameToken>> multiTokenResponse = await Ioc.Default
.GetRequiredService<AuthClientOs>()
.GetMultiTokenByLoginTicketAsync(loginTicketCookie, token)
.GetRequiredService<AuthClient>()
.GetMultiTokenByLoginTicketAsync(loginTicketCookie, true, token)
.ConfigureAwait(false);
if (!multiTokenResponse.IsOk())
@@ -87,7 +85,7 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
// 处理 cookie 并添加用户
(UserOptionResult result, string nickname) = await Ioc.Default
.GetRequiredService<IUserService>()
.ProcessInputOsCookieAsync(hoyoLabCookie)
.ProcessInputCookieAsync(hoyoLabCookie, true)
.ConfigureAwait(false);
Ioc.Default.GetRequiredService<INavigationService>().GoBack();
@@ -120,11 +118,6 @@ internal sealed partial class LoginHoyoverseUserPage : Microsoft.UI.Xaml.Control
private void CookieButtonClick(object sender, RoutedEventArgs e)
{
HandleCurrentCookieAsync(CancellationToken.None).SafeForget();
}
private void TextBoxOnTextChanging(TextBox sender, TextBoxTextChangingEventArgs args)
{
sender.Text = new(sender.Text.Where(char.IsDigit).ToArray());
HandleCurrentCookieAsync().SafeForget();
}
}

View File

@@ -49,7 +49,7 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
}
}
private async Task HandleCurrentCookieAsync(CancellationToken token)
private async Task HandleCurrentCookieAsync(CancellationToken token = default)
{
CoreWebView2CookieManager manager = WebView.CoreWebView2.CookieManager;
IReadOnlyList<CoreWebView2Cookie> cookies = await manager.GetCookiesAsync("https://user.mihoyo.com");
@@ -57,7 +57,7 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
Cookie loginTicketCookie = Cookie.FromCoreWebView2Cookies(cookies);
Response<ListWrapper<NameToken>> multiTokenResponse = await Ioc.Default
.GetRequiredService<AuthClient>()
.GetMultiTokenByLoginTicketAsync(loginTicketCookie, token)
.GetMultiTokenByLoginTicketAsync(loginTicketCookie, false, token)
.ConfigureAwait(false);
if (!multiTokenResponse.IsOk())
@@ -70,7 +70,7 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
Cookie stokenV1 = Cookie.Parse($"stuid={loginTicketCookie["login_uid"]};stoken={multiTokenMap["stoken"]}");
Response<LoginResult> loginResultResponse = await Ioc.Default
.GetRequiredService<PassportClient2>()
.LoginByStokenAsync(stokenV1, token)
.LoginBySTokenAsync(stokenV1, token)
.ConfigureAwait(false);
if (!loginResultResponse.IsOk())
@@ -81,7 +81,7 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
Cookie stokenV2 = Cookie.FromLoginResult(loginResultResponse.Data);
(UserOptionResult result, string nickname) = await Ioc.Default
.GetRequiredService<IUserService>()
.ProcessInputCookieAsync(stokenV2)
.ProcessInputCookieAsync(stokenV2, false)
.ConfigureAwait(false);
Ioc.Default.GetRequiredService<INavigationService>().GoBack();
@@ -115,6 +115,6 @@ internal sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.P
private void CookieButtonClick(object sender, RoutedEventArgs e)
{
HandleCurrentCookieAsync(CancellationToken.None).SafeForget();
HandleCurrentCookieAsync().SafeForget();
}
}

View File

@@ -250,7 +250,7 @@ internal sealed class DailyNoteViewModel : Abstraction.ViewModel
// TODO: Add verify support for oversea user
if (userAndUid.User.IsOversea)
{
serviceProvider.GetRequiredService<IInfoBarService>().Warning("Unsupported for hoyoverse account");
serviceProvider.GetRequiredService<IInfoBarService>().Warning(SH.ViewModelDailyNoteHoyolabVerificationUnsupported);
}
else
{

View File

@@ -59,7 +59,7 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel
this.options = options;
RefreshByWebCacheCommand = new AsyncRelayCommand(RefreshByWebCacheAsync);
RefreshByStokenCommand = new AsyncRelayCommand(RefreshByStokenAsync);
RefreshBySTokenCommand = new AsyncRelayCommand(RefreshBySTokenAsync);
RefreshByManualInputCommand = new AsyncRelayCommand(RefreshByManualInputAsync);
ImportFromUIGFJsonCommand = new AsyncRelayCommand(ImportFromUIGFJsonAsync);
ExportToUIGFJsonCommand = new AsyncRelayCommand(ExportToUIGFJsonAsync);
@@ -112,9 +112,9 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel
public ICommand RefreshByWebCacheCommand { get; }
/// <summary>
/// Stoken 刷新命令
/// SToken 刷新命令
/// </summary>
public ICommand RefreshByStokenCommand { get; }
public ICommand RefreshBySTokenCommand { get; }
/// <summary>
/// 手动输入Url刷新命令
@@ -164,9 +164,9 @@ internal sealed class GachaLogViewModel : Abstraction.ViewModel
return RefreshInternalAsync(RefreshOption.WebCache);
}
private Task RefreshByStokenAsync()
private Task RefreshBySTokenAsync()
{
return RefreshInternalAsync(RefreshOption.Stoken);
return RefreshInternalAsync(RefreshOption.SToken);
}
private Task RefreshByManualInputAsync()

View File

@@ -46,7 +46,7 @@ internal static class ApiEndpoints
/// 获取 stoken 与 ltoken
/// </summary>
/// <param name="actionType">操作类型 game_role</param>
/// <param name="stoken">Stoken</param>
/// <param name="stoken">SToken</param>
/// <param name="uid">uid</param>
/// <returns>Url</returns>
public static string AuthActionTicket(string actionType, string stoken, string uid)
@@ -86,7 +86,7 @@ internal static class ApiEndpoints
/// <summary>
/// 用户游戏角色
/// </summary>
public const string UserGameRolesByStoken = $"{ApiTaKumiBindingApi}/getUserGameRolesByStoken";
public const string UserGameRolesBySToken = $"{ApiTaKumiBindingApi}/getUserGameRolesByStoken";
/// <summary>
/// AuthKey
@@ -289,12 +289,12 @@ internal static class ApiEndpoints
public const string AccountGetCookieTokenBySToken = $"{PassportApiAuthApi}/getCookieAccountInfoBySToken";
/// <summary>
/// 获取Ltoken
/// 获取LToken
/// </summary>
public const string AccountGetLtokenByStoken = $"{PassportApiAuthApi}/getLTokenBySToken";
public const string AccountGetLTokenBySToken = $"{PassportApiAuthApi}/getLTokenBySToken";
/// <summary>
/// 获取V2Stoken
/// 获取V2SToken
/// </summary>
public const string AccountGetSTokenByOldToken = $"{PassportApi}/account/ma-cn-session/app/getTokenBySToken";

View File

@@ -1,8 +1,6 @@
// 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;
@@ -14,9 +12,29 @@ namespace Snap.Hutao.Web;
/// </summary>
[HighQuality]
[SuppressMessage("", "SA1201")]
[SuppressMessage("", "SA1202")]
[SuppressMessage("", "SA1124")]
internal static class ApiOsEndpoints
{
#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 ApiGeetest
/// <summary>
@@ -59,7 +77,7 @@ internal static class ApiOsEndpoints
/// 获取 stoken 与 ltoken
/// </summary>
/// <param name="actionType">操作类型 game_role</param>
/// <param name="stoken">Stoken</param>
/// <param name="stoken">SToken</param>
/// <param name="uid">uid</param>
/// <returns>Url</returns>
public static string AuthActionTicket(string actionType, string stoken, string uid)
@@ -75,7 +93,7 @@ internal static class ApiOsEndpoints
/// 用户游戏角色
/// </summary>
/// <returns>用户游戏角色字符串</returns>
public const string UserGameRolesByCookie = $"{ApiOsTaKumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_global";
public const string UserGameRolesByCookie = $"{ApiOsTakumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_global";
/// <summary>
/// 用户游戏角色
@@ -89,46 +107,6 @@ internal static class ApiOsEndpoints
#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}&region={uid.Region}";
}
/// <summary>
/// 计算器同步角色列表 size 20
/// </summary>
public const string CalculateOsSyncAvatarList = $"{SgPublicApi}/event/calculateos/sync/avatar/list";
#endregion
#region BbsApiOsApi
/// <summary>
@@ -202,23 +180,44 @@ internal static class ApiOsEndpoints
}
#endregion
#region ApiAccountOsApi
#region SgPublicApi
/// <summary>
/// Hoyolab App Login api
/// Can fetch stoken
/// 计算器家具计算
/// </summary>
public const string WebLoginByPassword = $"{ApiAccountOsAuthApi}/webLoginByPassword";
public const string CalculateOsFurnitureCompute = $"{SgPublicApi}/event/calculateos/furniture/list";
/// <summary>
/// 获取 Ltoken
/// 计算器角色列表 size 20
/// </summary>
public const string AccountGetLtokenByStoken = $"{ApiAccountOsAuthApi}/getLTokenBySToken";
public const string CalculateOsAvatarList = $"{SgPublicApi}/event/calculateos/avatar/list";
/// <summary>
/// fetch CookieToken
/// 计算器武器列表 size 20
/// </summary>
public const string AccountGetCookieTokenBySToken = $"{ApiAccountOsAuthApi}/getCookieAccountInfoBySToken";
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}&region={uid.Region}";
}
/// <summary>
/// 计算器同步角色列表 size 20
/// </summary>
public const string CalculateOsSyncAvatarList = $"{SgPublicApi}/event/calculateos/sync/avatar/list";
#endregion
#region SdkStaticLauncherApi
@@ -237,8 +236,8 @@ internal static class ApiOsEndpoints
#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 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";
@@ -259,6 +258,10 @@ internal static class ApiOsEndpoints
/// Web static referer
/// </summary>
public const string WebStaticSeaMihoyoReferer = "https://webstatic-sea.mihoyo.com";
/// <summary>
/// Act hoyolab referer
/// </summary>
public const string ActHoyolabReferer = "https://act.hoyolab.com/";
#endregion

View File

@@ -28,7 +28,7 @@ internal static class CoreWebView2Extension
/// </summary>
/// <param name="webView">webview2</param>
/// <returns>链式调用的WebView2</returns>
public static CoreWebView2 SetMobileOsUserAgent(this CoreWebView2 webView)
public static CoreWebView2 SetMobileOverseaUserAgent(this CoreWebView2 webView)
{
webView.Settings.UserAgent = Core.CoreEnvironment.HoyolabOsMobileUA;
return webView;
@@ -39,10 +39,10 @@ internal static class CoreWebView2Extension
/// </summary>
/// <param name="webView">webview2</param>
/// <param name="cookieToken">CookieToken</param>
/// <param name="ltoken">Ltoken</param>
/// <param name="stoken">Stoken</param>
/// <param name="lToken">LToken</param>
/// <param name="sToken">SToken</param>
/// <returns>链式调用的WebView2</returns>
public static CoreWebView2 SetCookie(this CoreWebView2 webView, Cookie? cookieToken = null, Cookie? ltoken = null, Cookie? stoken = null)
public static CoreWebView2 SetCookie(this CoreWebView2 webView, Cookie? cookieToken = null, Cookie? lToken = null, Cookie? sToken = null)
{
CoreWebView2CookieManager cookieManager = webView.CookieManager;
@@ -51,14 +51,14 @@ internal static class CoreWebView2Extension
cookieManager.AddMihoyoCookie("account_id", cookieToken).AddMihoyoCookie("cookie_token", cookieToken);
}
if (ltoken != null)
if (lToken != null)
{
cookieManager.AddMihoyoCookie("ltuid", ltoken).AddMihoyoCookie("ltoken", ltoken);
cookieManager.AddMihoyoCookie("ltuid", lToken).AddMihoyoCookie("ltoken", lToken);
}
if (stoken != null)
if (sToken != null)
{
cookieManager.AddMihoyoCookie("stuid", stoken).AddMihoyoCookie("stoken", stoken);
cookieManager.AddMihoyoCookie("stuid", sToken).AddMihoyoCookie("stoken", sToken);
}
return webView;

View File

@@ -70,7 +70,7 @@ internal class MiHoYoJSInterface
User user = serviceProvider.GetRequiredService<IUserService>().Current!;
return await serviceProvider
.GetRequiredService<AuthClient>()
.GetActionTicketByStokenAsync(jsParam.Payload!.ActionType, user.Entity)
.GetActionTicketBySTokenAsync(jsParam.Payload!.ActionType, user.Entity)
.ConfigureAwait(false);
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
/// <summary>
/// 用户信息客户端
/// </summary>
internal interface IUserClient : IOverseaSupport
{
/// <summary>
/// 获取当前用户详细信息
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
Task<Response<UserFullInfoWrapper>> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default);
}

View File

@@ -14,8 +14,8 @@ namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
/// </summary>
[HighQuality]
[UseDynamicSecret]
[HttpClient(HttpClientConfiguration.XRpc)]
internal sealed class UserClient
[HttpClient(HttpClientConfiguration.XRpc, typeof(IUserClient))]
internal sealed class UserClient : IUserClient
{
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
@@ -34,6 +34,9 @@ internal sealed class UserClient
this.logger = logger;
}
/// <inheritdoc/>
public bool IsOversea => false;
/// <summary>
/// 获取当前用户详细信息
/// </summary>
@@ -54,17 +57,17 @@ internal sealed class UserClient
}
/// <summary>
/// 获取当前用户详细信息,使用 Ltoken
/// 获取当前用户详细信息,使用 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OS)]
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSK2)]
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)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
.TryCatchGetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiOsEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token)
.ConfigureAwait(false);

View File

@@ -0,0 +1,56 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Bbs.User;
/// <summary>
/// 用户信息客户端 Hoyolab版
/// </summary>
[UseDynamicSecret]
[HttpClient(HttpClientConfiguration.XRpc, typeof(IUserClient))]
internal sealed class UserClientOversea : IUserClient
{
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
private readonly ILogger<UserClientOversea> logger;
/// <summary>
/// 构造一个新的用户信息客户端
/// </summary>
/// <param name="httpClient">http客户端</param>
/// <param name="options">Json序列化选项</param>
/// <param name="logger">日志器</param>
public UserClientOversea(HttpClient httpClient, JsonSerializerOptions options, ILogger<UserClientOversea> logger)
{
this.httpClient = httpClient;
this.options = options;
this.logger = logger;
}
/// <inheritdoc/>
public bool IsOversea => true;
/// <summary>
/// 获取当前用户详细信息,使用 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>详细信息</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSK2)]
public async Task<Response<UserFullInfoWrapper>> GetUserFullInfoAsync(Model.Entity.User user, CancellationToken token = default)
{
Response<UserFullInfoWrapper>? resp = await httpClient
.SetUser(user, CookieType.LToken)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
.TryCatchGetFromJsonAsync<Response<UserFullInfoWrapper>>(ApiOsEndpoints.UserFullInfoQuery(user.Aid!), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}

View File

@@ -94,13 +94,13 @@ internal sealed partial class Cookie
return new(cookieMap);
}
public bool TryGetAsStoken([NotNullWhen(true)] out Cookie? cookie)
public bool TryGetAsSToken([NotNullWhen(true)] out Cookie? cookie)
{
bool hasMid = TryGetValue(MID, out string? mid);
bool hasStoken = TryGetValue(STOKEN, out string? stoken);
bool hasStuid = TryGetValue(STUID, out string? stuid);
bool hasSToken = TryGetValue(STOKEN, out string? stoken);
bool hasSTuid = TryGetValue(STUID, out string? stuid);
if (hasMid && hasStoken && hasStuid)
if (hasMid && hasSToken && hasSTuid)
{
cookie = new Cookie(new()
{
@@ -122,12 +122,12 @@ internal sealed partial class Cookie
/// </summary>
/// <param name="cookie">A cookie contains stoken and stuid, without mid.</param>
/// <returns>是否获取成功</returns>
public bool TryGetAsLegacyStoken([NotNullWhen(true)] out Cookie? cookie)
public bool TryGetAsLegacySToken([NotNullWhen(true)] out Cookie? cookie)
{
bool hasStoken = TryGetValue(STOKEN, out string? stoken);
bool hasStuid = TryGetValue(STUID, out string? stuid);
bool hasSToken = TryGetValue(STOKEN, out string? stoken);
bool hasSTuid = TryGetValue(STUID, out string? stuid);
if (hasStoken && hasStuid)
if (hasSToken && hasSTuid)
{
cookie = new Cookie(new()
{
@@ -142,7 +142,7 @@ internal sealed partial class Cookie
return false;
}
public bool TryGetAsLtoken([NotNullWhen(true)] out Cookie? cookie)
public bool TryGetAsLToken([NotNullWhen(true)] out Cookie? cookie)
{
bool hasLtoken = TryGetValue(LTOKEN, out string? ltoken);
bool hasStuid = TryGetValue(LTUID, out string? ltuid);

View File

@@ -40,7 +40,7 @@ internal enum SaltType
LK2,
/// <summary>
/// 国际服
/// Hoyolab K2
/// </summary>
OS,
OSK2,
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// 通行证客户端
/// </summary>
internal interface IPassportClient : IOverseaSupport
{
/// <summary>
/// 异步获取 CookieToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>cookie token</returns>
Task<Response<UidCookieToken>> GetCookieAccountInfoBySTokenAsync(User user, CancellationToken token = default);
/// <summary>
/// 异步获取 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>uid 与 cookie token</returns>
Task<Response<LTokenWrapper>> GetLTokenBySTokenAsync(User user, CancellationToken token = default);
}

View File

@@ -10,7 +10,7 @@ namespace Snap.Hutao.Web.Hoyolab.Passport;
internal class LoginResult
{
/// <summary>
/// Stoken 包装
/// SToken 包装
/// </summary>
[JsonPropertyName("token")]
public TokenWrapper Token { get; set; } = default!;

View File

@@ -4,14 +4,14 @@
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// Ltoken 包装器
/// LToken 包装器
/// </summary>
[HighQuality]
internal sealed class LtokenWrapper
internal sealed class LTokenWrapper
{
/// <summary>
/// Ltoken
/// LToken
/// </summary>
[JsonPropertyName("ltoken")]
public string Ltoken { get; set; } = default!;
public string LToken { get; set; } = default!;
}

View File

@@ -16,8 +16,8 @@ namespace Snap.Hutao.Web.Hoyolab.Passport;
/// </summary>
[HighQuality]
[UseDynamicSecret]
[HttpClient(HttpClientConfiguration.XRpc2)]
internal sealed class PassportClient2
[HttpClient(HttpClientConfiguration.XRpc2, typeof(IPassportClient))]
internal sealed class PassportClient2 : IPassportClient
{
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
@@ -27,7 +27,7 @@ internal sealed class PassportClient2
/// 构造一个新的通行证客户端
/// </summary>
/// <param name="httpClient">http客户端</param>
/// <param name="options">json序列化选项</param>
/// <param name="options">Json序列化选项</param>
/// <param name="logger">日志器</param>
public PassportClient2(HttpClient httpClient, JsonSerializerOptions options, ILogger<PassportClient> logger)
{
@@ -36,14 +36,17 @@ internal sealed class PassportClient2
this.logger = logger;
}
/// <inheritdoc/>
public bool IsOversea => false;
/// <summary>
/// V1Stoken 登录
/// V1 SToken 登录
/// </summary>
/// <param name="stokenV1">v1 Stoken</param>
/// <param name="stokenV1">v1 SToken</param>
/// <param name="token">取消令牌</param>
/// <returns>登录数据</returns>
[ApiInformation(Salt = SaltType.PROD)]
public async Task<Response<LoginResult>> LoginByStokenAsync(Cookie stokenV1, CancellationToken token)
public async Task<Response<LoginResult>> LoginBySTokenAsync(Cookie stokenV1, CancellationToken token)
{
HttpResponseMessage message = await httpClient
.SetHeader("Cookie", stokenV1.ToString())
@@ -77,18 +80,18 @@ internal sealed class PassportClient2
}
/// <summary>
/// 异步获取 Ltoken
/// 异步获取 LToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>uid 与 cookie token</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.PROD)]
public async Task<Response<LtokenWrapper>> GetLTokenBySTokenAsync(User user, CancellationToken token)
public async Task<Response<LTokenWrapper>> GetLTokenBySTokenAsync(User user, CancellationToken token = default)
{
Response<LtokenWrapper>? resp = await httpClient
Response<LTokenWrapper>? resp = await httpClient
.SetUser(user, CookieType.SToken)
.UseDynamicSecret(DynamicSecretVersion.Gen2, SaltType.PROD, true)
.TryCatchGetFromJsonAsync<Response<LtokenWrapper>>(ApiEndpoints.AccountGetLtokenByStoken, options, logger, token)
.TryCatchGetFromJsonAsync<Response<LTokenWrapper>>(ApiEndpoints.AccountGetLTokenBySToken, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);

View File

@@ -1,107 +0,0 @@
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(HttpClientConfiguration.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;
if (user.SToken == null)
{
return Response.Response.DefaultIfNull(resp);
}
string stoken = user.SToken.GetValueOrDefault(Cookie.STOKEN)!;
// Post json with stoken and uid (stuid/ltuid)
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;
if (user.SToken == null)
{
return Response.Response.DefaultIfNull(resp);
}
string stoken = user.SToken.GetValueOrDefault(Cookie.STOKEN)!;
// Post json with stoken and uid (stuid/ltuid)
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; }
}
}

View File

@@ -0,0 +1,78 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
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>
[HttpClient(HttpClientConfiguration.XRpc3, typeof(IPassportClient))]
internal sealed class PassportClientOversea : IPassportClient
{
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 PassportClientOversea(HttpClient httpClient, JsonSerializerOptions options, ILogger<PassportClient> logger)
{
this.httpClient = httpClient;
this.options = options;
this.logger = logger;
}
/// <inheritdoc/>
public bool IsOversea => true;
/// <summary>
/// 异步获取 CookieToken
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>cookie token</returns>
[ApiInformation(Cookie = CookieType.SToken)]
public async Task<Response<UidCookieToken>> GetCookieAccountInfoBySTokenAsync(User user, CancellationToken token = default)
{
STokenWrapper data = new(user.SToken?.GetValueOrDefault(Cookie.STOKEN)!, user.Aid!);
Response<UidCookieToken>? resp = await httpClient
.SetUser(user, CookieType.SToken)
.TryCatchPostAsJsonAsync<STokenWrapper, 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)]
public async Task<Response<LTokenWrapper>> GetLTokenBySTokenAsync(User user, CancellationToken token = default)
{
STokenWrapper data = new(user.SToken?.GetValueOrDefault(Cookie.STOKEN)!, user.Aid!);
Response<LTokenWrapper>? resp = await httpClient
.SetUser(user, CookieType.SToken)
.TryCatchPostAsJsonAsync<STokenWrapper, Response<LTokenWrapper>>(ApiOsEndpoints.AccountGetLTokenBySToken, data, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
}

View File

@@ -0,0 +1,25 @@
namespace Snap.Hutao.Web.Hoyolab.Passport;
/// <summary>
/// SToken 包装器
/// </summary>
internal sealed class STokenWrapper
{
public STokenWrapper(string stoken, string uid)
{
SToken = stoken;
Uid = uid;
}
/// <summary>
/// SToken
/// </summary>
[JsonPropertyName("stoken")]
public string SToken { get; set; }
/// <summary>
/// Uid
/// </summary>
[JsonPropertyName("uid")]
public string Uid { get; set; }
}

View File

@@ -36,6 +36,22 @@ internal readonly struct PlayerUid
return new(source);
}
/// <summary>
/// 判断是否为国际服
/// We make this a static method rather than property,
/// to avoid unnecessary memory allocation.
/// </summary>
/// <param name="uid">uid</param>
/// <returns>是否为国际服</returns>
public static bool IsOversea(string uid)
{
return uid[0] switch
{
>= '1' and <= '4' => false,
_ => true,
};
}
/// <inheritdoc/>
public override string ToString()
{

View File

@@ -43,13 +43,15 @@ internal sealed class AuthClient
/// <param name="user">用户</param>
/// <returns>操作凭证</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.K2)]
public async Task<Response<ActionTicketWrapper>> GetActionTicketByStokenAsync(string action, User user)
public async Task<Response<ActionTicketWrapper>> GetActionTicketBySTokenAsync(string action, User user)
{
string url = ApiEndpoints.AuthActionTicket(action, user.SToken?[Cookie.STOKEN] ?? string.Empty, user.Aid!);
Response<ActionTicketWrapper>? resp = await httpClient
.SetUser(user, CookieType.SToken)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.K2, true)
.TryCatchGetFromJsonAsync<Response<ActionTicketWrapper>>(ApiEndpoints.AuthActionTicket(action, user.SToken?[Cookie.STOKEN] ?? string.Empty, user.Aid!), options, logger)
.ConfigureAwait(false);
.SetUser(user, CookieType.SToken)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.K2, true)
.TryCatchGetFromJsonAsync<Response<ActionTicketWrapper>>(url, options, logger)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
}
@@ -58,15 +60,20 @@ internal sealed class AuthClient
/// 获取 MultiToken
/// </summary>
/// <param name="cookie">login cookie</param>
/// <param name="isOversea">是否为国际服</param>
/// <param name="token">取消令牌</param>
/// <returns>包含token的字典</returns>
public async Task<Response<ListWrapper<NameToken>>> GetMultiTokenByLoginTicketAsync(Cookie cookie, CancellationToken token)
public async Task<Response<ListWrapper<NameToken>>> GetMultiTokenByLoginTicketAsync(Cookie cookie, bool isOversea, CancellationToken token)
{
string loginTicket = cookie[Cookie.LOGIN_TICKET];
string loginUid = cookie[Cookie.LOGIN_UID];
string url = isOversea
? ApiOsEndpoints.AuthMultiToken(loginTicket, loginUid)
: ApiEndpoints.AuthMultiToken(loginTicket, loginUid);
Response<ListWrapper<NameToken>>? resp = await httpClient
.TryCatchGetFromJsonAsync<Response<ListWrapper<NameToken>>>(ApiEndpoints.AuthMultiToken(loginTicket, loginUid), options, logger, token)
.TryCatchGetFromJsonAsync<Response<ListWrapper<NameToken>>>(url, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);

View File

@@ -1,53 +0,0 @@
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(HttpClientConfiguration.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);
}
}

View File

@@ -1,9 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Web.Hoyolab.Annotation;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
using Snap.Hutao.Web.Response;
using System.Net.Http;
@@ -16,6 +18,7 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
[HttpClient(HttpClientConfiguration.Default)]
internal sealed class BindingClient
{
private readonly IServiceProvider serviceProvider;
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
private readonly ILogger<BindingClient> logger;
@@ -23,18 +26,53 @@ internal sealed class BindingClient
/// <summary>
/// 构造一个新的用户游戏角色提供器
/// </summary>
/// <param name="serviceProvider">服务提供器</param>
/// <param name="httpClient">请求器</param>
/// <param name="options">Json序列化选项</param>
/// <param name="logger">日志器</param>
public BindingClient(HttpClient httpClient, JsonSerializerOptions options, ILogger<BindingClient> logger)
public BindingClient(IServiceProvider serviceProvider, HttpClient httpClient)
{
options = serviceProvider.GetRequiredService<JsonSerializerOptions>();
logger = serviceProvider.GetRequiredService<ILogger<BindingClient>>();
this.serviceProvider = serviceProvider;
this.httpClient = httpClient;
this.options = options;
this.logger = logger;
}
/// <summary>
/// 获取用户角色信息
/// 异步获取用户角色信息
/// 自动判断是否为国际服
/// </summary>
/// <param name="user">用户</param>
/// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns>
public async Task<Response<ListWrapper<UserGameRole>>> GetUserGameRolesOverseaAwareAsync(User user, CancellationToken token = default)
{
if (user.IsOversea)
{
return await GetOverseaUserGameRolesByCookieAsync(user, token).ConfigureAwait(false);
}
else
{
Response<ActionTicketWrapper> actionTicketResponse = await serviceProvider
.GetRequiredService<AuthClient>()
.GetActionTicketBySTokenAsync("game_role", user)
.ConfigureAwait(false);
if (actionTicketResponse.IsOk())
{
string actionTicket = actionTicketResponse.Data.Ticket;
return await GetUserGameRolesByActionTicketAsync(actionTicket, user, token).ConfigureAwait(false);
}
else
{
return Response.Response.DefaultIfNull<ListWrapper<UserGameRole>, ActionTicketWrapper>(actionTicketResponse);
}
}
}
/// <summary>
/// 异步获取用户角色信息
/// </summary>
/// <param name="actionTicket">操作凭证</param>
/// <param name="user">用户</param>
@@ -54,19 +92,17 @@ internal sealed class BindingClient
}
/// <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)
public async Task<Response<ListWrapper<UserGameRole>>> GetOverseaUserGameRolesByCookieAsync(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)
.TryCatchGetFromJsonAsync<Response<ListWrapper<UserGameRole>>>(ApiOsEndpoints.UserGameRolesByCookie, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);

View File

@@ -11,7 +11,7 @@ using System.Net.Http;
namespace Snap.Hutao.Web.Hoyolab.Takumi.Binding;
/// <summary>
/// Stoken绑定客户端
/// SToken绑定客户端
/// </summary>
[HighQuality]
[UseDynamicSecret]
@@ -42,12 +42,12 @@ internal sealed class BindingClient2
/// <param name="token">取消令牌</param>
/// <returns>用户角色信息</returns>
[ApiInformation(Cookie = CookieType.SToken, Salt = SaltType.LK2)]
public async Task<List<UserGameRole>> GetUserGameRolesByStokenAsync(User user, CancellationToken token = default)
public async Task<List<UserGameRole>> GetUserGameRolesBySTokenAsync(User user, CancellationToken token = default)
{
Response<ListWrapper<UserGameRole>>? resp = await httpClient
.SetUser(user, CookieType.SToken)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.LK2, true)
.TryCatchGetFromJsonAsync<Response<ListWrapper<UserGameRole>>>(ApiEndpoints.UserGameRolesByStoken, options, logger, token)
.TryCatchGetFromJsonAsync<Response<ListWrapper<UserGameRole>>>(ApiEndpoints.UserGameRolesBySToken, options, logger, token)
.ConfigureAwait(false);
return EnumerableExtension.EmptyIfNull(resp?.Data?.List);

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
/// <summary>
/// 角色 POST 数据
/// </summary>
internal sealed class CharacterData
{
/// <summary>
/// 构造一个新的角色 POST 数据
/// </summary>
/// <param name="uid">uid</param>
/// <param name="characterIds">角色id</param>
public CharacterData(PlayerUid uid, IEnumerable<AvatarId> characterIds)
{
CharacterIds = characterIds;
Uid = uid.Value;
Server = uid.Region;
}
/// <summary>
/// 角色id
/// </summary>
[JsonPropertyName("character_ids")]
public IEnumerable<AvatarId> CharacterIds { get; }
/// <summary>
/// uid
/// </summary>
[JsonPropertyName("role_id")]
public string Uid { get; } = default!;
/// <summary>
/// 服务器
/// </summary>
[JsonPropertyName("server")]
public string Server { get; }
}

View File

@@ -3,8 +3,6 @@
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;
@@ -18,10 +16,11 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// </summary>
[HighQuality]
[UseDynamicSecret]
[HttpClient(HttpClientConfiguration.XRpc)]
[HttpClient(HttpClientConfiguration.XRpc, typeof(IGameRecordClient))]
[PrimaryHttpMessageHandler(UseCookies = false)]
internal sealed class GameRecordClient
internal sealed class GameRecordClient : IGameRecordClient
{
private readonly IServiceProvider serviceProvider;
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
private readonly ILogger<GameRecordClient> logger;
@@ -30,15 +29,19 @@ internal sealed class GameRecordClient
/// 构造一个新的游戏记录提供器
/// </summary>
/// <param name="httpClient">请求器</param>
/// <param name="options">json序列化选项</param>
/// <param name="logger">日志器</param>
public GameRecordClient(HttpClient httpClient, JsonSerializerOptions options, ILogger<GameRecordClient> logger)
/// <param name="serviceProvider">访问提供器</param>
public GameRecordClient(HttpClient httpClient, IServiceProvider serviceProvider)
{
options = serviceProvider.GetRequiredService<JsonSerializerOptions>();
logger = serviceProvider.GetRequiredService<ILogger<GameRecordClient>>();
this.httpClient = httpClient;
this.options = options;
this.logger = logger;
this.serviceProvider = serviceProvider;
}
/// <inheritdoc/>
public bool IsOversea => false;
/// <summary>
/// 异步获取实时便笺
/// </summary>
@@ -54,16 +57,14 @@ internal sealed class GameRecordClient
.TryCatchGetFromJsonAsync<Response<DailyNote.DailyNote>>(ApiEndpoints.GameRecordDailyNote(userAndUid.Uid.Value), options, logger, token)
.ConfigureAwait(false);
// We hava a verification procedure to handle
// We have a verification procedure to handle
if (resp?.ReturnCode == (int)KnownReturnCode.CODE1034)
{
resp.Message = SH.WebDailyNoteVerificationFailed;
CardVerifier cardVerifier = Ioc.Default.GetRequiredService<CardVerifier>();
CardVerifier cardVerifier = serviceProvider.GetRequiredService<CardVerifier>();
if (await cardVerifier.TryGetXrpcChallengeAsync(userAndUid.User, token).ConfigureAwait(false) is string challenge)
{
Ioc.Default.GetRequiredService<IInfoBarService>().Success(SH.WebDailyNoteSenselessVerificationSuccess);
resp = await httpClient
.SetUser(userAndUid.User, CookieType.Cookie)
.SetXrpcChallenge(challenge)
@@ -151,23 +152,4 @@ internal sealed class GameRecordClient
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; }
}
}

View File

@@ -1,7 +1,8 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
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;
@@ -13,11 +14,10 @@ namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// Hoyoverse game record provider
/// </summary>
[HighQuality]
[UseDynamicSecret]
[HttpClient(HttpClientConfiguration.XRpc3)]
[HttpClient(HttpClientConfiguration.XRpc3, typeof(IGameRecordClient))]
[PrimaryHttpMessageHandler(UseCookies = false)]
internal sealed class GameRecordClientOs
internal sealed class GameRecordClientOversea : IGameRecordClient
{
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
@@ -29,25 +29,28 @@ internal sealed class GameRecordClientOs
/// <param name="httpClient">请求器</param>
/// <param name="options">json序列化选项</param>
/// <param name="logger">日志器</param>
public GameRecordClientOs(HttpClient httpClient, JsonSerializerOptions options, ILogger<GameRecordClient> logger)
public GameRecordClientOversea(HttpClient httpClient, JsonSerializerOptions options, ILogger<GameRecordClient> logger)
{
this.httpClient = httpClient;
this.options = options;
this.logger = logger;
}
/// <inheritdoc/>
public bool IsOversea => true;
/// <summary>
/// 异步获取实时便笺
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>实时便笺</returns>
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OS)]
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OSK2)]
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)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
.TryCatchGetFromJsonAsync<Response<DailyNote.DailyNote>>(ApiOsEndpoints.GameRecordDailyNote(userAndUid.Uid.Value), options, logger, token)
.ConfigureAwait(false);
@@ -60,12 +63,12 @@ internal sealed class GameRecordClientOs
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>玩家的基础信息</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OS)]
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSK2)]
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)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
.TryCatchGetFromJsonAsync<Response<PlayerInfo>>(ApiOsEndpoints.GameRecordIndex(userAndUid.Uid), options, logger, token)
.ConfigureAwait(false);
@@ -79,12 +82,12 @@ internal sealed class GameRecordClientOs
/// <param name="schedule">1当期2上期</param>
/// <param name="token">取消令牌</param>
/// <returns>深渊信息</returns>
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OS)]
[ApiInformation(Cookie = CookieType.Cookie, Salt = SaltType.OSK2)]
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)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, false)
.TryCatchGetFromJsonAsync<Response<SpiralAbyss.SpiralAbyss>>(ApiOsEndpoints.GameRecordSpiralAbyss(schedule, userAndUid.Uid), options, logger, token)
.ConfigureAwait(false);
@@ -98,36 +101,17 @@ internal sealed class GameRecordClientOs
/// <param name="playerInfo">玩家的基础信息</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.X4)]
[ApiInformation(Cookie = CookieType.LToken, Salt = SaltType.OSK2)]
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)
.UseDynamicSecret(DynamicSecretVersion.Gen1, SaltType.OSK2, 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; }
}
}

View File

@@ -0,0 +1,48 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord.Avatar;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
/// <summary>
/// 游戏记录提供器
/// </summary>
internal interface IGameRecordClient : IOverseaSupport
{
/// <summary>
/// 获取玩家角色详细信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="playerInfo">玩家的基础信息</param>
/// <param name="token">取消令牌</param>
/// <returns>角色列表</returns>
Task<Response<CharacterWrapper>> GetCharactersAsync(UserAndUid userAndUid, PlayerInfo playerInfo, CancellationToken token = default);
/// <summary>
/// 异步获取实时便笺
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>实时便笺</returns>
Task<Response<DailyNote.DailyNote>> GetDailyNoteAsync(UserAndUid userAndUid, CancellationToken token = default);
/// <summary>
/// 获取玩家基础信息
/// </summary>
/// <param name="userAndUid">用户与角色</param>
/// <param name="token">取消令牌</param>
/// <returns>玩家的基础信息</returns>
Task<Response<PlayerInfo>> GetPlayerInfoAsync(UserAndUid userAndUid, CancellationToken token = default);
/// <summary>
/// 获取玩家深渊信息
/// </summary>
/// <param name="userAndUid">用户</param>
/// <param name="schedule">1当期2上期</param>
/// <param name="token">取消令牌</param>
/// <returns>深渊信息</returns>
Task<Response<SpiralAbyss.SpiralAbyss>> GetSpiralAbyssAsync(UserAndUid userAndUid, SpiralAbyssSchedule schedule, CancellationToken token = default);
}

View File

@@ -23,7 +23,7 @@ internal sealed class HomaSpiralAbyssClient
{
private readonly HttpClient httpClient;
private readonly GameRecordClient gameRecordClient;
private readonly GameRecordClientOs gameRecordClientOs;
private readonly GameRecordClientOversea gameRecordClientOs;
private readonly JsonSerializerOptions options;
private readonly ILogger<HomaSpiralAbyssClient> logger;
@@ -34,7 +34,7 @@ internal sealed class HomaSpiralAbyssClient
/// <param name="gameRecordClient">游戏记录客户端</param>
/// <param name="options">json序列化选项</param>
/// <param name="logger">日志器</param>
public HomaSpiralAbyssClient(HttpClient httpClient, GameRecordClient gameRecordClient, GameRecordClientOs gameRecordClientOs, JsonSerializerOptions options, ILogger<HomaSpiralAbyssClient> logger)
public HomaSpiralAbyssClient(HttpClient httpClient, GameRecordClient gameRecordClient, GameRecordClientOversea gameRecordClientOs, JsonSerializerOptions options, ILogger<HomaSpiralAbyssClient> logger)
{
this.httpClient = httpClient;
this.gameRecordClient = gameRecordClient;

View File

@@ -40,6 +40,12 @@ internal class Response
[JsonPropertyName("message")]
public string Message { get; set; } = default!;
/// <summary>
/// 返回本体或带有消息提示的默认值
/// </summary>
/// <param name="response">本体</param>
/// <param name="callerName">调用方法名称</param>
/// <returns>本体或默认值,当本体为 null 时 返回默认值</returns>
public static Response DefaultIfNull(Response? response, [CallerMemberName] string callerName = default!)
{
// 0x26F19335 is a magic number that hashed from "Snap.Hutao"
@@ -59,6 +65,29 @@ internal class Response
return response ?? new(0x26F19335, $"[{callerName}] 中的 [{typeof(TData).Name}] 请求异常", default);
}
/// <summary>
/// 返回本体或带有消息提示的默认值
/// </summary>
/// <typeparam name="TData">类型</typeparam>
/// <typeparam name="TOther">其他类型</typeparam>
/// <param name="response">本体</param>
/// <param name="callerName">调用方法名称</param>
/// <returns>本体或默认值,当本体为 null 时 返回默认值</returns>
public static Response<TData> DefaultIfNull<TData, TOther>(Response<TOther>? response, [CallerMemberName] string callerName = default!)
{
if (response != null)
{
Must.Argument(response.ReturnCode != 0, "返回代码必须为0");
// 0x26F19335 is a magic number that hashed from "Snap.Hutao"
return new(response.ReturnCode, response.Message, default);
}
else
{
return new(0x26F19335, $"[{callerName}] 中的 [{typeof(TData).Name}] 请求异常", default);
}
}
/// <inheritdoc/>
public override string ToString()
{