mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
phase 2
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -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/
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
32
src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs
Normal file
32
src/Snap.Hutao/Snap.Hutao.Test/DependencyInjectionTest.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
1
src/Snap.Hutao/Snap.Hutao.Test/GlobalUsings.cs
Normal file
1
src/Snap.Hutao/Snap.Hutao.Test/GlobalUsings.cs
Normal file
@@ -0,0 +1 @@
|
||||
global using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
22
src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj
Normal file
22
src/Snap.Hutao/Snap.Hutao.Test/Snap.Hutao.Test.csproj
Normal 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>
|
||||
@@ -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
|
||||
|
||||
@@ -62,7 +62,7 @@ internal static class CoreEnvironment
|
||||
[SaltType.PROD] = "JwYDpKvLj6MrMqqYU6jTKF17KNO2PXoS",
|
||||
|
||||
// This SALT is not reliable
|
||||
[SaltType.OS] = "6cqshh5dhw73bzxn20oexa9k516chk7s",
|
||||
[SaltType.OSK2] = "6cqshh5dhw73bzxn20oexa9k516chk7s",
|
||||
}.ToImmutableDictionary();
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
/// 有名称的对象
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -24,7 +24,7 @@ internal enum RefreshOption
|
||||
/// <summary>
|
||||
/// 通过Stoken刷新
|
||||
/// </summary>
|
||||
Stoken,
|
||||
SToken,
|
||||
|
||||
/// <summary>
|
||||
/// 手动输入Url刷新
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -48,9 +48,9 @@
|
||||
<AppBarButton.Flyout>
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuFlyoutItem
|
||||
Command="{Binding RefreshByStokenCommand}"
|
||||
Command="{Binding RefreshBySTokenCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
Text="{shcm:ResourceString Name=ViewPageGachaLogRefreshByStoken}"/>
|
||||
Text="{shcm:ResourceString Name=ViewPageGachaLogRefreshBySToken}"/>
|
||||
<MenuFlyoutItem
|
||||
Command="{Binding RefreshByWebCacheCommand}"
|
||||
Icon="{shcm:FontIcon Glyph=}"
|
||||
@@ -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=}"
|
||||
IsClickEnabled="True"/>
|
||||
<clw:SettingsCard
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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}®ion={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}®ion={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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -40,7 +40,7 @@ internal enum SaltType
|
||||
LK2,
|
||||
|
||||
/// <summary>
|
||||
/// 国际服
|
||||
/// Hoyolab K2
|
||||
/// </summary>
|
||||
OS,
|
||||
OSK2,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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!;
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user